Service Worker那些缓存坑

Nenge123 ServiceWorker

前言

ServiceWorker总会存在意想不到的问题.例如增加缓存,制造虚拟地址等,内容不更新等等.因此我们要赋予客户端控制权.

脚本状态变化

当修改CACHE_LIST时,在关闭浏览器之前,发生的变化不会失去.当我重新打开时,CACHE_LIST值被还原到安装状态. 原因是因为ServiceWorker安装时,也就是脚本初始化的状态.发生交互时的交互状态是临时的.当重启浏览器时会回归初始化状态.当脚本内容发生变化时,ServiceWorker会触发重新安装,也就是重置初始化状态.但是苹果的手机浏览器会缓存ServiceWorker脚本,导致脚本不会立即更新.这就导致更新不同步.

"use strict";
const CACHE_NAME = 'NENGE.NET';
const version = Date.parse('2023 09/23 19:40');
const CACHE_LIST = [];
importScripts("/assets/js/lib/zip.min.js?"+version);
Object.entries({
    install(event) {
        console.log('serviceWorker install');
        return self.skipWaiting();
    },
    activate(event) {
        console.log('serviceWorker activate');
        return event.waitUntil(checkList().then(e => self.skipWaiting()));
    },
    async fetch(event) {
        let request = event.request;
        let url = request.url.replace(location.origin,'');
        //把完整url替换成短地址.
        if(!CACHE_LIST.length){
            //缓存未初始化
            return event.respondWith(CheckCache(request));
        }
        if(CACHE_LIST.includes(url)){
            //记录中的缓存 直接返回
            return event.respondWith(loadCache(request));
        }
        return false;
    },
    async message(event) {
        let data = event.data;
        //event.source;
        console.log(data);
    }
}).forEach(
    entry => self.addEventListener(entry[0], entry[1])
);

async function CheckCache(request){
    await checkList();
    let cache = await caches.open(CACHE_NAME);
    let response = await cache.match(request);
    if(response) return response;
    return fetch(request);
}
async function checkList(){
    const cache = await caches.open(CACHE_NAME);
    const list = await cache.keys();
    if(!CACHE_LIST.length){
        //添加一个默认值
        CACHE_LIST.push("/assets/js/lib/zip.min.js?"+version);
    }
    list.forEach(v=>{
        let url = v.url.replace(location.origin,'');
        if(/zip\.min\.js/.test(url)&&url.indexOf(version)===false){
            cache.delete(v);
        }else if(!CACHE_LIST.includes(url))CACHE_LIST.push(url);
    });
}
async function loadCache(request){
    let DB = await caches.open(CACHE_NAME);
    let response = await DB.match(request);
    if(!response){
        response = await fetch(request);
        DB.put(request, response.clone());
    }
    return response;
}

引入交互

为了确保消息不重叠,客户端发送的消息含有clientID作为回调函数标记,而ServiceWorker请求客户端交互含有workerId作为回调函数. 客户端可以随时控制缓存更新,并不需要每次更新时,需要重新安装ServiceWorker!

    //事件处理 self.addEventListener('message',message);
    async function message(event) {
        let data = event.data;
        Client = event.source; //此处记录了客户端的通信对象
        console.log(data);
        if (!data && data.constructor !== Object) return;
        let id = data.workerId;
        if (action[id]) return action[id](data);
        else{
            switch(data.method){
                //要求work缓存链接
                case 'setcache':{
                    if(data.result){
                        let cache = await caches.open(CACHE_NAME);
                        await Promise.all(data.result.forEach(async v=>{
                            if(!CACHE_LIST.includes(v)){
                                CACHE_LIST.push(url);
                                cache.put(url,await fetch(url));
                            }
                        }));
                    }
                    data.result = CACHE_LIST;
                    postMessage(data);
                    break;
                }
               //要求work删除缓存
                case 'resetcache':{
                    if(data.result){
                        let cache = caches.open(CACHE_NAME);
                        await Promise.all(data.result.map(async v=>{
                            if((await cache).match(v)){
                                (await cache).delete(v);
                                let index = CACHE_LIST.indexOf(v);
                                if(v!==false){
                                    CACHE_LIST.splice(index,1);
                                }
                            }
                        }));
                        data.result = CACHE_LIST;
                    }
                    postMessage(data);
                    break;
                }
                //让serviceWorker解压一个zip
                case 'unpack':{
                    if(!data.result){
                        data.result = null;
                        return postMessage(data);
                    }
                    data.result = await unzip(data.result,data.password,data.clientID);
                    postMessage(data);
                    break;
                }
            }
        }
    }

