1、serviceWorker介绍
service worker是一段运行在浏览器后台的JavaScript脚本,在页面中注册并安装成功后,它可以拦截和处理网络请求,实现缓存资源并可在离线时响应用户的请求。针对弱网及无网络场景,可使用离线资源。

特点:
网站必须使用 HTTPS。本地可使用 localhost
单独的运行环境和执行线程:在一个新线程中,不会影响js主线程的运行,所以不会造成阻塞。
不能访问dom:但service worker可以通过postMessage与页面之间通信

生命周期:

installing - 安装事件被触发,但还没完成。缓存静态资源
installed" - 安装完成
activating - 激活事件被触发,但还没完成
activated - 激活成功 更新资源
redundant - 废弃,可能是因为安装失败,或者是被一个新版本覆盖

事件:
install: 安装阶段触发,缓存静态资源
activate:sw激活时触发,做缓存更新,删除旧资源
fetch:拦截与处理网络请求,缓存匹配及缓存策略制定

使用实例:

//缓存资源的两种方式:
//(1)在install事件中,进行静态资源的缓存,自定义要缓存的资源
//(2)在fetch事件中,通过service worker的fetch截获网络请求,请求成功后将资源缓存起来。即在网络请求完成后,缓存也完成,不需额外请求。 // 要注意一点,/sw.js 文件可能会因为浏览器缓存问题,当文件有了变化时,浏览器里还是旧的文件。这会导致更新得不到响应。如遇到该问题,可尝试这么做:在 Web Server 上添加对该文件的过滤规则,不缓存或设置较短的有效期。 //定义缓存的key及缓存列表
var cacheStorageKey = 'pwa-sw20180409-2';
var cacheWhitelist = ['pwa-sw20180409-2']; //清理缓存,保留whitelist中的缓存,其他的删除
var cacheList = [
'./main.html',
'./js/main.js',
'./css/main.css',
]; self.addEventListener("install", function(e) {
console.log("sw install 事件触发", e);
e.waitUntil( //安装成功后 ServiceWorker 状态会从 installing 变为 installed 当 Promise reject 的时候,代表着安装失败,浏览器将这个 SW 废弃掉,不会控制任何 clients。
caches.open(cacheStorageKey)
.then(cache => cache.addAll(cacheList)) //cacheList中的文件全部安装完成 加载这些资源并将响应添加至缓存。cache.addAll() 是原子操作,如果某个文件缓存失败了,那么整个缓存就会失败!
.then(() => self.skipWaiting()) //self.skipWaiting() 页面更新的过程当中, 新的 Service Worker 脚本能立即激活和生效 跳过等待状态
)
}); // 如果希望在有了新版本时,所有的页面都得到及时更新,则在安装阶段跳过等待,直接进入 active
/*self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});*/ self.addEventListener('activate', event => event.waitUntil(
Promise.all([
// 更新客户端
self.clients.claim(), //让 clients 受service worker控制
// 清理旧版本
caches.keys().then(cacheList => Promise.all(
cacheList.map(cacheName => {
console.log("缓存键名", cacheName, cacheWhitelist);
/*if (cacheWhitelist.indexOf(cacheName) == -1) {
console.log("删除老的缓存", cacheName);
caches.delete(cacheName);
}*/
})
)).then(function(){ console.log("activate事件回调");
console.log('self.clients',self.clients);
// return self.clients.matchAll()
return self.clients.matchAll()
.then(function(clients) {
console.log("客户端clients",clients);
if (clients && clients.length) {
clients.forEach(function (client) {
console.log("客户端client",client);
// 这样发出去,sw-register.js 就能收的到啦
client.postMessage('sw.update');
})
}
})
}).catch(function(err) {
console.log("err", err);
})
])
));
//更新缓存,清理旧缓存
/*self.addEventListener("activate",function(e){
console.log("清理旧缓存");
e.waitUntil(
caches.keys().then(function(keyList){
return Promise.all(keyList.map(function(key){
console.log("缓存列表中的key",key);
if(cacheWhitelist.indexOf(key)==-1){
console.log("删除key",key);
return caches.delete(key);
}
}));
})
)
});*/ //监听资源请求 判断是否从缓存中获取
self.addEventListener("fetch", function(e) { console.log("fetch拦截事件", e.request);
var startTime = + new Date();
e.respondWith(
caches.match(e.request)
.then(function(response) {
console.log("caches", caches);
e.request.cache = false;
e.request.headers = {"Cache-Control":"no-cache"};
// console.log("缓存结果", response);
// console.log('更新后e.request',e.request);
if (response != null) { // // 如果 Service Worker 有自己的返回,就直接返回,减少一次 http 请求
console.log("匹配成功 使用缓存", e.request);
var endTime = + new Date();
console.log("使用缓存耗时",endTime-startTime);
return response;
}
console.log("response为null 走网络请求", e.request); // 如果 service worker 没有返回,那就得直接请求真实远程服务
//var request = e.request.clone(); // 把原始请求拷过来
return fetch(e.request).then(function(httpRes) { // fetch(fetchRequest, { credentials: 'include' } ); //fetch请求默认不带cookie,可通过此参数将cookie带过去
var endTime = + new Date();
console.log("走网络请求耗时",endTime-startTime);
// 请求失败了,直接返回失败的结果就好了。。
if (!httpRes || httpRes.status !== 200) {
return httpRes;
} // 请求成功的话,将请求缓存起来。 如果是index.html页面的话 需要考虑更新问题
// var responseClone = httpRes.clone();
// caches.open(cacheStorageKey).then(function (cache) {
// cache.put(e.request, responseClone);
// }); return httpRes; }).catch(function(e) {
console.log("网络请求失败", e);
// return fetch(e.request);
})
}).catch(function(e) {
console.log("匹配失败", e.request);
return fetch(e.request);
})
)
}); //监听离线状态
/*self.addEventListener('offline', function() {
Notification.requestPermission().then(grant => {
if (grant !== 'granted') {
return;
} const notification = new Notification("Hi,网络不给力哟", {
body: '您的网络貌似离线了,不过在志文工作室里访问过的页面还可以继续打开~',
icon: '//lzw.me/images/avatar/lzwme-80x80.png'
}); notification.onclick = function() {
notification.close();
};
});
});
*/ //错误监控
self.onerror = function(errorMessage, scriptURI, lineNumber, columnNumber, error) {
if (error) {
console.log(error);
// reportError(error);
} else {
console.log(errorMessage, scriptURI, lineNumber, columnNumber, error)
// reportError({
// message: errorMessage,
// script: scriptURI,
// line: lineNumber,
// column: columnNumber
// });
}
} //当 Promise 类型的回调发生reject 却没有 catch 处理,会触发 unhandledrejection 事件。 self.addEventListener('unhandledrejection', function(event) {
console.log("unhandledrejection", event);
// reportError({
// message: event.reason
// })
});

