service worker静态资源离线缓存实践

前记

早在半年前,在公司内部的前端研习会上,就在研究pwa这个东东了,我负责的特色恰好是用service-worker来实现资源缓存。因此以前就已经尝试在本地的富途资讯页面中引入pwa了,后面也在准备着要在正式环境中也引入,可一直没有好的机会。javascript

而就在上上个星期,收到用户反馈,种子页面进不去,一直卡着,上服务器上查看了nginx日志,发现是资源响应太慢超时了(这个站点没有使用cdn资源),js等资源没有加载出来。种子这个页面又是用前端渲染的,因此用户就一直白屏了。css

恰好,组内的技术建设一直在准备引入pwa这个东西,此次正好能够如今种子这个影响面不会太广而且更新太频繁的页面来作实验,万一出了问题影响面也不会太大。。前端

因而就吭哧吭哧地开干了java

首先问了一下公司内的同事,发现并无人在正式环境中引入过sw。。看来我是第一个吃螃蟹的人,刺激。。webpack

一. service worker介绍

service worker的由来

service worker是浏览器的一个高级特性,本质是一个web worker,是独立于网页运行的脚本。 web worker这个api被造出来时,就是为了释放主线程。由于,浏览器中的JavaScript都是运行在单一个线程上,随着web业务变得愈来愈复杂,js中耗时间、耗资源的运算过程则会致使各类程度的性能问题。 而web worker因为独立于主线程,则能够将一些复杂的逻辑交由它来去作,完成后再经过postMessage的方法告诉主线程。 service worker则是web worker的升级版本,相较于后者,前者拥有了持久离线缓存的能力。ios

service worker的特色

sw有如下几个特色:nginx

  • 独立于主线程、在后台运行的脚本
  • 被install后就永远存在,除非被手动卸载
  • 可编程拦截请求和返回,缓存文件。sw能够经过fetch这个api,来拦截网络和处理网络请求,再配合cacheStorage来实现web页面的缓存管理以及与前端postMessage通讯。
  • 不能直接操纵dom:由于sw是个独立于网页运行的脚本,因此在它的运行环境里,不能访问窗口的window以及dom。
  • 必须是https的协议才能使用。不过在本地调试时,在http://localhost 和http://127.0.0.1 下也是能够跑起来的。
  • 异步实现,sw大量使用promise。

service worker的生命周期

service worker从代码的编写,到在浏览器中的运行,主要通过下面几个阶段 installing -> installed -> activating -> activated -> redundant; web

installing: 这个状态发生在service worker注册以后,表示开始安装。在这个过程会触发install事件回调指定一些静态资源进行离线缓存。chrome

installed: sw已经完成了安装,进入了waiting状态,等待其余的Service worker被关闭(在install的事件回调中,能够调用skipWaiting方法来跳过waiting这个阶段)编程

activating: 在这个状态下没有被其余的 Service Worker 控制的客户端,容许当前的 worker 完成安装,而且清除了其余的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。

activated: 在这个状态会处理activate事件回调,并提供处理功能性事件:fetch、sync、push。(在acitive的事件回调中,能够调用self.clients.claim())

redundant: 废弃状态,这个状态表示一个sw的使命周期结束

service worker代码实现

//在页面代码里面监听onload事件,使用sw的配置文件注册一个service worker
 if ('serviceWorker' in navigator) {
        window.addEventListener('load', function () {
            navigator.serviceWorker.register('serviceWorker.js')
                .then(function (registration) {
                    // 注册成功
                    console.log('ServiceWorker registration successful with scope: ', registration.scope);
                })
                .catch(function (err) {
                    // 注册失败
                    console.log('ServiceWorker registration failed: ', err);
                });
        });
    }
复制代码
//serviceWorker.js
var CACHE_NAME = 'my-first-sw';
var urlsToCache = [
    '/',
    '/styles/main.css',
    '/script/main.js'
];

