Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。(引用自:链接)

简单的来说,ServiceWorker(后文简称sw)运行在页面后台,使用了sw的页面可以利用sw来拦截页面发出的请求,同时配合CacheAPI可以将请求缓存到客户本地

因此我们可以:

  • 将页面的文件存储在客户端,下次打开页面可以不向服务器发出资源请求,极大的加快页面加载速度
  • 离线打开页面的同时可以在sw发出请求,更新本地的资源文件
  • 实现离线访问页面

但是也存在着一些问题

  • 由于打开页面不再向服务器发出页面请求,因此当服务器上的页面有新版本的时候客户端无法及时升级
  • sw存在一定的兼容性问题

IE全面扑街,pc上兼容性不太好,移动端安卓支持良好,ios要12+。但考虑到sw并不会影响的页面的正常运行,所以项目上还是能投入生产的。

基本例子

index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Sw demo</title>
  8. </head>
  9. <body>
  10. </body>
  11. <script src="index.js"></script>
  12. <script>
  13. if(navigator.serviceWorker){
  14. navigator.serviceWorker.register('sw.js').then(function(reg){
  15. if(reg.installing){
  16. console.log('client-installing');
  17. }else if(reg.active){
  18. console.log('client-active')
  19. }
  20. })
  21. }
  22. </script>
  23. </html>

index.js

  1. document.body.innerHTML="hello world!";

sw.js

  1. var cacheStorageKey = 'one';
  2. var cacheList = [
  3. "index.html",
  4. "index.js"
  5. ]
  6. self.addEventListener('install', function (e) {
  7. console.log('sw-install');
  8. self.skipWaiting();
  9. })
  10. self.addEventListener('activate', function (e) {
  11. console.log('sw-activate');
  12. caches.open(cacheStorageKey).then(function (cache) {
  13. cache.addAll(cacheList)
  14. })
  15. var cacheDeletePromises = caches.keys().then(cacheNames => {
  16. return Promise.all(cacheNames.map(name => {
  17. if (name !== cacheStorageKey) {
  18. // if delete cache,we should post a message to client which can trigger a callback function
  19. console.log('caches.delete', caches.delete);
  20. var deletePromise = caches.delete(name);
  21. send_message_to_all_clients({ onUpdate: true })
  22. return deletePromise;
  23. } else {
  24. return Promise.resolve();
  25. }
  26. }));
  27. });
  28. e.waitUntil(
  29. Promise.all([cacheDeletePromises]
  30. ).then(() => {
  31. return self.clients.claim()
  32. })
  33. )
  34. })
  35. self.addEventListener('fetch', function (e) {
  36. e.respondWith(
  37. caches.match(e.request).then(function (response) {
  38. if (response != null) {
  39. console.log(`fetch:${e.request.url} from cache`);
  40. return response
  41. } else {
  42. console.log(`fetch:${e.request.url} from http`);
  43. return fetch(e.request.url)
  44. }
  45. })
  46. )
  47. })

说明

这样就完成了一个简单的sw页面了,现在通过服务器访问页面html、js资源将直接从客户端本地读取,实现页面的快速打开和离线访问

  • 客户端和sw都有不同的事件回调,这些事件将在不同的sw生命周期中被触发,后续会有详细介绍
  • 首次打开页面的时候sw先进行install回调,执行self.skipWaiting()后将接着执行activate,activate内对缓存列表的文件进行缓存
  • cacheStorageKey是缓存的识别码,当cacheStorageKey的值变化,sw的activate会将旧的缓存给删除掉,重新调用cache.addAll设置缓存
  • sw的fetch事件会拦截页面发出的请求,将根据缓存情况作出不同的处理

生命周期与事件

sw应用的生命周期我简单抽象为三种

  • 安装:页面首次打开,加载对应的sw文件
  • 活动:加载过sw文件后,打开页面
  • 更新:当服务器的sw文件与客户端的不一致的时候打开页面

客户端

名称 installing active
安装 触发 不触发
活动 不触发 触发
更新 不触发 触发

sw

名称 install activate fetch
安装 触发 触发 不触发
活动 不触发 不触发 触发
更新 触发 触发 不触发

总结一下:

  • 客户端除了在首次打开的时候触发installing,其他都是触发的active
  • sw端活动状态只执行fetch,安装和更新状态只执行install和activate

页面与sw通信

通信方面我之前有翻译过文章,链接地址,大家感兴趣可以看看。这里我直接展示把封装好的通信接口接口

有了通信接口,我们就可以优化很多事情,比方说在 cacheStorageKey发生变化的时候通知页面给予客户一定的响应

客户端

  1. function send_message_to_sw(msg){
  2. return new Promise(function(resolve, reject){
  3. // Create a Message Channel
  4. var msg_chan = new MessageChannel();
  5. // Handler for recieving message reply from service worker
  6. msg_chan.port1.onmessage = function(event){
  7. if(event.data.error){
  8. reject(event.data.error);
  9. }else{
  10. resolve(event.data);
  11. }
  12. };
  13. // Send message to service worker along with port for reply
  14. navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2]);
  15. });
  16. }

