此次体验一种新的博客风格,咱们长话短说,针针见“血”。javascript
在深刻 Service Worker 以前,咱们须要快速回顾以下基础。php
诞生之初,JavaScript 是单线程的。css
进程有私有的虚拟地址空间、代码、数据和其它系统资源,进程申请建立和使用的系统资源会随其终止而销毁。线程运行在进程之中,系统建立进程以后就开始启动执行进程的主线程,并随主线程的退出而终止。html
JavaScript 做为浏览器脚本语言,为方便准确无误的操做 DOM,诞生之初便采用了单线程的方式。举个例子,若多线程同时分别删除和修改同一个 DOM,咱们很难预知其执行结果。java
但单线程中,必须经过异步和回调来优化耗时操做。git
咱们在网页上提交一个表单,并不但愿在提交后页面卡顿,一直等待服务端返回的提交结果。这时咱们须要能在单线程中发送异步请求,点击提交表单后能够先在页面进行其余操做。github
Ajax 让咱们能够向后端发送异步请求,同时不影响用户在界面中继续操做。当 Ajax 接收到服务端的响应以后,便经过回调函数执行以后的操做。一个典型的异步 Ajax 实战场景以下:chrome
// 生成可发送同步/异步请求的 XMLHttpRequest 对象实例 var oReq = new XMLHttpRequest(); // open 方法初始化请求方法、地址,第三个参数 true 声明进行异步请求 oReq.open("GET", "http://www.jianshu.com/", true); // 请求的整个过程当中有五种状态,且同一时刻只能存在一种状态: // 1. 未打开 // 2. 未发送 // 3. 已获取响应体 // 4. 正在下载响应体 // 5. 请求完成 // 当请求状态发生改变时,触发 onreadystatechange 会被调用 oReq.onreadystatechange = function (oEvent) { // 若是已经开始下载响应体了 if (oReq.readyState === 4) { // 若是响应体成功下载,而且服务端返回 200 状态码 if (oReq.status === 200) { // 打印响应信息 console.log(oReq.responseText); } else { console.log("Error", oReq.statusText); } } }; // send 方法发送请求,因为此请求是异步的,该方法马上返回 oReq.send(null);
当咱们的多个请求须要依赖于上一个请求的服务端响应时,回调函数中 Ajax 的层级逐步提升,可维护性极度降低,这就是回调地狱。编程
I Promise U that I`ll Marry U!!!后端
Promise 由 ES6 标准原生支持。正如题名,Promise 做出诺言,也要所以承担成功(fulfilled)或失败(rejected)的结果,以便解决回调地狱问题:
// 生成一个 Promise 实例,传入有特定的两个参数的匿名函数 // Promise 初始状态是 pending // resolve 被调用时,将 Promise 状态改成成功(fulfilled) // reject 被调用时,将 Promise 状态改成失败(rejected) // 该匿名函数抛出错误时,Promise 状态为失败(rejected) var a = new Promise(function(resolve, reject) { // setTimeout() 模拟异步请求,成功后执行 resolve() 方法 setTimeout(function() { resolve('1') }, 2000) }) a.then(function(val){ // then() 有两个函数做为参数,onfulfilled 和 onrejected // 当 Promise 状态为 fulfilled 时,调用 then 的 onfulfilled 方法 // 当 Promise 状态为 rejected 时,调用 then 的 onrejected 方法 console.log(val) // then() 方法返回 Promise 对象实例,因此可被链式调用 return new Promise(function(resolve, reject) { setTimeout(function() { resolve('2') }, 2000) }) }) .then(function(val) { // 链式调用的第二个环节,处理上一个环节返回的 Promise 对象 console.log(val) })
Promise 对象的生命周期以下图。
除了异步编程,咱们还能够有 Web Worker。
经过异步编程,咱们的页面能够边响应用户的下一步操做边等待服务端的回应,再也不拥有阻塞感,但 JavaScript 的单线程问题并无获得相应的解决。经过 HTML 5 标准支持的 Web Worker,咱们能够为 JavaScript 建立运行在后台的额外线程,并被多个页面共享。
在一个简单的 Web Worker 实例中,main.js 和 task.js 的源码以下。
// main.js // 实例化 Worker 对象,其实质为新建立的工做线程在主线程的引用 var worker = new Worker("task.js") // postMessage 方法与新建立的工做线程通讯 worker.postMessage({ id:1, msg:'Hello World' }); // 当 Worker 线程返回数据时,onmessage 回调函数执行 worker.onmessage = function(message) { var data = message.data; console.log(JSON.stringify(data)) // terminate 方法终止 worker 线程的运行 worker.terminate() }; // 当 Worker 线程出错时,onerror 回调函数执行 // error 参数中封装了错误对象的文件名、出错行号和具体错误信息 worker.onerror = function(error) { console.log(error.filename, error.lineno, error.message) }
// task.js onmessage = function(message) { var data = message.data data.msg = 'Hi from task.js' postMessage(data) }
在 Chrome 浏览器里,以上代码必须运行在 Web 容器如 Apache 中。同时,WebKit 内核加载并执行 Worker 线程的流程以下图所示。
上述知识点的详尽博客尽请期待,您能够先查阅其它资料进行补充。
Service Worker 基于 Web Worker 事件驱动。
Service Worker 一样能够在浏览器后台挂起新线程,来缓解 JavaScript 的单线程问题。而且,咱们能够用 Service Worker 拦截网络请求进行本地缓存或请求转发,至关于充当服务端与浏览器、浏览器与 Web 应用程序之间的代理服务器。
Service Worker 带来了速度,极大的提升了用户体验。
Service Worker 大量使用 Promise 对象。
由于一般 Service Worker 会等待响应后继续,并根据响应返回一个成功或者失败的操做。Promise 很是适合这种场景。
零、Service Worker 的生命周期。
所谓生命周期,包括 Service Worker 的注册、安装、激活、控制和销毁时的所有过程。咱们须要对 Service Worker 的生命周期有所了解。
先决条件:
控制时:处于两种状态之一:
1、注册 Service Worker。
当浏览器对 Service Worker 提供原生支持时,咱们即可以在页面加载后注册指定的 JavaScript 文件,并运行在后台线程之中,如下代码是这一过程的实例。
<!DOCTYPE html> <html> <head> <title>ServiceWorker</title> </head> <body> <h1>Hello World!</h1> <script> // 检查浏览器是否对 serviceWorker 有原生支持 if ('serviceWorker' in navigator) { // 有原生支持时,在页面加载后开启新的 Service Worker 线程,从而优化首屏加载速度 window.addEventListener('load', function() { // register 方法里第一个参数为 Service Worker 要加载的文件;第二个参数 scope 可选,用来指定 Service Worker 控制的内容的子目录 navigator.serviceWorker.register('./ServiceWorker.js').then(function(registration) { // Service Worker 注册成功 console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { // Service Worker 注册失败 console.log('ServiceWorker registration failed: ', err); }); }); } </script> </body> </html>
这里经过 php 内置命令监听项目目录,便能看到 Service Worker 注册成功。同时,在 Chrome 浏览器里,能够访问 chrome://inspect/#service-workers
和 chrome://serviceworker-internals/
来检查 Service Worker 是否已经启用。
2、安装 Service Worker。
安装阶段,咱们能够执行任何任务。这里咱们逐步打开缓存、缓存文件和确认全部须要的资产是否缓存。ServiceWorker.js
中的实例安装代码以下:
var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ '/', '/styles/main.css', '/script/main.js' ]; self.addEventListener('install', function(event) { // Perform install steps event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); });
这要求咱们在与项目根目录下创建 main.js
和 main.css
空文件。咱们能够在 Chrome 开发者工具里的“Application”菜单的“Cache Storage”中看到相应的缓存。而且在图中的“Service Workers”选项卡中看到正在运行的 Service Workers。
且从上面的代码能够看到,经过 Service Worker 对象加载的文件拥有全局变量 caches 等,而且 self 关键字指向这个对象自己。cache 使咱们能够存储网络响应发来的资源,而且根据它们的请求来生成 key。这个 API 和浏览器的标准的缓存工做原理很类似,且会持久存在,直到咱们释放主动空间——咱们拥有所有的控制权。
3、激活 Service Worker。
当 Service Worker 安装成功后,便被激活,这时可实时控制做用域中的全部网站,进行缓存文件等操做。不过首次使用 Service Worker 的页面须要再次加载才会受其控制。
4、控制 Service Worker
如下列举几个常见的 Service Worker 应用场景。
1. 文件缓存
self.addEventListener('fetch', function(event) { event.respondWith( // 如下方法检视请求,并从服务工做线程所建立的任何缓存中查找缓存的结果。 caches.match(event.request) .then(function(response) { console.log(event.request) console.log(caches) // 若是发现匹配的响应,则返回缓存的值 if (response) { return response; } return fetch(event.request); } ) ); });
经过上述文件缓存过程,咱们能够告诉 Service Worker 如何使用这些缓存文件,并经过 fetch 事件来捕获。fetch 事件只会在浏览器准备请求 Service Worker 控制的资源时才会被触发。这些资源包括了指定的 scope 内的文档,和这些文档内引用的其余任何资源。
2. 多页面传递消息
咱们能够打开多个 https://nzv3tos3n.qnssl.com/m... 测试页面来进行测试,效果以下。
其中,index.js
源码为:
(function () { if (navigator.serviceWorker) { // 获取页面 DOM 元素 var msgIpt = document.getElementById('ipt'), showArea = document.getElementById('show'), sendBtn = document.getElementById('sendBtn'); navigator.serviceWorker.register('service-worker3.js'); navigator.serviceWorker.addEventListener('message', function (event) { // 接受数据,并填充在 DOM 中 showArea.innerHTML = showArea.innerHTML + ('<li>' + event.data.message + '</li>'); }); sendBtn.addEventListener('click', function () { // 绑定点击事件,点击后发送数据 navigator.serviceWorker.controller.postMessage(msgIpt.value); msgIpt.value = ''; }); } })();
3. 更新 Service Worker
每次用户导航至使用 Service Worker 的站点时,浏览器会尝试在后台从新下载该脚本文件。这时新的 Service Worker 将会在后台安装,并在第二次访问时获取控制权,为了避免与新的 Service Worker 缓存的文件冲突,咱们可使用相似 caches.open('v2')
语句来建立新的缓存目录。
this.addEventListener('install', function(event) { event.waitUntil( // 建立新的缓存目录,并指定 caches.open('v2').then(function(cache) { return cache.addAll([ '/sw-test/', '/sw-test/index.html', … ]); }); ); });
当新的 Service Worker 激活,记得删除 v1 缓存目录,代码以下。
this.addEventListener('activate', function(event) { // 声明缓存白名单,该名单内的缓存目录不会被生成 var cacheWhitelist = ['v2']; event.waitUntil( // 传给 waitUntil() 的 promise 会阻塞其余的事件,直到它完成 // 确保清理操做会在第一次 fetch 事件以前完成 caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { if (cacheWhitelist.indexOf(key) === -1) { return caches.delete(key); } })); }) ); });
4. 预缓存
Service Worker 也能够在后台主动发送请求,优化用户体验,图片来源于《饿了么的 PWA 升级实践》。
5. Service Worker 支持的全部事件
5、销毁 Service Worker
浏览器决定是否销毁 Service Worker。在无痕浏览中,当页面关闭时相应的 Service Worker 会被销毁,所以尽可能不要在代码中留存全局变量。能够访问 chrome://inspect/#service-workers和 chrome://serviceworker-internals/ 来检查 Service Worker 是否已经停用。
困扰 Web 用户多年的难题——丢失网络链接,从 APPCache 到 Service Worker,解决办法一直在完善。Service Worker 开启的服务工做线程,对如何步入 Web 应用开发之旅,提供了很棒的切入角度。
那么,如何从本文开始,更好的学习 Service Worker?结合更多其它技术博客与 Service Worker 的 API 文档会更好。本文图片素材、写做思路多取源于此。
接口列表 | |
---|---|
Cache | CacheStorage |
Client | Clients |
ExtendableEvent | FetchEvent |
InstallEvent | Navigator.serviceWorker |
NotificationEvent | PeriodicSyncEvent |
PeriodicSyncManager | PeriodicSyncRegistration |
ServiceWorker | ServiceWorkerContainer |
ServiceWorkerGlobalScope | ServiceWorkerRegistration |
SyncEvent | SyncManager |
SyncRegistration | WindowClient |
本文全部关于 Web Worker、Service Worker 的代码,持续更新在个人 gist 中:
https://gist.github.com/hyler...
- Hello,我是韩亦乐,现任本科软工男一枚。软件工程专业的一路学习中,我有不少感悟,也享受持续分享的过程。若是想了解更多或能及时收到个人最新文章,欢迎订阅个人我的微信号:韩亦乐。个人简书我的主页中,有个人订阅号二维码和 Github 主页地址;[个人知乎主页]中也会坚持产出,欢迎关注。
- 本文内部编号经由个人 Github 相关仓库统一管理;本文可能发布在多个平台但仅在上述仓库中长期维护;本文同时采用【知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议】进行许可。