来用Service worker吧

Posted 恪愚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了来用Service worker吧相关的知识,希望对你有一定的参考价值。

之前说了笔者写了一个微前端框架。在微前端中子应用切换前要先获取到子应用的资源(比如js、css)和 html 片段进行加载。那么这里就涉及到一个“老生常谈”的话题:资源缓存

为了缓存子应用的 js、css资源,笔者分两步进行:首先是在主应用加载时“预加载”子应用:

export const prefetch = async () => 
    // 获取到所有子应用的列表,但不包括当前正在显示的
    const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))

    // 预加载剩下的所有子应用
    await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))

然后是在资源请求前后判断缓存、存入缓存:

const cache =  //根据子应用的name做缓存

export const parseHtml = async (entry, name) => 
    if(cache[name]) 
        return cache[name]
    
    // 资源加载其实是一个get请求,我们去模拟这个过程
    const html = await fetchResource(entry)
    let allScripts = []
    //...
    // 抽离标签、link、script(src、代码)
    const [dom, scriptUrl, script] = await getResources(div, entry)
    // 获取所有的js资源
    const fetchedScripts = await Promise.all(scriptUrl.map(async item => await fetchResource(item)))
    allScripts = script.concat(fetchedScripts)
    cache[name] = [dom, allScripts]
    //...

这样就达到了提高响应速度的效果。

那在普通项目中呢?
Service Worker API 能够很好的帮助我们完成这个功能:资源缓存、请求缓存。

拿静态资源来说,使用时先注册:

function register() 
    if('serviceWorker' in navigator) 
        // 注册,可以给一个scope参数作为作用域
        navigator.serviceWorker.register('/worker文件路径', scope: '作用域路径')
            .then((res) => 
                if(res.installing) 
                    // 注册成功
                 else if (res.waiting) 
                    // 注册过了
                 else if (res.active) 
                    // 已经开启
                
            )
            .catch((err) => 
                console.log('register failed with: ' + err)
            )
    

然后进入到 worker 文件中。

一个service注册成功后必然进行install 事件,这里我们可以进行一些静态资源的 cache,以防止在后面的请求时依然对这些资源发送请求!

self.addEventListener('install', function(event) 
    event.waitUntil(
        caches.open("缓存名(key)")
            .then(function(cache) 
                console.log('[SW]: Opened cache');
                return cache.addAll(静态资源列表);
            )
    );
);

这里还有一个需要注意的地方:
如果是第一次加载 sw ,在install后,会直接进入activated阶段,而如果 sw 进行更新,情况就会显得复杂一些:当新的 sw 进入install阶段,而老的那个还处于工作状态,新的那个就会进入waiting阶段。只有等到老的被terminated后,新的才能正常替换老的那个的工作。

如果你不想等待,可以试试把它加到install的“最前面”:

//跳过等待过程
self.skipWaiting();

然后就进入了activated阶段,激活 sw 工作。
activated阶段我们主要用于处理缓存,比如更新存储在cache中的key和value:

self.addEventListener('activate', function(event) 
    //清除cache中原来老的一批相同key的数据
    var setOfExpectedUrls = new Set(缓存列表的key-value二维数组.values());
    event.waitUntil(
        caches.open(缓存名).then(function(cache) 
            return cache.keys().then(function(existingRequests) 
                return Promise.all(
                    existingRequests.map(function(existingRequest) 
                        if (!setOfExpectedUrls.has(existingRequest.url)) 
                            //cache中删除指定对象
                            return cache.delete(existingRequest);
                        
                    )
                );
            );
        ).then(function() 
            //self相当于webworker线程的当前作用域
            //当一个 service worker 被初始注册时,页面在下次加载之前不会使用它。claim() 方法会立即控制这些页面
            //从而更新客户端上的serviceworker
            return self.clients.claim();

        )
    );
);

然后最关键的就是 fetch 阶段了。

var isPathWhitelisted = function(whitelist, absoluteUrlString) 
    // If the whitelist is empty, then consider all URLs to be whitelisted.
    if (whitelist.length === 0) 
        return true;
    
    // Otherwise compare each path regex to the path of the URL passed in.
    var path = (new URL(absoluteUrlString)).pathname;
    return whitelist.some(function(whitelistedPathRegex) 
        return path.match(whitelistedPathRegex);
    );
;

self.addEventListener('fetch', function(event) 
    if (event.request.method === 'GET') 
        // 标识位,用来判断是否需要缓存
        var shouldRespond;
        // 处理参数
        
        // 再次验证,判断其是否是一个navigation类型的请求
        var navigateFallback = '';
        if (!shouldRespond &&
            navigateFallback &&
            (event.request.mode === 'navigate') &&
            isPathWhitelisted([], event.request.url)) 
            url = new URL(navigateFallback, self.location).toString();
            shouldRespond = urlsToCacheKeys.has(url);
        
        // 如果标识位为true
        if (shouldRespond) 
            event.respondWith(
                caches.open(cacheName).then(function(cache) 
                    //去缓存cache中找对应的url的值
                    return cache.match(urlsToCacheKeys.get(url)).then(function(response) 
                        //如果找到了,就返回value
                        if (response) 
                            return response;
                        
                        throw Error('The cached response that was expected is missing.');
                    );
                ).catch(function(e) 
                    // 如果没找到则请求该资源、
                    return fetch(event.request);
                )
            );
        
    
);

先校验请求url和参数。然后判断 cache 中是否有缓存,没有的话就请求资源。

注意:上面说的是“静态资源缓存”,也就是缓存列表是固定的。但是如果你想要缓存普通请求或者其他资源,就不能只是简单的fetch一下了:

self.addEventListener('fetch', function (evt) 
	//处理
    // 在发起请求时候会触发fetch事件
    evt.respondWith(
        caches.match(evt.request).then(function (response) 
            // 如果 sw 已经保存了请求的响应,直接返回响应,减少http请求
            if (response !== undefined) 
                return response
            
            // 不存在需要发起请求
            return fetch(evt.request).then((httpRes) => 
                if (!httpRes || httpRes !== 200) 
                    // 请求出错则直接返回错误信息
                    return httpRes
                
                // 将响应复制一份
                const httpResClone = httpRes.clone()
                // 并且保存到安装时候的缓存对象里
                caches.open(缓存名).then((cache) => 
                    cache.put(evt.request, httpResClone)
                )
                return httpRes
            )
        )
    )
)

当cache里面没有缓存,则使用fetch发起请求,这个Fetch发起请求的是用来代替XMLHttpRequest来发请求的方案;当请求响应了错误直接返回错误信息,当请求响应成功的情况,更新cache缓存,将新的响应存入cache缓存中,下次在访问就直接从缓存中读取。

以上是关于来用Service worker吧的主要内容,如果未能解决你的问题,请参考以下文章

来用Service worker吧

解决网络不可用--Using_Service_Workers

如何通过 cdn 脚本注册 service worker。如何使用外部 url 注册 service worker

web worker 与 service worker

Using Service Workers

Service Worker 与 Web Worker