service-worker

/**
 sw created by xiaogang on 2018/1/12
 功能描述:簡單的實現緩存 功能!
 */

/**
 * sw更新 方案彙總
 *
 *      一、更新 cacheName 等待瀏覽器重啓以後 新的sw 文件接管 (須要配合 activate 事件)
 *      二、install event 中 執行 self.skipWaiting(); 只須要sw文件有任何的更新 既可實現更新 (其實觸發了install事件,從新拉取文件實現更新緩存!cache.addAll(cacheFiles) 不會被fetch攔截代理)
 *
 * cacheFiles 更新方案彙總
 *      一、永不更新 :除非sw 觸發更新了
 *      二、實時更新 :增長動態時間戳 永不命中
 *      三、定時更新 :timeout 參數控制 (兩種方案實現,業務層 或者 sw層.具體看ajax.js)
 *      四、下次更新 :優先取緩存,同時異步取更新緩存
 *
 */

const dataUrl = new RegExp('\.s?json'); //異步實時更新的數據 規則

const cacheName = 'sw_cache_update';
/**
 * 須要緩存的 文件列表
 * @type {string[]}
 */
const cacheFiles = [
    './script/ajax.js',
    './script/page.js',
    './style/page.css'
];

const cacheWhitelist = ['index.html'];

/**
 * sw install 事件(生命週期)
 *      推薦作緩存 工做!(也能夠什麼都不作)
 *
 * caches.open(cacheName).then()
 *      緩存相關操做 徹底能夠在頁面加載等任什麼時候候處理。(具體參考 官方文檔 或者 cache 目錄demos)
 *      只是單純的緩存起來,不配合sw.fetch 無法 作攔截代理。因此推薦在install成功以後 再緩存
 *
 */
self.addEventListener('install', InstallEvent => {
    //sw 更新以後當即 接管替換舊的 sw (無需 activate 事件)
    self.skipWaiting();
    // console.log(InstallEvent);
    //waitUntil :
    InstallEvent.waitUntil(
        //連接 對應的cache ,並進行下載 & 緩存 數據!
        caches.open(cacheName).then(cache => {
            cacheKey(cache, 'cacheFiles added before');
            return cache.addAll(cacheFiles).then(data => {
                console.log(data);//undefined
                cacheKey(cache, 'cacheFiles added after');
            });
        })
    );
});


/**
 * fetch 事件(生命週期)
 *      實現 緩存的關鍵。能夠攔截和代理 頁面的請求!
 *
 * 問題:
 *      怎麼更新!!!!
 *
 *
 */
self.addEventListener('fetch', FetchEvent => {
    // console.log(FetchEvent);

    if (dataUrl.test(FetchEvent.request.url)) {
        //先取緩存 後當即更新緩存
        FetchEvent.respondWith(
            caches.match(FetchEvent.request).then(response => {
                if (response) {
                    fetchRequest(FetchEvent.request).then(data => {
                        console.log(data);
                    });
                    return responseTimeout(response, FetchEvent.request)
                } else {
                    return fetchRequest(FetchEvent.request);
                }

            })
        );
    } else {
        //優先緩存
        FetchEvent.respondWith(
            caches.match(FetchEvent.request).then(response => {
                //已經緩存 直接返回 。沒有則從新fetch 同時更新到緩存中!
                return responseTimeout(response, FetchEvent.request) || fetchRequest(FetchEvent.request)
            })
        );
    }
});

/**
 * activate 事件(生命週期)
 *      緩存更新的關鍵! (緩存管理控制!)
 *
 *      一、更新您的服務工做線程 JavaScript 文件。用戶導航至您的站點時,瀏覽器會嘗試在後臺從新下載定義服務工做線程的腳本文件。若是服務工做線程文件與其當前所用文件存在字節差別,則將其視爲「新服務工做線程」。
 *      二、新服務工做線程將會啓動,且將會觸發 install 事件。
 *      三、此時,舊服務工做線程仍控制着當前頁面,所以新服務工做線程將進入 waiting 狀態。
 *      四、當網站上當前打開的頁面關閉時,舊服務工做線程將會被終止,新服務工做線程將會取得控制權。
 *      五、新服務工做線程取得控制權後,將會觸發其 activate 事件。
 *
 *      //install event中  使用self.skipWaiting(); 可使 sw 更新以後當即 接管替換舊的 sw (無需 activate 事件)
 */
self.addEventListener('activate', ActivateEvent => {
    console.log(ActivateEvent);
    //
    ActivateEvent.waitUntil(
        updateCacheName()
    );
    /*
     * Fixes a corner case in which the app wasn't returning the latest data.
     * You can reproduce the corner case by commenting out the line below and
     * then doing the following steps: 1) load app for first time so that the
     * initial New York City data is shown 2) press the refresh button on the
     * app 3) go offline 4) reload the app. You expect to see the newer NYC
     * data, but you actually see the initial data. This happens because the
     * service worker is not yet activated. The code below essentially lets
     * you activate the service worker faster.
     */
    return self.clients.claim();
});