2、workbox
workbox 是 GoogleChrome 团队推出的一套 Web App 静态资源本地存储的解决方案,该解决方案包含一些 Js 库和构建工具,在 Chrome Submit 2017 上首次隆重面世。而在 workbox 背后则是 Service Worker 和 Cache API 等技术和标准在驱动。
配置化:几乎不用考虑太多的具体实现,只用做一些配置。
简单却不失灵活,可以完全自定义相关需求(支持 Service Worker 相关的特性如 Web Push, Background sync 等)。
支持多种缓存策略:针对各种应用场景的多种缓存策略以及自定义缓存策略

(1)基于workbox-build工具生成缓存列表

需要先安装workbox-build,npm install workbox-build --save-dev

根据配置参数提取预缓存的资源文件,注入到预缓存内容列表中,生成一份service worker 文件。
workboxBuild.injectManifest(params)

swSrc : 生成 service-worker.js 所需的模板文件所在位置
swDest:生成的 service-worker.js 的存放位置。
globDirectory:指定需要预缓存的静态文件的目录。
globPatterns:相对于 globDirectory 指定的目录,指出哪些文件需要被预缓存。
globIgnores:相对于 globDirectory 指定的目录,指出哪些文件不需要被预缓存。service-worker.js 本身会被自动排除