self.addEventListener('install', function(event) {
    // 在install阶段里能够预缓存一些资源
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(urlsToCache);
            })
    );
});

//在fetch事件里能拦截网络请求,进行一些处理
self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request).then(function (response) {
            // 若是匹配到缓存里的资源,则直接返回
            if (response) {
                return response;
            }
          
            // 匹配失败则继续请求
            var request = event.request.clone(); // 把原始请求拷过来

            //默认状况下,从不支持 CORS 的第三方网址中获取资源将会失败。
            // 您能够向请求中添加 no-CORS 选项来克服此问题,不过这可能会致使“不透明”的响应,这意味着您没法辨别响应是否成功。
            if (request.mode !== 'navigate' && request.url.indexOf(request.referrer) === -1) 						{
                request = new Request(request, { mode: 'no-cors' })
            }

            return fetch(request).then(function (httpRes) {
								//拿到了http请求返回的数据,进行一些操做
              
              	//请求失败了则直接返回、对于post请求也直接返回,sw不能缓存post请求
                if (!httpRes  || ( httpRes.status !== 200 && httpRes.status !== 304 && httpRes.type !== 'opaque') || request.method === 'POST') {
                    return httpRes;
                }

                // 请求成功的话,将请求缓存起来。
                var responseClone = httpRes.clone();
                caches.open('my-first-sw').then(function (cache) {
                    cache.put(event.request, responseClone);
                });

                return httpRes;
            });
        })
    );
});


复制代码

二. service worker在seed中的引入

上面展现了在半年前研究pwa离线缓存时写的代码,而此次,真正要在正式环境上使用时,我决定使用webpack一个插件:workbox-webpack-plugin。workbox是google官方的pwa框架,workbox-webpack-plugin是由其产生的其中一个工具,内置了两个插件:GenerateSWInjectManifest

  • GenerateSW:这个插件会帮你生成一个service worker配置文件,不过这个插件的能力较弱,主要是处理文件缓存和install、activate
  • InjectManifest:这个插件能够自定义更多的配置,好比fecth、push、sync事件

因为此次是为了进行资源缓存,因此只使用了GenerateSW这部分。

//在webpack配置文件里
		var WorkboxPlugin = require('workbox-webpack-plugin');
		
		new WorkboxPlugin.GenerateSW({
            cacheId: 'seed-cache',

            importWorkboxFrom: 'disabled', // 可填`cdn`,`local`,`disabled`,
            importScripts: '/scripts-build/commseed/workboxswMain.js',

            skipWaiting: true, //跳过waiting状态
            clientsClaim: true, //通知让新的sw当即在页面上取得控制权
            cleanupOutdatedCaches: true,//删除过期、老版本的缓存
            
            //最终生成的service worker地址,这个地址和webpack的output地址有关
            swDest: '../workboxServiceWorker.js', 
            include: [
                
            ], 
            //缓存规则,可用正则匹配请求,进行缓存
            //这里将js、css、还有图片资源分开缓存,能够区分缓存时间(虽然这里没作区分。。)
            //因为种子农场此站点较长时间不更新,因此缓存时间能够稍微长一些
            runtimeCaching: [
                {
                    urlPattern: /.*\.js.*/i,
                    handler: 'CacheFirst',
                    options: {
                        cacheName: 'seed-js',
                        expiration: {
                            maxEntries: 20,  //最多缓存20个,超过的按照LRU原则删除
                            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                        },
                    },
                },
                {
                    urlPattern: /.*css.*/,
                    handler: 'CacheFirst',
                    options: {
                        cacheName: 'seed-css',
                        expiration: {
                            maxEntries: 30,  //最多缓存30个,超过的按照LRU原则删除
                            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                        },
                    },
                },
                {
                    urlPattern: /.*(png|svga).*/,
                    handler: 'CacheFirst',
                    options: {
                        cacheName: 'seed-image',
                        expiration: {
                            maxEntries: 30,  //最多缓存30个,超过的按照LRU原则删除
                            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                        },
                    },
                }
            ]
        })
