大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
1. 什么是 Web Worker一直阻碍 JavaScript 的因素实际上是语言本身,JavaScript 是一种单线程环境,即多个脚本无法同时运行。例如:处理界面事件、查询和处理大量 API 数据以及操作 DOM。
开发者虽然可以使用 setTimeout()、setInterval()、XMLHttpRequest 和事件处理脚本等技术来模拟 “并发”。虽然功能是异步的,但不阻塞不一定就意味着并发,系统会在生成当前执行脚本后处理异步事件。
Web Worker 规范定义了一个用于在 Web 应用中生成后台脚本的 API。借助 Web Workers,开发者可以执行一系列操作,例如:触发长时间运行的脚本来处理计算密集型任务,同时不会阻止界面或其他脚本处理用户互动。
Worker 利用类似线程的消息传递实现并行,非常适合用来保持界面刷新、性能和响应速度。同时,Service Worker 还可以嵌入到 PWA 中,使应用程序能够离线运行,此时 Service Worker 相当于扮演了某种虚拟代理。
但是值得一提的是,Web Worker 无权访问 DOM,因此无法更改 Web 应用程序的外观。
2.Service Worker 本质是 Web WorkerService worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,其会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源,其还提供入口以访问推送通知(Push Notification)和后台同步 API(Background Sync API)。
Push API:给与 Web 应用程序接收从服务器发出的推送消息的能力,无论 Web 应用程序是否在用户设备前台,甚至刚加载完成。这样,开发人员就可以向用户投放异步通知和更新,从而让用户能更及时地获取新内容。Background Synchronization API: 使 Web 应用程序能够推迟任务,以便一旦用户拥有稳定的网络连接就可以在 Service Worker 中运行。其实,为 Service Worker 编写代码时,很多都是信手拈来,比如:可以使用 JavaScript 语言、可以像监听界面事件一样监听生命周期事件、可以像往常一样使用 Promise 来管理控制流。但是,有时 Service Worker 的行为会导致开发者不知所措,比如:当刷新页面却没有看到代码更改实时生效。
与传统 Web 应用程序架构相比,Service Worker 相当于构造了第三、即中间层,该层用于控制和操作客户端和服务器之间发送的所有数据。
开发者还可以将 Service Worker 视为一种浏览器扩展程序,即网站可以安装在用户浏览器中的一种扩展程序。安装后,Service Worker 会通过强大的中间层扩展网站的浏览器,Service Worker 层可拦截和处理网站发出的所有请求。
同时,Service Worker 层还有自己的生命周期,独立于浏览器标签页。简单的页面刷新不足以更新 Service Worker,同时每个层都有自己唯一的更新规则。
3.Service Worker 能力强大但依然有限在 Web 上使用 Service Worker 可以带来诸多好处,比如:
即使用户处于离线状态也可以顺利运行通过缓存大幅提升性能允许使用推送通知允许作为 PWA 安装注意:即使用户离开网站或关闭标签页,活跃的 Service Worker 仍会继续工作。浏览器会保留此 Service Worker,以便其在下次用户返回网站时做好准备。在发出第一个请求之前,Service Worker 将有机会拦截该请求并控制页面,此时网站甚至可以离线工作,即 Service Worker 可以提供网页的缓存版本,即使用户没有连接到互联网也是如此。
但是,Service Worker 虽然强大也依然受到设计的限制,比如:无法进行同步,更不能与网站在同一会话中执行任何操作,这也意味着 Service Worker 无法访问以下对象。
localStorageDOM窗口但是,网页可以通过多种方式与其 Service Worker 通信,包括: postMessage、一对一消息通道(MessageChannel)和一对多广播(BroadcastChannel)通道。
下面是 Service Worker 中使用 postMessage 通信的示例:
navigator.serviceWorker.register("service-worker.js");navigator.serviceWorker.ready.then((registration) => { registration.active.postMessage( "Test message sent immediately after creation", );});如果是 MessageChannel 可以通过下面的方式:
var messageChannel = new MessageChannel();messageChannel.port1.addEventListener('message', replyHandler);worker.postMessage(data, [messageChannel.port2]);function replyHandler (event) { console.log(event.data);}// 下面是 Service Worker 中代码self.addEventListener('message', function handler (event) { event.ports[0].postMessage(data);});注意 :开发者可以理解为 Service Worker 独立于页面并能与之通信,但无法直接访问页面。
4.Service Worker 常见注意事项停止 Service WorkerService Worker 可以随时停止,浏览器不希望将资源浪费在没有执行任何操作的 Service Worker 上。停止与终止并不相同,Service Worker 仍保持安装和激活状态,其直接进入休眠状态了。下次需要请求时,浏览器会将其唤醒。
event.waitUntil由于 Service Worker 随时可能会休眠,因此需要通过某种方式让浏览器知道其何时正在执行重要的操作,比如: event.waitUntil() 。此方法会延长其生命周期,使其不会停止并进入其生命周期的下一阶段,直到准备就绪,从而有时间设置缓存、从网络中提取资源等。
以下示例告知浏览器,在缓存创建完毕并填充图片之前, Service Worker 并未完成安装:
self.addEventListener("install", (event) => { const preCache = async () => { const cache = await caches.open("static-v1"); return cache.addAll(["/", "/about/", "/static/styles.css"]); }; event.waitUntil(preCache());});这里重点讨论两个方法:
caches.open:返回一个 Promise,解析为与 cacheName 匹配的 Cache 对象addAll() 方法接受一个 URL 数组,检索并将生成的 response 对象添加到给定的缓存中。在检索期间创建的 request 对象成为存储的 response 操作的 key。全局状态当 start/stop 时,Service Worker 全局作用域会重置。因此,不要在 Service Worker 中使用全局状态,否则下次其被唤醒可能会出现与预期不同的状态。
// 这里使用了全局变量 favoriteNumber,每次都会重新生成const favoriteNumber = Math.random();let hasHandledARequest = false;self.addEventListener("fetch", event => { console.log(favoriteNumber); console.log(hasHandledARequest); hasHandledARequest = true;});在每个 fetch 中,Service Worker 都将打印一个数字,假设为 0.13981866382421893,hasHandledARequest 变量也会更改为 true。此时,Service Worker 会暂时处于空闲状态,因此浏览器会停止它。下次有请求时,浏览器会再次唤起 Service Worker,同时执行脚本,此时,hasHandledARequest 已重置为 false,而 favoriteNumber 则完全不同,即 0.5907281835659033。
因此,开发者不要依赖于 Service Worker 中存储的状态。此外,创建 Message Channel 等内容的实例也可能会导致 bug,即每次 Service Worker stops/start 时都将获得一个全新的实例。
Service Worker 连接在一起,但相互独立网页一次只能由一个 Service Worker 控制,但可以同时安装两个 Service Worker。
当对 Service Worker 代码进行更改并刷新页面时,实际上并没有编辑 Service Worker,因为 Service Worker 是不可变的。新的 Service Worker 会安装,但不会激活,其必须等待当前 Service Worker 终止(当用户离开网站时)。
if ("serviceWorker" in navigator) { navigator.serviceWorker .register("/sw.js", { scope: "/"}) .then((registration) => { // registration worked console.log("Registration succeeded."); registration.unregister().then((boolean) => { // if boolean = true, unregister is successful }); }) .catch((error) => { // registration failed console.error(`Registration failed with ${error}`); });}与其他 Service Worker 的缓存相混淆在安装过程中,SW2 可以进行一些设置 ,通常是创建和填充缓存。
但请注意,新的 Service Worker 可以访问当前 Service Worker 可以访问的所有内容。如果不小心,新的等待 Service Worker 可能会把当前 Service Worker 弄得一团糟,比如:
SW2 可以删除 SW1 正在使用的缓存SW2 可以修改 SW1 正在使用的缓存的内容,导致 SW1 提供页面不需要的资源进行响应跳过等待Service Worker 还可以使用存在风险的 skipWaiting() 方法在完成安装后立即控制页面。除非有意尝试替换有问题的 Service Worker,否则通常不建议采用这种做法,新 Service Worker 可能正在使用当前页面不需要的更新资源,从而导致错误和 bug。
self.addEventListener("install", (event) => { // 可以安全地忽略 skipWaiting() 返回的 Promise self.skipWaiting(); // 执行所需的任何其他操作 // 要安装的 Service Worker,可能位于内部 event.waitUntil();});虽然 self.skipWaiting() 可以在 Service Worker 执行期间的任何时刻调用,但只有在新安装的 Service Worker 可能在 waiting 状态时才会起作用。 因此,通常从 InstallEvent 处理程序内部调用 self.skipWaiting()。
开始清理防止 Service Worker 相互破坏的方法是确保其使用不同的缓存,最简单的方法是对所使用的缓存名称进行版本控制。
// 使用版本管理const version = 1;const assetCacheName = `assets-${version}`;self.addEventListener("install", event => { caches.open(assetCacheName).then(cache => { // 自信地使用自己的缓存进行操作 });});部署新的 Service Worker 时,会自增 version,使其使用与上一个 Service Worker 完全独立的缓存来执行所需的操作。
结束清理一旦 Service Worker 达到 activated 状态就会知道其已被接管,因此上一个 Service Worker 就是多余的。此时,务必在旧的 Service Worker 之后进行清理,这样不仅可以遵循用户缓存存储空间限制,还可以防止出现意外的错误。
cache.match(request, { options}).then(function (response) { // 注意: Cache.match() 和 Cache.matchAll() 基本一样,但前者只解析为 response[0] 而不是所有 response});caches.match() 是一种常用的快捷方式,用于从存在匹配项的任何缓存中检索项,其会按照创建顺序遍历缓存。因此,假设在两个不同的缓存(assets-1 和 assets-2)中有脚本文件 app.js 的两个版本,网页正在等待存储在 assets-2 中的新脚本。但如果未删除旧缓存,caches.match('app.js') 将返回 assets-1 中的旧缓存,且很有可能会破坏网站。
因此,可以通过下面的方法删除 Service Worker 中不需要的任何缓存:
const version = 2;const assetCacheName = `assets-${version}`;self.addEventListener("activate", event => { event.waitUntil( // 删除不需要的缓存 caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== assetCacheName){ return caches.delete(cacheName); } }); ); }); );});caches.keys:返回一个 Promise ,该 Promise 将解析为一个 Cache 键数组caches.delete(key):查询 request 为 key 的 Cache 条目,如果找到则删除该条目并返回 resolve 为 true 的 Promise 。如果没有找到则返回 resolve 为 false 的 Promise 。5.本文总结本文主要和大家介绍 Service worker ,其本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器,旨在创建有效的离线体验,还提供入口以推送通知和访问后台同步 API。因为篇幅问题,关于 Service worker 主题只是做了一个简短的介绍,但是文末的参考资料提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。如果大家有什么疑问欢迎在评论区留言。
参考资料注意:本文部分内容借鉴了Dave Geddes的文章《Service worker mindset》
https://www.bothrs.com/story/heres-why-service-workers-have-superpowers
https://web.dev/articles/service-worker-mindset?hl=zh-cn
https://developer.chrome.com/blog/broadcastchannel?hl=zh-cn
https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel
https://web.dev/learn/pwa/service-workers?hl=zh-cn
https://ponyfoo.com/articles/serviceworker-messagechannel-postmessage
https://developer.mozilla.org/zh-CN/docs/Web/API/Cache/match
https://googlechrome.github.io/samples/service-worker/custom-offline-page/index.html
https://www.youtube.com/watch?app=desktop&v=1usuYqZMT7Q