交互处理函数

ServiceWorker函数处理

    //zip.js 解压函数
    async function unzip(contents,password,clientID){        
        const ReaderList = await new zip.ZipReader(new zip.BlobReader(new Blob([contents]))).getEntries().catch(e=>null);
        if(!ReaderList||!ReaderList.length) return null;
        let result;
        if(!clientID)password = false;
        const getData = (entry)=>{
            return entry.getData(new zip.Uint8ArrayWriter(), {password:entry.encrypted&&password!==false?password:undefined, onprogress: (current, total) =>clientID&&postMessage({clientID,current, total,filename:entry.filename,state:'progress'})}).catch(async e=>{
                let msg = e.message;
                //当且设置false 跳过加密文件
                if(password===false) return;
                if(msg == zip.ERR_INVALID_PASSWORD||msg==zip.ERR_ENCRYPTED){
                    password = await getMessage({clientID,state:'password',password});
                    if(password){
                        return await getData(entry);
                    }else{
                        password = false;
                    }
                }
            });
        }
        if(ReaderList){
            for await(let entry of ReaderList){
                if(entry.directory)continue;
                let data = await getData(entry);
                if(data){
                    if(!result)result={};
                    result[entry.filename] = data;
                }
            }
        }
        return result||null;
    }
    let Client;
    //记录回调函数
    const action = {};
    //交互 对客户端发送消息并且等待消息返回
    async function getMessage(ARG){
        let id = getRandom();
        ARG.workerId = id;
        return new Promise(back=>{
            action[id] = (data)=>{
                back(data.result);
                delete action[id];
            };
            postMessage(ARG);
        });
    }
    //生成一个唯一ID
    function getRandom(){
        return crypto&&crypto.randomUUID()||Date.now()+Math.random();
    }
    //发送客户端消息
    function postMessage(str,bool) {
        if(bool)return clients.matchAll().then(client=>client.postMessage(str));
        getClient().then(client => client && client.postMessage(str));
    }
    //确保当前通信客户端对象正处于浏览中
    function isVisible(client) {
        return client.visibilityState == 'visible';
    }
    //遍历可用客户端通信接口
    async function getClient() {
        let { Client, isVisible } = self;
        if (Client && isVisible(Client)) {
            return Client;
        }
        let clients = await self.clients.matchAll();
        return clients.filter(v => isVisible(v))[0] || Client || clients[0];
    }

客户端

发送一个解压请求await ZipWorker(u8array);

        const action = {};
        async postMessage(str,sw) {
            if(!sw){
                //确保 serviceWorker对象激活中
                sw = navigator.serviceWorker.ready;
                if(sw instanceof Promise){
                    sw = (await sw).active;
                }else if(self.sw){
                    sw = self.sw;
                }
            }
            sw&&sw.postMessage(str);
        }
        async function ZipWorker(contents,progress,pwFn){
            let clientID = Date.now()+Math.random();
            if(!contents) return contents;
            return new Promise(back=>{
                action[clientID] = async function(data){
                    if(data.result !== undefined){
                        back(data.result);
                        delete action[clientID];
                    }else if(data.state=='progress'){
                        progress&&progress(data.current, data.total,data.filename);
                    }else if(data.state=='password'){
                        let password = pwFn? await pwFn(data.password):prompt('请输入密码', data.password);
                        data.result = password;
                        postMessage(data);
                    }
                }
                postMessage({
                    clientID,
                    method:'unpack',
                    result:contents
                })

            });
        }

评论