sw

  1. function send_message_to_client(client, msg){
  2. return new Promise(function(resolve, reject){
  3. var msg_chan = new MessageChannel();
  4. msg_chan.port1.onmessage = function(event){
  5. if(event.data.error){
  6. reject(event.data.error);
  7. }else{
  8. resolve(event.data);
  9. }
  10. };
  11. client.postMessage(msg, [msg_chan.port2]);
  12. });
  13. }
  14. function send_message_to_all_clients(msg){
  15. clients.matchAll().then(clients => {
  16. clients.forEach(client => {
  17. send_message_to_client(client, msg).then(m => console.log("SW Received Message: "+m));
  18. })
  19. })
  20. }

动态缓存资源文件

上述的做法需要事先写好cacheList,有一定的维护量,现在介绍一种不需要维护cacheList的做法:

  1. self.addEventListener('fetch', function (e) {
  2. e.respondWith(
  3. caches.match(e.request).then(res => {
  4. return res ||
  5. fetch(e.request)
  6. .then(res => {
  7. const resClone = res.clone();
  8. caches.open(cacheStorageKey).then(cache => {
  9. cache.put(e.request, resClone);
  10. })
  11. return res;
  12. })
  13. })
  14. )
  15. });

这样做的话缓存资源的操作将从activate转移到fetch事件内,fetch事件先判断有没有缓存,没有缓存的话将发出对应的请求并进行缓存

这样的做法的缺点是无法在首次加载页面的时候就完成静态化,因为sw的安装声明周期是不会触发sw的fetch事件的。

页面url带参数

针对一些页面渲染结果与url参数有关的情况,上述的架构无法完成对应的本地化需求。之前的做法是在cacheList加入了入口页面的地址,无法适应带动态参数url的情况。

在fetch内动态缓存请求

具体做法在动态缓存资源文件章节有描述,不再重复描述。

使用通信接口通知sw缓存入口页面

客户端

  1. navigator.serviceWorker.register(file).then(function (reg) {
  2. if (reg.installing) {
  3. //send_message_to_sw({pageUrl:location.href})
  4. }
  5. if (reg.active) {
  6. send_message_to_sw({pageUrl:location.href})
  7. }
  8. return reg;
  9. })

sw

  1. self.addEventListener('message',function(e){
  2. var data=e.data;
  3. if(data.pageUrl){
  4. addPage(data.pageUrl)
  5. }
  6. })
  7. function addPage(page){
  8. caches.open(cacheStorageKey).then(function(cache){
  9. cache.add(page);
  10. })
  11. }

在客户端的active发消息给sw,sw就能够获取到对应的页面url进行缓存。

:客户端的installing事件没法使用消息接口,大家可以在sw的activate事件向客户端发出一个消息请求获取当前页面url

常见问题

sw文件至少要与入口页面文件在同一目录下,如:

  • /sw.js 可以管理 /index.html
  • /js/sw.js 不能管理 /index.html

笔者在这里踩了很久的坑...

webpack-sw-plugin

介绍一个笔者写的webpack的sw插件,在弄sw页面的时候很方便,github地址

安装

  1. npm install --save-dev webpack-sw-plugin

webpack配置

  1. const WebpackSWPlugin = require('webpack-sw-plugin');
  2. module.exports = {
  3. // entry
  4. // output
  5. plugins:[
  6. new WebpackSWPlugin()
  7. ]
  8. }

客户端配置

  1. import worker from 'webpack-sw-plugin/lib/worker';
  2. worker.register({
  3. onUpdate:()=>{
  4. console.log('client has a new version. page will refresh in 5s....');
  5. setTimeout(function(){
  6. window.location.reload();
  7. },5000)
  8. }
  9. });

效果

  • 自动生成页面与sw交互的体系,无需提供额外的sw文件
  • 自动适配url带参数的情况
  • 当webpack输出文件变化的时候,客户端的onUpdate将会被触发,上述例子中当输出文件变化时,客户端将会在5秒后进行刷新,刷新后将会使用全新的文件