const workboxBuild = require('workbox-build');
const path = require("path"); const fs = require("fs"); workboxBuild.injectManifest({
swSrc: path.join(__dirname, './', 'sw.dev.js'),
swDest: path.join(__dirname, './', 'sw.js'),
globDirectory: '.\\',
globPatterns: ['**\/*.{html,js,css,png,jpeg}'
],
globIgnores: ['clear.html',"sw.js", "sw-register.js", "sw.dev.js", "sw-register.dev.js", "build/*.js", "workbox-cli-config.js", "webpack.config.js", "build-sw.js", "Gruntfile.js", "package.json", "batlog.log", 'node_modules/**', "js/lib/**"],
templatedUrls: {
// '/shell': ['dev/templates/app-shell.html', 'dev/**\/*.css'],
},
// 要替换的预留代码区正则
// injectionPointRegexp: /(\.precacheAndRoute\()\s*\[\s*\]\s*(\))/,
// workbox.precaching.precacheAndRoute([],{ ignoreUrlParametersMatching:[/v/,/\d+/]});
injectionPointRegexp: /(\.precacheAndRoute\()\s*\[\s*\]\s*(,\{ignoreUrlParametersMatching\:\s*\[\S*\]\s*\}\))/, }).then(function() {
console.log("注入预缓存文件列表,sw.js生成");
})
.catch(err => {
console.error(`Unable to inject the precache manifest into sw.js`);
console.error(err);
throw err;
});

(2)缓存更新

增量更新:当Service worker文件发生变化时,会重新触发注册,新的sw安装后,会缓存新的sw中的缓存资源列表中发生变化的文件。而每个资源的更新主要依靠每个文件的revision值,根据revision值来判断文件的变化,决定是否更新。

3、需要注意的问题
(1)sw缓存问题:
sw文件本身不能被缓存,同时,注册Service worker的载体文件也不能被缓存。

SW注册思路:
借助一个js文件(sw-register.js)来注册sw,并且保证这个文件是每次都请求最新的,同时在注册sw时增加版本号管理,每次发布时更新版本号

 function insertSWregister(){
console.log("插入sw-register.js");
var script = document.createElement('script');
var firstScript = document.getElementsByTagName('script')[0];
script.type = 'text/javascript';
script.async = true;
script.src = './sw-register.js?v=' + Date.now();
firstScript.parentNode.insertBefore(script, firstScript);
}

sw-register.js

// var VERSION = '2.04';//window._pasSwVersion;
if ('serviceWorker' in navigator) {
console.log("支持service worker!!!!!");
// 为了保证首屏渲染性能,可以在页面 load 完之后注册 Service Worker
navigator.serviceWorker.getRegistrations().then(function(regs) {
console.log("注册的sw list", regs);
// delSW(regs,function(){ //删除成功后回调
// window.location.reload();
// });
regSW();
}).catch(function(e) {
console.log("getRegistrations出错", e);
});
} else {
console.log("不支持service worker!");
}
//监听sw更新
window.addEventListener('swupdate', e => {
// service-worker.js 如果更新成功会 postMessage 给页面,内容为 'sw.update'
console.log("sw.update更新完成");
if(confirm("内容发生更新,是否要立即更新?")){
window.location.reload();
}
}); function regSW() {
navigator.serviceWorker.register('./sw.js?v=1523357182344').then(function(reg) {
console.log("注册成功,当前作用域为:", reg.scope); reg.onupdatefound = function() { console.log("状态变化",reg);
var installingWorker = reg.installing; //安装中的 SW
console.log("状态变化installingWorker.state",installingWorker.state);
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
//state;
// "installing" - 安装事件被触发,但还没完成
// "installed" - 安装完成
// "activating" - 激活事件被触发,但还没完成
// "activated" - 激活成功
// "redundant" - 废弃,可能是因为安装失败,或者是被一个新版本覆盖
case 'installed':
console.log("安装完成installed");
console.log('navigator.serviceWorker.controller:',navigator.serviceWorker.controller);
if (navigator.serviceWorker.controller) { //判断当前client是否被serviceworker控制
var event = document.createEvent('Event');
event.initEvent('swupdate', true, true);
window.dispatchEvent(event);
}
break;
}
};
};
if (reg.installing) {
console.log('Service worker installing。。。');
}
if (reg.waiting) {
console.log('Service worker installed。。。');
}
if (reg.active) {
console.log('Service worker active。。。');
}
}).catch(function(e) {
console.log("注册service worker失败", e);
})
} //删除缓存
function delSW(regs,callback) {
for (var reg of regs) {
console.log("遍历注册的sw", reg);
var url= window.location.href;
var scope = url.substr(0,url.indexOf("/index.html")+1);
if (reg.scope == scope) {
reg.unregister().then(function(boolean) { //卸载sw 后,再次请求会直接走网络请求,但是缓存文件并未删除
// if boolean = true, unregister is successful
console.log("注销成功 unregister", reg);
//清除所有缓存文件
caches.delete("workbox-precache-"+scope).then(function(data) {
console.log("删除缓存workbox-precache-"+scope, data);
if(typeof callback=="function"){
callback();
}
});
});
}
} }