/**
 * 經過 更新cacheName 來 實現更新(即 :刪除舊的 caches[cacheName])
 * @returns {Promise<any[]>}
 */
function updateCacheName() {
    //caches.keys(): 返回 一個 promise
    return caches.keys().then(cacheNameList => {
        console.log(cacheNameList);
        return Promise.all(
            cacheNameList.map(_cacheName => {
                console.log(_cacheName);
                if (_cacheName !== cacheName) {
                    return caches.delete(_cacheName);
                }
            })
        );
    })
}

/**
 *
 * @param request
 * @param response
 */
function updateCache(request, response) {
    caches.open(cacheName)
        .then(function (cache) {
            cache.put(request, response);
        });
}

/**
 *
 * @param request
 * @returns {Promise<Response>}
 */
function fetchRequest(request) {
    /**
     * 在 fetch 請求中添加對 .then() 的回調。
     * 得到響應後,執行如下檢查:
     * 確保響應有效。
     * 檢查並確保響應的狀態爲 200。
     * 確保響應類型爲 basic,亦即由自身發起的請求。 這意味着,對第三方資產的請求不會添加到緩存。
     * 若是經過檢查,則克隆響應。這樣作的緣由在於,該響應是 Stream,所以主體只能使用一次。因爲咱們想要返回能被瀏覽器使用的響應,並將其傳遞到緩存以供使用,所以須要克隆一份副本。咱們將一份發送給瀏覽器,另外一份則保留在緩存。
     *
     */
    return fetch(request).then(response => {
        // Check if we received a valid response
        if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
        }
        //
        if (cacheWhitelist.indexOf('') >= 0) {
        }
        // IMPORTANT: Clone the response. A response is a stream
        // and because we want the browser to consume the response
        // as well as the cache consuming the response, we need
        // to clone it so we have two streams.
        updateCache(request, response.clone());
        //todo updateCache() 也能夠放到 調用以後 自行處理!
        // fetchRequest(FetchEvent.request).then(data => {
        //     console.log(data);
        //     updateCache();
        // });

        return responseTimeout(response, request);//response
    });
}

//response 只讀 (無法把 timeout 參數寫入對應的response中去,只能定義一個全局的變量:有點坑!)
let _requestTimeout = {};

/**
 * 定時 timeout 內不重複調用!
 * sw 層統一封裝
 * @param response
 * @param request
 * @returns {*}
 */
function responseTimeout(response, request) {
    if (!response) {
        //response 不存在直接 返回獲取 服務端最新的
        return response
    }
    let _url2Obj = url2Obj(request.url);
    let _timeout = _url2Obj.timeout || 0;
    let _resTimeout = _requestTimeout[response.url];

    let _now = (new Date()).getTime() / 1000; //單位秒
    console.log(response.timeout);

    if (_timeout && _resTimeout) {
        console.log(request.url);
        console.log(_now - _resTimeout);
        // 請求設置 _timeout & response 有更新 timeout (即 不是第一次)
        if (_now - _resTimeout > _timeout) {
            // 過時 刪除 緩存
            caches.open(cacheName)
                .then(function (cache) {
                    cache.delete(request).then(data => {
                        if (data) {
                            delete _requestTimeout[response.url];
                        }

                    });
                });
        }
        //刪除以後當即獲取最新的數據 (或者 下次獲取 最新的數據)
        // return null;
    } else {
        console.log(response);
        // 請求沒有設置 _timeout 或者 response 沒有 timeout (即 第一次請求)
        // response.timeout = _now;
        // response.__proto__.timeout = _now;
        _requestTimeout[response.url] = _now
    }
    console.log('------------_requestTimeout--------------');
    console.log(_requestTimeout)
    return response;
}


/**
 * 沒啥 實際做用。
 * @param cache
 * @param tips
 */
function cacheKey(cache, tips) {
    cache.keys().then(cacheKeyList => {
        console.log(`-------caches[${cacheName}]------${tips}--------------`);
        console.log(cacheKeyList);//request 對象
    });
}

function url2Obj(str) {
    if (!str) {
        //單頁面 hash 模式下 search ='';
        str = location.search || location.hash || location.href;
    }

    var query = {};

    str.replace(/([^?&=]*)=([^?&=]*)/g, function (m, a, d) {
        if (typeof query[a] !== 'undefined') {
            query[a] += ',' + decodeURIComponent(d);
        } else {
            query[a] = decodeURIComponent(d);
        }
    });

    return query;
}

實現pwa只需添加 manifest.json 配置文件(記得是域名根目錄下)css

參考:
https://github.com/wxungang/c...html

相關文章
相關標籤/搜索