利用ServiceWorker实现页面的快速加载和离线访问的更多相关文章

  1. 利用Jquery的load函数实现页面的动态加载

    利用Jquery的load函数实现页面的动态加载  js的强大功能相信大家都知晓,今天通过jquery的库函数load可以更加方便的实现页面的动态刷新,经过几天的研究与探索,终于有所成效!吾心甚蔚! ...

  2. HTML5的页面资源预加载技术(Link prefetch)加速页面加载

    不管是浏览器的开发者还是普通web应用的开发者,他们都在做一个共同的努力:让Web浏览有更快的速度感觉.有很多已知的技术都可以让你的网站速度变得更快:使用CSS sprites,使用图片优化工具,使用 ...

  3. 使用HTML5的页面资源预加载(Link prefetch)功能加速你的页面加载速度

    不管是浏览器的开发者还是普通web应用的开发者,他们都在做一个共同的努力:让Web浏览有更快的速度感觉.有很多已知的技术都可以让你的网站速度变得更快:使用CSS sprites,使用图片优化工具,使用 ...

  4. jQuery Pjax – 页面无刷新加载,优化用户体验

    pjax 是 HTML5 pushState 以及 Ajax 两项技术的简称,综合这两个技术可以实现在不刷新页面的情况下载入 HTML 到当前网页,带给你超快速的浏览器体验,而且有固定链接.标题以及后 ...

  5. Winform开发框架之客户关系管理系统(CRM)的开发总结系列4-Tab控件页面的动态加载

    在前面介绍的几篇关于CRM系统的开发随笔中,里面都整合了多个页面的功能,包括多文档界面,以及客户相关信息的页面展示,这个模块就是利用DevExpress控件的XtraTabPage控件的动态加载实现的 ...

  6. 页面资源预加载(Link prefetch)功能加速你的页面加载速度

    有了浏览器缓存,为何还需要预加载? 用户可能是第一次访问网站,此时还无缓存 用户可能清空了缓存 缓存可能已经过期,资源将重新加载 用户访问的缓存文件可能不是最新的,需要重新加载 页面资源预加载/预读取 ...

  7. 在NVIDIA A100 GPU中使用DALI和新的硬件JPEG解码器快速加载数据

    在NVIDIA A100 GPU中使用DALI和新的硬件JPEG解码器快速加载数据 如今,最流行的拍照设备智能手机可以捕获高达4K UHD的图像(3840×2160图像),原始数据超过25 MB.即使 ...

  8. MVC中 _ViewStart _Layout Index三个页面中的加载顺序

    MVC学习中忽然想到一个问题.. 在访问一个Index.cshtml页面时, MVC的加载顺序是怎么样的呢? 首先说下我的结论 . _ViewStart.cshtml . Index.cshtml . ...

  9. 当滚动条滚动到页面底部自动加载增加内容的js代码

    这篇文章主要介绍了如何使用javscript实现滚动条滚动到页面底部自动加载增加页面内容,需要的朋友可以参考下..1,注册页面滚动事件,window.onscroll = function(){ }; ...

随机推荐

  1. BZOJ2636: crisis(可持久化线段树)

    传送门: 解题思路: 题目描述是一大坑点,cancel后面是直接加ask或者redo的. 那么就可以愉快地可持久化了. 注意需要支持区间修改,那么就只需要在再次更新这个点的时候将标记储存在新的儿子中. ...

  2. jQuery和JavaScript的点击事件区别

    // $("#indexPage").click(); // 触发了a标签的点击事件,但是没有触发页面跳转 document.getElementById("indexP ...

  3. Android学习总结(1)——好的 Android 开发习惯

    Android编码规范 java代码中不出现中文,最多注释中可以出现中文: 局部变量命名.静态成员变量命名:只能包含字母,单词首字母出第一个都为大写,其他字母都为小写: 常量命名:只能包含字母和 ,字 ...

  4. HDU 3911 Black And White

    Black And White Time Limit: 3000ms Memory Limit: 32768KB This problem will be judged on HDU. Origina ...

  5. eclipse部署maven web项目到tomcat服务器时,没有将lib、web.xml复制过去的解决办法

    我这几天在写项目的时候发现自己以前的项目能够访问,隔一段时间写的这个项目却不能够访问,没有发现代码的逻辑错,但是就是访问不了jsp页面,项目一发布就是出现404错误,后来发现原来是发布到tomcat上 ...

  6. Vue项目自动转换 px 为 rem,高保真还原设计图

    技术栈 vue-cli:使用脚手架工具创建项目. postcss-pxtorem:转换px为rem的插件. 自动设置根节点html的font-size 因为rem单位是相对于根节点的字体大小的,所以通 ...

  7. POJ 3172 (认真读题的题)

    题目: 思路: 题目很有意思 首先 题里说:N<=1000 题里又说 诶呦 woc? 这不自相矛盾嘛 最坏情况也就是个 斐波那契数列 几十个数 暴搜+剪枝不就好了嘛 剪枝:从大往小搜,如果前缀和 ...

  8. AC自动机 hdu2222

    #include <iostream> using namespace std; struct Node{ Node *next[]; Node* fail; int count; Nod ...

  9. IBM Tivoli Netview在企业网络管理中的实践(附视频)

    今天我为大家介绍的一款高端网管软件名叫IBM Tivoli NetView,他主要关注是IBM整理解决方案的用户,分为Unix平台和Windwos平台两种,这里视频演示的是基于Windows 2003 ...

  10. tree ---树状显示

    tree命令以树状图列出目录的内容. 语法 tree(选项)(参数) 选项 -a:显示所有文件和目录: -A:使用ASNI绘图字符显示树状图而非以ASCII字符组合: -C:在文件和目录清单加上色彩, ...