(2)缓存匹配为题

我们的文件都带了时间戳,并且入口文件在native中会被自动加上版本号v,而缓存匹配是精确匹配

https://XX/index.html?20180410
https://XX/index.html?v=6.1

方案:workbox中支持忽略参数配置项,可以忽略url中的部分参数
ignoreUrlParametersMatching:[/v/,/\d+/]  忽略v参数和随机数参数

   workbox.precaching.precacheAndRoute([],{ignoreUrlParametersMatching:[/v/,/\d+/]});

(2)浏览器缓存问题:
sw在更新时,如果文件的revision发生变化,则sw会做一次请求,重新请求该文件,但是如果浏览器缓存未失效,则返回的文件内容还是旧的,无法更新。

请求的响应顺序: Service Worker > http cache > 网络请求

针对在更新文件时被浏览器缓存拦截导致的无法更新,workbox中采用设置Request对象上的cache配置项,跳过浏览器缓存。对于不支持cache配置项的使用追加参数的方式避开浏览器缓存。

 _cacheBustRequest(request) {
// let url = request.url+"?"+(+new Date());
let url = request.url;
const requestOptions = {};
if ('cache' in Request.prototype) {
// Make use of the Request cache mode where we can.
// Reload skips the HTTP cache for outgoing requests and updates
// the cache with the returned response.
requestOptions.cache = 'reload'; requestOptions.headers = myHeaders;
} else {
const parsedURL = new URL(url, location); // This is done so the minifier can mangle 'global.encodeURIComponent'
const _encodeURIComponent = encodeURIComponent; parsedURL.search += (parsedURL.search ? '&' : '') + _encodeURIComponent(`_workbox-cache-bust`) + '=' + _encodeURIComponent(this._revision);
url = parsedURL.toString();
} return new Request(url, requestOptions);
}
}

最终方案:(1)直接在url后面追加时间戳,保证在sw更新文件时,绕过浏览器缓存,从服务器获取最新文件

(2)强制请求服务器,跳过http缓存

var myHeaders = new Headers();
myHeaders.append('Cache-Control', 'max-age=0');
requestOptions.headers = myHeaders;
new Request(url, requestOptions);

4、pwa前后对比

(1)离线可用性:提供离线访问能力

(2)性能:文件加载速度,service worker缓存和浏览器缓存读取差异

使用serviceWorker后,多一个向sw发送请求的耗时

             