复制代码
  1. importWorkboxForm和importScripts:

importWorkboxFrom:workbox框架文件的地址,可选cdn、local、disabled

  • cdn:引入google的官方cdn,固然在国内会被强。。pass
  • Local:workboxPlugin会在本地生成workbox的代码,能够将这些配置文件一块儿上传部署,这样是每次都要部署一次这个生成的代码。
  • Disabled:上面两种都不选用,将生成出来的workbox代码使用importscript指定js文件从而引入。

我最终选择的是第三种,由于这样能够由本身指定要从哪里引入,好比之后若是这个站点有了cdn,能够将这个workbox.js放到cdn上面。目前是将生成的文件,放到script文件夹下。

  1. workbox的策略
    • Stale-While-Revalidate:尽量快地利用缓存返回响应,缓存无效时则使用网络请求
    • Cache-First:缓存优先
    • Network-First:网络优先
    • Network-Only:只使用网络请求的资源
    • Cache-Only:只使用缓存

通常站点的 CSS,JS 都在 CDN 上,SW 并无办法判断从 CDN 上请求下来的资源是否正确(HTTP 200),若是缓存了失败的结果,就很差了。这种状况下使用stale-while-Revalidate策略,既保证了页面速度,即使失败,用户刷新一下就更新了。

而因为种子项目的js和css资源都在站点下面,因此这里就直接使用了cache-first策略。

在webpack中配置好以后,执行webpack打包,就能看到在指定目录下由workbox-webpack-plugin生成的service worker配置文件了。

接入以后,打开网站,在电脑端的chrome调试工具上能够看到缓存的资源

接入过程的考虑

  • 前文也有介绍,service worker一旦被install,就永远存在;若是有一天想要去除跑在浏览器背后的这个service worker线程,要手动去卸载。因此在接入以前,我得先知道如何卸载service worker,留好后手:
if ('serviceWorker' in navigator) {
       navigator.serviceWorker.getRegistrations()
           .then(function(registrations) {
				for(let registration of registrations) {
                     //安装在网页的service worker不止一个,找到咱们的那个并删除
                    if(registration && registration.scope === 'https://seed.futunn.com/'){
                        registration.unregister();
                    }
                }
            });
    }
复制代码
  • 使用service worker缓存了资源,那下次从新发布了,还会不会拉取新的资源呢?这里也是能够的,只要资源地址不同、修改了hash值,那么资源是会从新去拉取并进行缓存的,以下图,能够看到对同一个js的不一样版本,都进行了缓存。

  • 还有个就是对于考虑开发过程的问题,若是之后上线了,sw这个东西安装下去了,每次打开都直接读取缓存的资源,那之后在本地调试时怎办?试了下,chrome的“disabled cache”也没有用,总不能在本地开发时也给资源打上hash值吧(目前这个项目是在发布到正式环境时才会打上hash值)。。而后针对这个问题想了蛮久的,最后发现chrome早有这个设置,在devtool中能够设置跳过service worker,bypass for network

  • 比起浏览器的默认缓存功能,service woker的缓存功能赋予咱们更强大地、更完善地控制缓存的能力。

  • 这个东西其中一个不足在于,尚未不少浏览器支持service worker这个东西,苹果系统是从11.3才开始支持,因此直到如今,富途nn的app的webview、微信ios版的webview都还不支持service worker这个特性;在安卓上的支持更为普遍一些,因此此次在种子的优化上,安卓客户能够更好地感觉到这个成效。

后记

种子农场加入service worker上线快两周了,到如今尚未啥问题,彷佛一切都挺顺利的。

从最开始研习会上的接触以后,就一直在想着要准备把它用起来,可一直都有种对于它的不肯定性的畏惧。随着对它的愈来愈熟悉,此次终于把它搞起来了, 挂念许久的东西可算是有个交代了。。

相关文章
相关标签/搜索