老马首次接触Service Worker还是在使用 jsproxy 开源项目中开始以为是一种新技术普及少应用更少随着使用的过程中,不断的发现其实国内外的一些大型购物,技术性,社交网站包过一些混合APP等等已经再使用 Service Worker 这项技术了本文将一步异步全面详细的讲述 Service Worker 技术的应用实操
什么是Service Worker
服务器与浏览器之间的中间人,如果网站中注册了Service Worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器,我们在Service Worker 中可以做拦截客户端的请求、向客户端发送消息、向服务器发起请求等先关操作,其中最重要且广泛的的作用就是离线资源缓存。
特性
- 基于web worker(JavaScript主线程的独立线程,如果执行消耗大量资源的操作也不会堵塞主线程)
- 在web worker的基础上增加了离线缓存的能力
- 本质上充当Web应用程序(服务器)与浏览器之间的代理服务器
- 创建有效的离线体验(将一些不常更新的内容缓存在浏览器,提高访问体验)
- 由事件驱动的,具有生命周期
- 可以访问cache和indexDB
- 支持消息推送
- 并且可以让开发者自己控制管理缓存的内容以及版本
- 可以通过 postMessage 接口把数据传递给其他 JS 文件
- 更多无限可能
兼容
作为一个新技术,我们最关注的肯定是它在不同浏览器的兼容性以及覆盖率,我们可以通过我能用这个网站查看当前使用的最新w3c技术是否可以再相关浏览器版本是否支持
JS
- web workers
- Service Worker
- Web Sockets
- ES6
CSS
- CSS Grid Layout
- Flexbox
- CSS position:sticky
可以看到大部分最新版本浏览器已经支持Service Worker只有一些老版本目前还没有支持
注意
不能访问DOM
Service Worker运行在worker上下文,因此它不能访问DOM。
不能同步操作
相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage)不能在service worker中使用
- XHR
XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容
- localStorage
在HTML5中新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同
(function () {
if (!window.localStorage) {
console.log('当前浏览器不支持localStorage!')
}
let test = '0123456789';
let add = function (num) {
num += num;
if (num.length == 10240) {
test = num;
return;
}
add(num);
}
add(test);
let sum = test;
let show = setInterval(function () {
sum += test;
try {
window.localStorage.removeItem('test');
window.localStorage.setItem('test', sum);
console.log(sum.length / 1024)
} catch (e) {
console.log("最大容量" + sum.length / 1024)
clearInterval(show);
}
}, 0.1)
})()
浏览器 | 最大LocalStorage |
---|---|
Chrome | 最大5M |
Firefox | 最大5M |
Edge | 最大5M |
IE11 | 最大3120kb,大小会变化 |
非Https不可用
出于安全考量,Service workers只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。在Firefox浏览器的用户隐私模式,Service Worker不可用。
本地开发调试可以直接使用localhost域名进行测试
大量使用Promise
因为Service workers为异步非等待响应模式所有可以在Service workers大量使用Promise,因为通常某些程序逻辑需要等待响应后继续进行处理,并根据响应返回一个成功或者失败的操作而Promise非常适合这种场景。
官方解读
本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了,Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象
示例
常规异步处理方式
function sendAsync(success_call){
$.ajax({
type: "POST",
url: "xxxx",
data: {},
dataType: 'JSON',
success: function (result) {
success_call(result)
}
});
}
// 成功的回调函数
let success_call= function(result) {
//xxxxxxx
}
sendAsync(success_call);
Promise异步处理方式
let promise = new Promise(function(resolve, reject){
$.ajax({
type: "POST",
url: "xxxx",
data: {},
dataType: 'JSON',
success: function (result) {
resolve(result)
}
});
});
// 成功的回调函数
let success_call= function(result) {
//xxxxxxx
}
promise.then(success_call);
独立的生命周期
Service Worker的生命周期与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)注册Service Worke后,浏览器会默默地在背后安装Service Worke
Safari
最早时候Safari 对于Service workers的全线不支持,这是因为通过Service workers可以在浏览器上实现一种类似小程序的功能(PWA)这种方式可以绕过苹果的app store导致苹果不能再和开发者37开分成,所以苹果不喜欢这项技术不过最终还是在18年开始支持了
使用场景
- 后台数据同步
- 响应来自其它源的资源请求
- 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
- 在客户端进行CoffeeScript,LESS,CJS/AMD等模块编译和依赖管理
- 后台服务钩子
- 自定义模板用于特定URL模式
- 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片
- 后台同步:启动一个service worker即使没有用户访问特定站点,也可以更新缓存
- 响应推送:启动一个service worker向用户发送一条信息通知新的内容可用
- 对时间或日期作出响应
- 进入地理围栏
是否值得学习使用
说了那么多那么这项技术是否成熟和值得尝试使用呢,首先肯定要考虑下他的成熟度和使用率以及是否有大厂在使用,列几个使用Service Worker的网站
等等...也可以通过 edge://serviceworker-internals/?devtools 查看都有哪些网站使用了Service Worker
调试
除了通过上面那个方法查看网站是否使用了Service Worker那么还有那种方式知道网站使用了Service Worker 并且怎么去调试使用呢?
网站是否启用Service Worker,可以通过开发者工具中的应用程序(Application)来查看
因为主要用于中间缓存所以可以到大部分都是css,js一类的东西
生命周期
其生命周期分为首次加载和更新加载
首次访问页面时候Service Worker会立即被下载下来并进行尝试安装,安装成功后就会尝试去激活等操作
更新在默认情况下Service Worker 一定会每24小时被下载一次,如果下载的文件是最新文件,那么它就会被重新注册和安装但不会被激活,当不再有页面使用旧的 Service Worker 的时候,它就会被激活。
用户首次访问service worker控制的网站或页面时,service worker会立刻被下载。
之后,在以下情况将会触发更新:
- 一个前往作用域内页面的导航
- 在 service worker 上的一个事件被触发并且过去 24 小时没有被下载
无论它与现有service worker不同(字节对比),还是第一次在页面或网站遇到service worker,如果下载的文件是新的,安装就会尝试进行。
如果这是首次启用service worker,页面会首先尝试安装,安装成功后它会被激活。
如果现有service worker已启用,新版本会在后台安装,但不会被激活,这个时序称为worker in waiting。直到所有已加载的页面不再使用旧的service worker才会激活新的service worker。只要页面不再依赖旧的service worker,新的service worker会被激活(成为active worker)。
首次加载
- 注册(register)
- 安装(installing)
- 活动(activated)或者异常(error)
- 空闲(idle)
- 拦截(fetch)或终止(terminated)
更新加载
- 更新(update)
- 安装(installing)
- 等待活动(waiting)或者异常(error)
注册
如果要使用Service worker那么首先需要在页面中注册一个sw服务以通知浏览器程序为其该页面分配一块浏览器内存,然后sw服务就会进入安装生命周期阶段
直接注册
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
页面加载完成注册
if('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('sw.js');
});
}
注册返回Promise
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js')
.then(function (reg) {
console.log('success', reg);
})
.catch(function (err) {
console.log('fail', err);
});
}
注册作用域(scope)
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js', { scope: './' });
}
scope表示定义
service worker注册范围的URL ;
service worker可以控制的URL范围。通常是相对URL。默认值是基于当前的location,并以此来解析传入的路径.
在同一个 Origin 下,我们可以注册多个 Service Worker,但是注意,这些 Service Worker 所使用的 scope 必须是唯一且不同的
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js', { scope: './' });
navigator.serviceWorker.register('/sw2/sw.js', { scope: './sw2' });
}
安装
sw注册完成之后,浏览器就开始尝试进行安装操作了可以通过安装事件进行监听(sw内可以使用self也可以使用this,每个sw仅会安装一次,除非发生更新)
sw.js
// this
this.addEventListener('install', function (event) {
console.log('Service Worker install');
});
// self
self.addEventListener('install', function (event) {
console.log('Service Worker install');
});
安装事件触发时的标准行为是使用service worker而准备的,例如使用内建的storage API来创建缓存,并且放置应用离线时所需资源
比如在安装过程中缓存一些静态文件
cons CACHE_NAME="site:static:file:v1"
self.addEventListener('install', function (event) {
let url_list=[
'/',
'/static/xxx.css',
'/static/xxx.js',
'https://www.baidu.com/img/pc_629ee8886a9c20e7f3cb1d2889c3e45d.gif',
'/static/xxx.txt',
];
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
consloe.log("缓存打开成功");
cache.addAll(url_list).then(function(){
consloe.log("所有资源都已获取并缓存");
});
}).catch(function(error) {
console.log('缓存打开失败:', error);
})
);
});
因为缓存文件需要时间所以可以通过waitUntil来防止缓存未完成就关闭serviceWorker一旦所有文件缓存成功那么serviceWorker就安装成功了,只要一个缓存失败就会导致安装失败waitUntil也会通过内部promise来获取安装事件和是否成功
激活
一旦首次安装成功后或者sw进行更新就会触发activated相对首次安装会直接进入激活状态更新触发会显得比较复杂比如
A为老的sw
B为新的sw
B进入安装更新阶段时候A还在工作状态那么B就会进waiting阶段,只有等到A被terminated后,B才能完全替换A的工作
activated阶段可以做很多有意义的事情,比如更新存储在cache中的key和value,可以清理旧缓存和旧的service worker关联的东西
self.addEventListener('activate', function(event) {
console.log('Service Worker activate');
event.waitUntil(
// 遍历 caches 里所有缓存的 keys 值
caches.keys().then(function() {
return caches.keys().then(function (keys) {
var all = keys.map(function (key) {
if (key.indexOf(CACHE_NAME) !== -1){
console.log('[SW]: Delete cache:' + key);
return caches.delete(key);
}
});
return Promise.all(all);
});
})
);
});
空闲
idle空闲状态一般是不可见的,这种一般说明sw的事情都处理完毕了,然后处于闲置状态了,浏览器会周期性的轮询,去释放处于idle的sw占用的资源
终止
terminated终止状态一般触发条件由下面几种方式
- 关闭浏览器一段时间
- 手动清除serviceworker
- 在sw安装时直接跳过waiting阶段
self.addEventListener('install', function(event) {
//跳过等待过程
self.skipWaiting();
});
拦截
fetch拦截阶段是sw最终要和关键阶段,主要用于拦截代理所有指定的请求,然后进行二次相应的处理操作通过这个阶段我们可以实现很多有意思的操作
输出缓存
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
//该fetch请求已经缓存
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
输出JSON
self.addEventListener("fetch", event => {
const data = {
hello: "world"
}
const json = JSON.stringify(data, null, 2)
return event.respondWith(
new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8"
}
})
)
})
输出HTML
const html = `<!DOCTYPE html>
<body>
<h1>Hello World</h1>
<p>This markup was generated by a Cloudflare Worker.</p>
</body>`
async function handleRequest(request) {
return new Response(html, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
})
}
addEventListener("fetch", event => {
return event.respondWith(handleRequest(event.request))
})
重定向URL
const destinationURL = "https://www.baidu.com"
const statusCode = 301
async function handleRequest(request) {
return Response.redirect(destinationURL, statusCode)
}
addEventListener("fetch", async event => {
event.respondWith(handleRequest(event.request))
})
更多例子
https://developers.cloudflare.com/workers/examples
respondWith回应方法里面传递一个promise caches方法 如果我们有一个匹配上response了那么就返回缓存值否则就返回调用fetch结果就是发起网络请求,并返回收到的数据
如果我们想缓存一个新的请求则可以处理fetch请求的response并把这个response加入缓存
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
//该fetch请求已经缓存
if (response) {
return response;
}
return fetch(event.request)
.then(function(response){
// 检查是否响应成功 basic是判断是否为源发起的请求不缓存第三方资源
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// response是一个数据流 因为浏览器会消耗掉流所有先克隆一个流 响应体只会被消耗一次
let responseClone = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseClone);
});
return response;
})
})
);
});
因为Service Worker只有在特定情况下才会下载更新这对我们开发很不方便我们可以通过浏览器开发工具勾选Update on reload 选中之后每次我们刷新都能够使用最新的Service Worker文件
也可以通过手动更新
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js').then(reg => {
reg.update();
reg.installing; // 安装中的 SW,或者是undefined
reg.waiting; // 等待中的 SW,或者是undefined
reg.active; // 激活中的 SW,或者是undefined
reg.addEventListener('updatefound', () => {
// 正在安装的新的 SW
const newWorker = reg.installing;
newWorker.state;
// "installing" - 安装事件被触发,但还没完成
// "installed" - 安装完成
// "activating" - 激活事件被触发,但还没完成
// "activated" - 激活成功
// "redundant" - 废弃,可能是因为安装失败,或者是被一个新版本覆盖
newWorker.addEventListener('statechange', () => {
// newWorker 状态发生变化
});
});
});
}
流程图
通讯
刚才说过页面通讯可以使用postMessage方法可以进行 Service Worker 和页面之间的通讯
页面通讯到SW
发送消息
page.html
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js', { scope: './' })
.then(function (reg) {
console.log('success', reg);
navigator.serviceWorker.controller &&
navigator.serviceWorker.controller.postMessage("hello im page");
});
}
为了保证 Service Worker 能够正常接收到来自页面的信息,可以在它被注册完成之后再发送信息
navigator.serviceWorker.controller 为ServiceWorker实例 我们需要在ServiceWorker 实例上调用postMessage注意当我们使用的scope不是当前Origin navigator.serviceWorker.controller将为Null不可使用
接收消息
sw.js
this.addEventListener('message', function (event) {
console.log(event.data);
})
我们只需要在sw中绑定message事件就可以接收到页面发来的消息了
不同的范围域
如果我们使用非当前Origin域时候我们可以使用reg.active下的postMessage发送消息reg.active就是被注册后激活 Serivce Worker 实例
发送消息
page.html
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw.js', { scope: './sw' })
.then(function (reg) {
console.log('success', reg);
reg.active.postMessage("sw.js");
})
navigator.serviceWorker.register('./sw2.js', { scope: './sw2' })
.then(function (reg) {
console.log('success', reg);
reg.active.postMessage("sw2.js");
})
}
接收消息
sw.js
this.addEventListener('message', function (event) {
console.log(event.data);
});
sw2.js
this.addEventListener('message', function (event) {
console.log(event.data);
});
但是由于Service Worker 的激活是异步的,因此首次注册 Service Worker 的时候可能Service Worker 不会被立刻激活, reg.active 为 Null,系统就会报错。
这个时候我们可以采用Promise内部轮询逻辑进行处理如果 Service Worker 已经被激活那就resolve
page.html
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 50)
})
}).then(sw => {
sw.postMessage("im sw");
})
navigator.serviceWorker.register('sw2.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 50)
})
}).then(sw => {
sw.postMessage("im sw2");
})
}
SW通信到页面
了解完页面到sw通讯我们现在了解下SW通信到页面不同于页面向 Service Worker 发送信息,我们需要在 WindowClient 实例上调用 postMessage 方法才能达到目的,而在页面的JS文件中监听 navigator.serviceWorker 的 message 事件就可以收到信息
定向发送
sw.js
this.addEventListener('message', function (event) {
event.source.postMessage('我是 sw 将发送信息到 page');
});
page.html
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.addEventListener('message', function (e) {
console.log(e.data);
});
}
定向发送比较简单可以直接从页面发送过来的消息中获取 WindowClient 实例 然后使用event.source.postMessage向来源页面发送消息
批量发送
sw.js
this.clients.matchAll().then(client => {
client[0].postMessage('我是 sw 将发送信息到 page');
})
如果不想受到定向发送限制,则可以在 serivce worker 文件中使用 this.clients 来获取其他的页面,并发送消息
注意
如果在注册 Service Worker 的时候,把 scope 设置为非 origin 目录,那么在 Service Worker 文件中,是无法获取到 Origin 路径对应页面的 client
page.html
navigator.serviceWorker.register('sw.js', { scope: './xx/' });
sw.js
this.clients.matchAll().then(client => {
console.log(client); // []
})
跨端通讯
Message Channel 消息通道一种比较好用的通讯方法,使用这种方式能够使得通道两端之间可以相互通信,而不是只能向消息源发送信息
页面消息
page.html
navigator.serviceWorker.register('sw.js')
.then(function (reg) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = e => {
console.log(e.data); // 此消息从SW发送到页面
}
reg.active.postMessage("此消息从页面发送到SW", [messageChannel.por2]);
})
sw.js
this.addEventListener('message', function (event) {
console.log(event.data); // 此消息从页面发送到SW
event.ports[0].postMessage('此消息从SW发送到页面');
});
Service Worker
两个sw服务之间进行通讯
page.html
var messageChannel = new MessageChannel();
navigator.serviceWorker.register('sw.js')
.then(function (reg) {
console.log(reg)
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 50)
})
}).then(sw => {
sw.postMessage("此消息从页面发送到SW", [messageChannel.port1]);
})
navigator.serviceWorker.register('sw2.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 50)
})
}).then(sw => {
sw.postMessage("此消息从页面发送到SW2", [messageChannel.port2]);
})
sw.js
this.addEventListener('message', function (event) {
console.log(event.data); // 此消息从页面发送到SW
event.ports[0].onmessage = e => {
console.log('sw:', e.data); // sw: 此消息从SW2发送到SW1
}
event.ports[0].postMessage('此消息从SW发送到SW2');
});
sw2.js
this.addEventListener('message', function (event) {
console.log(event.data); // 此消息从页面发送到SW2
event.ports[0].onmessage = e => {
console.log('sw2:', e.data); // sw2: 此消息从SW发送到SW2
}
event.ports[0].postMessage('此消息从SW2发送到SW1');
});
首先页面同时给两个不同的sw发送消息并且把信息通道的端口一块发送出去,然后两个不同的sw分别使用设置接收消息的回调函数之后他们之间就可以相互发送接收来自对方的消息了
后台同步
假如用户在页面上操作数据点击了提交,而这个时候呢又刚好网络情况不好或者干脆就断网了这个时候页面只能一直在打转。。。。无尽等待直到有网,然后用户就会直接关掉页面这次请求也就中断了,这种情况就出现了两种问题
- 普通页面会随着页面关闭而终止
- 网络极差或无网络情况下没用一种解决方案能够解决并维持当前请求以待有网时恢复请求
由于Service Worker在用户关闭该网站后仍可以运行因此我们可以充分利用其特性实现后台同步
工作流程
- 在Service Worker中监听sync事件
- 在浏览器中发起后台同步sync
- 就会触发Service Worker的sync事件,在该监听的回调中进行操作,例如向后端发起请求
-
然后可以在Service Worker中对服务端返回的数据进行处理
页面触发同步
page.html
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('sw.js')
navigator.serviceWorker.ready.then(function (registration) {
var tag = "data_sync";
document.getElementById('submit-btn').addEventListener('click', function () {
registration.sync.register(tag).then(function () {
console.log('后台同步已触发', tag);
}).catch(function (err) {
console.log('后台同步触发失败', err);
});
});
});
}
由于后台同步功能需要在Service Worker注册完成后触发,所有我们可以使用navigator.serviceWorker.ready等待注册完成准备好之后使用registration.sync.register注册同步事件
registration.sync 会返回一个SyncManager对象其中包含register方法和getTags方法
register() Create a new sync registration and return a Promise.
getTags() Return a list of developer-defined identifiers for SyncManager registration.
SW监听同步事件
当点击submit-btn触发同步事件后接下来的操作就可以交给SW sync 处理了
sw.js
self.addEventListener('sync', function (e) {
console.log(e);
console.log(`需要进行后台同步,tag: ${e.tag}`);
var init = {
method: 'GET'
};
switch (e.tag){
case "data_sync":
var request = new Request(`xxxxx/sync`, init);
e.waitUntil(
fetch(request).then(function (response) {
response.json().then(console.log.bind(console));
return response;
})
);
break;
}
});
如果需要试试同步页面数据可以配合通讯来使用
Workbox
service worker 框架库由于google推出直接编写原生sw比较繁琐和复杂所以一些工具框架就出现了,而Workbox相对来说较为优秀
简介
在 Workbox 之前,GoogleChrome 团队较早时间推出过 sw-precache 和 sw-toolbox 库,但是在 GoogleChrome 工程师们看来,workbox 才是真正能方便统一的处理离线能力的更完美的方案,所以停止了对 sw-precache 和 sw-toolbox 的维护。
使用
直接在sw.js文件内引入workbox官方JS包就可以了
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.0.0-alpha.3/workbox-sw.js');
if (workbox) {
console.log("已加载");
}else {
console.log("未加载");
}
注意 importScripts 方法只能在 sw 里面使用