前段日子有空,粗略学习了一下 Service Worker ;最近从新复习了下,而且把内容整理后写在这里,但愿对你们有所帮助。css
同时,若是文章中有错误或者描述不当的地方,欢迎你们可以帮我指正,谢谢。react
PS:文章很长,含有大量示例代码。你们能够慢慢看:)json
做为一个比较新的技术,你们能够把 Service Worker 理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中咱们能够作不少事情,好比拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的做用之一就是离线资源缓存。浏览器
首先,做为一个新技术,咱们须要关注的是它在不一样浏览器的兼容性,下面是来自于 caniuse.com 的一张图: 缓存
对于 Service Worker ,了解过 Web Worker 的同窗可能会比较好理解。它和 Web Worker 相比,有相同的点,也有不一样的地方。安全
相同:bash
postMessage
接口把数据传递给其余 JS 文件;不一样的地方在于,Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程,所以它在被注册安装以后,可以被在多个页面中使用,也不会由于页面的关闭而被销毁。所以,Service Worker 很适合被用与多个页面须要使用的复杂数据的计算——购买一次,全家“收益”。服务器
另外有一点须要注意的是,出于对安全问题的考虑,Service Worker 只能被使用在 https 或者本地的 localhost 环境下。并发
下面就让咱们来使用 Service Worker 。异步
若是当前使用的浏览器支持 Service Worker ,则在 window.navigator 下会存在 serviceWorker 对象,咱们可使用这个对象的 register 方法来注册一个 Service Worker。
这里须要注意的一点是,Service Worker 在使用的过程当中存在大量的 Promise ,对于 Promise 不是很了解的同窗能够先去看一下相关文档。 Service Worker 的注册方法返回的也是一个 Promise 。
// index.js
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw.js', { scope: './' })
.then(function (reg) {
console.log('success', reg);
})
.catch(function (err) {
console.log('fail', err);
});
}
复制代码
在这段代码中,咱们先使用 if 判断下浏览器是否支持 Service Worker ,避免因为浏览器不兼容致使的 bug 。
register 方法接受两个参数,第一个是 service worker 文件的路径,请注意:这个文件路径是相对于 Origin ,而不是当前 JS 文件的目录的;第二个参数是 Serivce Worker 的配置项,可选填,其中比较重要的是 scope 属性。按照文档上描述,它是 Service Worker 控制的内容的子目录,这个属性所表示的路径不能在 service worker 文件的路径之上,默认是 Serivce Worker 文件所在的目录;关于这个属性,文档中讲的不是很清楚,我也有不少疑问,会在接下来的内容中提出。但愿有知道的同窗能帮我解惑。
register 方法返回一个 Promise 。若是注册失败,能够经过 catch 来捕获错误信息;若是注册成功,可使用 then 来获取一个 ServiceWorkerRegistration 的实例,有兴趣的同窗能够去翻阅文档。
注册完 Service Worker 以后,浏览器会为咱们自动安装它,所以咱们就能够在 service worker 文件中监听它的 install 事件了。
// sw.js
this.addEventListener('install', function (event) {
console.log('Service Worker install');
});
复制代码
一样的,Service Worker 在安装完成后会被激活,因此咱们也可监听 activate 事件。
// sw.js
this.addEventListener('activate', function (event) {
console.log('Service Worker activate');
});
复制代码
这时,咱们能够在 Chorme 的开发者工具中看到咱们注册的 Service Worker。
在同一个 Origin 下,咱们能够注册多个 Service Worker。可是请注意,这些 Service Worker 所使用的 scope 必须是不相同的。
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw/sw.js', { scope: './sw' })
.then(function (reg) {
console.log('success', reg);
})
navigator.serviceWorker.register('./sw2/sw2.js', { scope: './sw2' })
.then(function (reg) {
console.log('success', reg);
})
}
复制代码
以前说过,使用 postMessage
方法能够进行 Service Worker 和页面之间的通信,下面就让咱们来试一下。
首先是从页面发送信息到 Serivce Worker 。
// index.js
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("this message is from page");
});
}
复制代码
为了保证 Service Worker 可以接收到信息,咱们在它被注册完成以后再发送信息,和普通的 window.postMessage
的使用方法不一样,为了向 Service Worker 发送信息,咱们要在 ServiceWorker 实例上调用 postMessage
方法,这里咱们使用到的是 navigator.serviceWorker.controller
。
// sw.js
this.addEventListener('message', function (event) {
console.log(event.data); // this message is from page
});
复制代码
在 service worker 文件中咱们能够直接在 this
上绑定 message 事件,这样就可以接收到页面发来的信息了。
对于不一样 scope 的多个 Service Worker ,我么也能够给指定的 Service Worker 发送信息。
// index.js
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw.js', { scope: './sw' })
.then(function (reg) {
console.log('success', reg);
reg.active.postMessage("this message is from page, to sw");
})
navigator.serviceWorker.register('./sw2.js', { scope: './sw2' })
.then(function (reg) {
console.log('success', reg);
reg.active.postMessage("this message is from page, to sw 2");
})
}
// sw.js
this.addEventListener('message', function (event) {
console.log(event.data); // this message is from page, to sw
});
// sw2.js
this.addEventListener('message', function (event) {
console.log(event.data); // this message is from page, to sw 2
});
复制代码
请注意,当咱们在注册 Service Worker 时,若是使用的 scope 不是 Origin ,那么navigator.serviceWorker.controller
会为 null。这种状况下,咱们可使用 reg.active
这个对象下的 postMessage
方法,reg.active
就是被注册后激活 Serivce Worker 实例。可是,因为 Service Worker 的激活是异步的,所以第一次注册 Service Worker 的时候,Service Worker 不会被马上激活, reg.active
为 null,系统会报错。我采用的方式是返回一个 Promise ,在 Promise 内部进行轮询,若是 Service Worker 已经被激活,则 resolve 。
// index.js
navigator.serviceWorker.register('./sw/sw.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw");
})
navigator.serviceWorker.register('./sw2/sw2.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw2");
})
复制代码
下一步就是从 Service Worker 发送信息到页面了,不一样于页面向 Service Worker 发送信息,咱们须要在 WindowClient 实例上调用 postMessage
方法才能达到目的。而在页面的JS文件中,监听 navigator.serviceWorker
的 message 事件便可收到信息。
而最简单的方法就是从页面发送过来的消息中获取 WindowClient 实例,使用的是 event.source ,不过这种方法只能向消息的来源页面发送信息。
// sw.js
this.addEventListener('message', function (event) {
event.source.postMessage('this message is from sw.js, to page');
});
// index.js
navigator.serviceWorker.addEventListener('message', function (e) {
console.log(e.data); // this message is from sw.js, to page
});
复制代码
若是不想受到这个限制,则能够在 serivce worker 文件中使用 this.clients
来获取其余的页面,并发送消息。
// sw.js
this.clients.matchAll().then(client => {
client[0].postMessage('this message is from sw.js, to page');
})
复制代码
关于这个方法,我有一些没有解决的疑问的。在个人试验中,注册 Service Worker 时候设置的 scope 的值会对获取到的 client 产生影响。
若是在注册 Service Worker 的时候,把 scope 设置为非 origin 目录,那么在 service worker 文件中,我没法获取到 Origin 路径对应页面的 client。
// index.js
navigator.serviceWorker.register('./sw.js', { scope: './sw/' });
// sw.js
this.clients.matchAll().then(client => {
console.log(client); // []
})
复制代码
我查找了一些资料,可是没有找到关于 scope 和 client 之间的联系的明确说明文档。个人猜想是,Service Worker 是否只可以获取到 scope 路径下的子页面的 client ,可是我使用 react router 试验发现彷佛又不是,但愿有知道的同窗可以帮忙解答,谢谢!
另一种比较好用的通讯方式是使用 Message Channel 。
// index.js
navigator.serviceWorker.register('./sw.js', { scope: './' })
.then(function (reg) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = e => {
console.log(e.data); // this message is from sw.js, to page
}
reg.active.postMessage("this message is from page, to sw", [messageChannel.por2]);
})
// sw.js
this.addEventListener('message', function (event) {
console.log(event.data); // this message is from page, to sw
event.ports[0].postMessage('this message is from sw.js, to page');
});
复制代码
使用这种方式可以使得通道两端之间能够相互通讯,而不是只能向消息源发送信息。举个例子,两个 Service Worker 之间的通讯。
// index.jsconst messageChannel = new MessageChannel();
navigator.serviceWorker.register('./sw/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);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw", [messageChannel.port1]);
})
navigator.serviceWorker.register('./sw2/sw2.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw2", [messageChannel.port2]);
})
// sw.js
this.addEventListener('message', function (event) {
console.log(event.data); // this message is from page, to sw
event.ports[0].onmessage = e => {
console.log('sw:', e.data); // sw: this message is from sw2.js
}
event.ports[0].postMessage('this message is from sw.js');
});
// sw2.js
this.addEventListener('message', function (event) {
console.log(event.data); // this message is from page, to sw2
event.ports[0].onmessage = e => {
console.log('sw2:', e.data); // sw2: this message is from sw.js
}
event.ports[0].postMessage('this message is from sw2.js');
});
复制代码
首先让页面给两个 Service Worker 发送信息,而且把信息通道的端口发送过去;而后在两个 service worker 文件中使用端口分别设置接受信息的回调函数,以后它们就可以互相发送信息并接收到来自通道对面的消息了。
下面要讲的就是重头戏,也是 Service Worker 可以实现的最主要的功能——静态资源缓存。
正常状况下,用户打开网页,浏览器会自动下载网页所须要的 JS 文件、图片等静态资源。咱们能够经过 Chrome 开发工具的 Network 选项来查看。
咱们可使用 Service Worker 配合 CacheStroage 来实现对静态资源的缓存。
// sw.js
this.addEventListener('install', function (event) {
console.log('install');
event.waitUntil(
caches.open('sw_demo').then(function (cache) {
return cache.addAll([
'/style.css',
'/panda.jpg',
'./main.js'
])
}
));
});
复制代码
当 Service Worker 在被安装的时候,咱们可以对制定路径的资源进行缓存。CacheStroage 在浏览器中的接口名是 caches ,咱们使用 caches.open
方法新建或打开一个已存在的缓存;cache.addAll
方法的做用是请求指定连接的资源并把它们存储到以前打开的缓存中。因为资源的下载、缓存是异步行为,因此咱们要使用事件对象提供的 event.waitUntil
方法,它可以保证资源被缓存完成前 Service Worker 不会被安装完成,避免发生错误。
从 Chrome 开发工具中的 Application 的 Cache Strogae 中能够看到咱们缓存的资源。
this.addEventListener('fetch', function (event) {
console.log(event.request.url);
event.respondWith(
caches.match(event.request).then(res => {
return res ||
fetch(event.request)
.then(responese => {
const responeseClone = responese.clone();
caches.open('sw_demo').then(cache => {
cache.put(event.request, responeseClone);
})
return responese;
})
.catch(err => {
console.log(err);
});
})
)
});
复制代码
咱们须要监听 fetch 事件,每当用户向服务器发起请求的时候这个事件就会被触发。有一点须要注意,页面的路径不能大于 Service Worker 的 scope,否则 fetch 事件是没法被触发的。
在回掉函数中咱们使用事件对象提供的 respondWith
方法,它能够劫持用户发出的 http 请求,并把一个 Promise 做为响应结果返回给用户。而后咱们使用用户的请求对 Cache Stroage 进行匹配,若是匹配成功,则返回存储在缓存中的资源;若是匹配失败,则向服务器请求资源返回给用户,并使用 cache.put
方法把这些新的资源存储在缓存中。由于请求和响应流只能被读取一次,因此咱们要使用 clone
方法复制一份存储到缓存中,而原版则会被返回给用户
在这里有几点须要注意:
对于用户发起的 POST 请求,咱们也能够在拦截后,经过判断请求中携带的 body 的内容来进行有选择的返回。
if(event.request.method === 'POST') {
event.respondWith(
new Promise(resolve => {
event.request.json().then(body => {
console.log(body); // 用户请求携带的内容
})
resolve(new Response({ a: 2 })); // 返回的响应
})
)
}
}
复制代码
咱们能够在 fetch 事件的回掉函数中对请求的 method 、url 等各项属性进行判断,选择不一样的操做。
对于静态资源的缓存,Cache Stroage 是个不错的选择;而对于数据,咱们可使用 IndexedDB 来存储,一样是拦截用户请求后,使用缓存在 IndexDB 中的数据做为响应返回,详细的内容我就不在这里讲了,有兴趣的同窗能够本身去了解下。
前面提到过,当有新的 service worker 文件存在的时候,他会被注册和安装,等待使用旧版本的页面所有被关闭后,才会被激活。这时候,咱们就须要清理下咱们的 Cache Stroage 了,删除旧版本的 Cache Stroage 。
this.addEventListener('install', function (event) {
console.log('install');
event.waitUntil(
caches.open('sw_demo_v2').then(function (cache) { // 更换 Cache Stroage
return cache.addAll([
'/style.css',
'/panda.jpg',
'./main.js'
])
}
))
});
const cacheNames = ['sw_demo_v2']; // Cahce Stroage 白名单
this.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all[keys.map(key => {
if (!cacheNames.includes(key)) {
console.log(key);
return caches.delete(key); // 删除不在白名单中的 Cache Stroage
}
})]
})
)
});
复制代码
首先在安装 Service Worker 的时候,要换一个 Cache Stroage 来存储,而后设置一个白名单,当 Service Worker 被激活的时候,将不在白名单中的 Cache Stroage 删除,释放存储空间。一样使用 event.waitUntil
,在 Service Worker 被激活前执行完删除操做。
Service Worker 做为一个新的技术,在静态资源缓存和处理多页面所需的复杂数据等方面都有很不错的应用前景。做为实现 PWA 不可或缺的一部分,我相信,不论是他的浏览器兼容性、功能的多样性以及文档的完整性,都会变的愈来愈好。
同时,确定还有不少我没有学到、讲到,或者是我忽略了 Service Worker 的内容存在,因此我但愿能够和你们一块儿学习,特别是 scope 这个属性,但愿有知道的同窗帮我答疑解惑,谢谢。
感谢阅读,未经容许,请勿转载:)