service-worker

/**
 sw created by xiaogang on 2018/1/12
 功能描述:简单的实现缓存 功能!
 */

/**
 * sw更新 方案汇总
 *
 *      1、更新 cacheName 等待浏览器重启之后 新的sw 文件接管 (需要配合 activate 事件)
 *      2、install event 中 执行 self.skipWaiting(); 只需要sw文件有任何的更新 既可实现更新 (其实触发了install事件,重新拉取文件实现更新缓存!cache.addAll(cacheFiles) 不会被fetch拦截代理)
 *
 * cacheFiles 更新方案汇总
 *      1、永不更新 :除非sw 触发更新了
 *      2、实时更新 :增加动态时间戳 永不命中
 *      3、定时更新 :timeout 参数控制 (两种方案实现,业务层 或者 sw层.具体看ajax.js)
 *      4、下次更新 :优先取缓存,同时异步取更新缓存
 *
 */

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 事件(生命周期)
 *      缓存更新的关键! (缓存管理控制!)
 *
 *      1、更新您的服务工作线程 JavaScript 文件。用户导航至您的站点时,浏览器会尝试在后台重新下载定义服务工作线程的脚本文件。如果服务工作线程文件与其当前所用文件存在字节差异,则将其视为“新服务工作线程”。
 *      2、新服务工作线程将会启动,且将会触发 install 事件。
 *      3、此时,旧服务工作线程仍控制着当前页面,因此新服务工作线程将进入 waiting 状态。
 *      4、当网站上当前打开的页面关闭时,旧服务工作线程将会被终止,新服务工作线程将会取得控制权。
 *      5、新服务工作线程取得控制权后,将会触发其 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}
 */
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}
 */
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 配置文件(记得是域名根目录下)

参考:
https://github.com/wxungang/c...

你可能感兴趣的:(service-worker,pwa)