PWA之serviceWorker应用的更多相关文章

  1. 重识 PWA 进阶到 workbox3

    看到PWA,似曾相识,但又感觉很模糊,于是乎,又重新翻阅文档,学习了一遍,顺便把相关知识学习了一下,比如service worker,workbox3. PWA 概念: 全称:Progressive ...

  2. ServiceWorker pwa缓存

    index.js if ( navigator.serviceWorker ) { console.log("cache index") window.addEventListen ...

  3. 轻松把你的项目升级到PWA

    什么是PWA PWA(Progressive Web Apps,渐进式网页应用)是Google在2015年推出的项目,致力于通过web app获得类似native app体验的网站. 优点 1.无需客 ...

  4. PWA学习心得

    PWA学习心得 一.什么是PWA Progressive  Web  App , (渐进式增强 WEB 应用) 简称 PWA ,是提升WebApp的体验的一种新方法,能给用户原生应用的体验. PWA ...

  5. Web离线应用解决方案——ServiceWorker

    什么是ServiceWorker 在介绍ServiceWorker之前,我们先来谈谈PWA.PWA (Progressive Web Apps) 是一种 Web App 新模型,并不是具体指某一种前沿 ...

  6. hexo 博客支持PWA和压缩博文

    目标网站 https://blog.rmiao.top/ PWA yarn add hexo-offline 然后在root config.yml里新增 # offline config passed ...

  7. 前端应该了解的PWA

    一.传统web 应用 当前web应用在移动时代并没有达到其在桌面设备上流行的程度,下面有张图来对比与原生应用之间的差别. 究其原因,无外乎下面不可避免的几点: 移动设备网络限制-不可忽略的加载时间 w ...

  8. 渐进式Web应用(PWA)入门教程(下)

    上篇文章我们对渐进式Web应用(PWA)做了一些基本的介绍. 渐进式Web应用(PWA)入门教程(上) 在这一节中,我们将介绍PWA的原理是什么,它是如何开始工作的. 第一步:使用HTTPS 渐进式W ...

  9. 为 VUE 项目添加 PWA 解决发布后刷新报错问题

    为什么要给 VUE 项目添加 PWA 为什么要添加?因为不管是部署在 IIS,还是 nginx,每次应用部署后,再次访问因为旧的 js 已经不存在,所以页面访问的时候会整个报错,报错的结果就是一个白屏 ...

随机推荐

  1. Linux 常用检测命令

    1.uptime [root@smgsim02 ~]# uptime  15:08:15 up 98 days,  4:19,  2 users,  load average: 0.07, 0.29, ...

  2. 学习Centos 7的笔记

    Step-1 yum install epel-release && yum clean all && yum update –y &&  yum -y ...

  3. Linux-百度云之AccleriderMini使用

    打开命令行,然后输入: sudo apt-get install mono-devel 最后安装完成时,运行: 示例:mono xx.exe 实例:mono accleridaMini.exe 最后成 ...

  4. LeetCode 168 Excel Sheet Column Title(Excel的列向表标题)

    翻译 给定一个正整数,返回它作为出如今Excel表中的正确列向标题. 比如: 1 -> A 2 -> B 3 -> C ... 26 -> Z 27 -> AA 28 - ...

  5. Navicat for MySQL出现1030-Got error 28 from storage engine错误

    Navicat for MySQL出现1030-Got error 28 from storage engine错误  刚刚还能用这会儿就用不了了,估计是磁盘空间不足引起的! 在根目录/下执行命令:d ...

  6. @Html.ValidationMessageFor客户端验证

    <%=Html.LabelFor(model => model.sUser) %><%=Html.TextBoxFor(model => model.sUser) %&g ...

  7. javascript闭包的应用

    我印象中,javascript的闭包属于进阶的范畴,无非是用来在面试中装装逼而已.你看我身边的一个小伙子,有一天我装逼地问他什么是javascript的闭包,他居然连听都没听说过.但他做起前端的东西来 ...

  8. DataTables warning requested unknown parameter

    This is possibly the most cryptic warning message that DataTables will show. It is a short error mes ...

  9. struts2的一些小问题

    1.action和ValueStack的关系2.ValueStack的类set()方法和setValue()方法的区别3.ValueStack的类push()方法的作用4.从ValueStack对象中 ...

  10. 通过Mesos、Docker和Go,使用300行代码创建一个分布式系统

    [摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...