likes
comments
collection
share

揭秘 Service Worker 技术的奥秘

作者站长头像
站长
· 阅读数 37

原创 袁亮 / 叫叫技术团队

引言

随着互联网的迅猛发展,网络应用正变得越来越强大和复杂。然而,网络连接的不可靠性和限制性仍然是用户面临的常见挑战之一。Service Worker 的出现不仅为开发者提供了更大的灵活性和控制权,也为用户带来了更快、更可靠的网络体验。无论您是一个开发者、一个技术爱好者,本文都将帮助您了解 Service Worker 基础知识及使用场景。

本文将围绕如下几点展开讲解

  • Service Worker 的基本原理是什么?
  • Service Worker 有哪些使用场景?
  • Service Worker 是如何使用的?

先体验

  1. 打开梦可宝图鉴
  2. 确任页面加载完成或显示 Ready to work offline 后关闭
  3. 断开网络连接重新打开页面发现网页正常工作而且是秒开

这就是使用 Service Worker 在离线访问场景应用下达成的效果

介绍

Service Worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。Service Worker 运行在一个独立的线程中,独立于网页运行,可以在后台处理网络请求,缓存资源,并满足像离线访问、推送通知等高级功能的需求。 揭秘 Service Worker 技术的奥秘

产生背景

Service Worker 技术的出现源于 Web 应用程序对性能和用户体验不断提升的要求,以及旧有技术在实现离线访问、推送通知和高效网络请求等方面的局限性。 在传统的 Web 应用程序中,客户端需要频繁地与服务器通信,从而导致网络延迟和响应时间较长的问题。同时,在断网的情况下,Web 应用程序无法正常工作,这也限制了 Web 应用程序的使用场景和可用性。 为了解决这些问题,开发者们在 HTML5 标准化的过程中引入了离线缓存、Web Workers 等技术。虽然这些技术已经在一定程度上提高了 Web 应用程序的可用性和性能,但它们仍存在一些限制:

  • 离线缓存需要额外的代码和配置来实现,而且在离线情况下缓存的数据也不能自动更新。
  • Web Workers 虽然可以使用多线程加速应用程序的处理速度,但它们无法在后台运行,且不能在不同的线程之间进行通信。

针对这些局限性,Service Worker 技术的出现填补了这一空白。Service Worker 可以使用多线程处理大量的 data,可以缓存应用程序的资源,在离线情况下也能正常工作,并且可以处理推送通知、优化网络请求等任务。这些能力使得 Service Worker 技术成为构建高性能、高可用性 Web 应用程序的重要工具之一。

Cache简单介绍

Cache API 是专门为 Service Workers 设计的,用于缓存网络请求和响应的异步储存方案。

MDN 介绍中 Cache 是一个实验中的功能,在低版本浏览器中可能无法正常工作注意兼容性问题。或者引入 Cache Api polyfill

揭秘 Service Worker 技术的奥秘

图:Cache API 是以 Requst 做为 keyResponse 做为 value 进行存储的

Cache 提供一个 Request -> Response 的持久缓存,除非显式删除,存储在 Cache 里面的数据不会主动过期,同时也不会主动去更新,需要手动维护其更新。

Cache 基础用法

存入Cache Storage

通过名称标识获取一个Cache对象

// 通过一个 cacheName 来获取对应的缓存对象
const cache = await caches.open('hello-cache-v1');

将响应内容(Response)存入指定的缓存中,add()addAll() 方法都是将返回的Response对象添加到 Cache 中,但是对于更复杂的操作推荐使用 put() 方法,具体差异可以参考 Cache api

// 请求一个txt文本资源
const request = new Request("/static/pre_fetched.txt", { method: "GET" });
fetch(request).then((response) => {
  // 成功后可以通过 Cache.put 方法将缓存设置进去
  caches.open("hello-cache-v1").then((cache) => {
    cache.put(request, response);
  });
});

可以看到/static/pre_fetched.txt文本内容已被存进缓存

揭秘 Service Worker 技术的奥秘

获取上次存储的 Response

使用 match 方法匹配一个 request 对应的 response, 如果匹配到直接返回命中的 Respose,未匹配则返回 undefined

// 再次请求pre_fetched.txt文本内容
const request = new Request("/static/pre_fetched.txt", { method: "GET" });
caches.open("hello-cache-v1").then((cache) => {
  // 获取上次存储的 Response
  cache.match(request).then((matchResponse) => {
    matchResponse.text().then((txt) => {
      console.log(txt);
    });
  });
});

打印结果

揭秘 Service Worker 技术的奥秘

手动更新

可以看到将 pre_fetched.txt 中文本内容更新,再次获取文本内容输出的始终是 Cache Storage 中内容,体现了 Cache 不会主动更新的特点,如果要更新可以再次使用 cache.put() 方法更新。

caches.open("hello-cache-v1").then((cache) => {
	cache.put(request, response);
}

手动删除

caches.open("hello-cache-v1").then(function (cache) {
  cache.delete("/static/pre_fetched.txt").then(function (response) {
    console.log("done");
  });
});

Service Worker

简单介绍了Cache Api回归正题我们先从如何注册一个Service Worker开始

注册

Service Worker 被安装之前,首先需要在HTML页面内的脚本里注册 server worker 的脚本,通过 register 方法即可注册一个 Service Worker,接受 2 个参数 scriptURLoptions(可选)

注意:一个页面只允许注册一个Service Worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('sw.js')
  .then(function(registration) {
    // success
  }).catch(function(error) {
    // error
  });
} else {
  // The current browser doesn't support service workers.
}

成功注册一个Service Worker

揭秘 Service Worker 技术的奥秘

作用范围Scope

options 选项中接受一个 scope,定义 service worker 注册范围的URL,表示 Service Worker 控制范围的 URL 子集。例如,如果你将 scope 设置为 /app/,那么 Service Worker 只会控制以 /app/ 开头的 URL。如果你没有提供 scope,那么默认的作用范围将是 Service Worker 脚本 URL 的目录。 例如:路径是 /sw.js 的 Service Worker,其作用范围默认是 /** ,因此 /** 下面的页面(如 /index.html)可以被 Service Worker 控制,如果我将 scope 调整为 /subdir 那么 index.html 中的资源请求,发送 postMessage 消息等都无法被 Service Worker 控制。

生命周期

揭秘 Service Worker 技术的奥秘

  1. Installing: 这是 Service Worker 生命周期的第一个阶段。在这个阶段,Service Worker 脚本被下载和安装。如果 Service Worker 脚本是第一次被注册,或者已经注册的 Service Worker 脚本发生了改变,那么就会触发安装过程。如果在安装过程中没有发生错误,那么** **Service Worker 将进入 installed 状态。
  2. Installed: 在这个阶段,Service Worker 已经被成功安装,但是还没有激活。如果当前已经有一个激活的 Service Worker,那么新安装的 Service Worker 将会进入等待状态,直到所有打开的页面都不再使用旧的 Service Worker。
  3. Activating: 当没有页面在使用旧的 Service Worker 时,新的 Service Worker 将进入激活阶段。在这个阶段,你可以执行一些更新操作,例如清理旧版本的 Service Worker 缓存的资源。如果在激活过程中没有发生错误,那么 Service Worker 将进入 activated 状态。
  4. Activated: 在这个阶段,Service Worker 已经被激活,并且可以控制页面。Service Worker 可以开始处理 fetchmessage事件,拦截和处理网络请求,以及与页面进行通信。
  5. Redundant: Service Worker 可能会因为多种原因进入 redundant 状态。例如,如果在安装或激活过程中发生错误,或者有一个新的 Service Worker 取代了它,那么它将进入 redundant 状态。进入这个状态的 Service Worker 将不再控制页面,也不会再接收到事件。

这些状态改变的过程是完全由浏览器自动管理的,但是你可以通过在 Service Worker 脚本中添加事件监听器,来在每个状态改变时执行自定义的代码。例如,你可以在 installactivate 事件的监听器中添加缓存管理的代码,或者在 fetch 事件的监听器中添加请求处理的代码。

生命周期事件

Service Worker 中的主要生命周期阶段的变化,是通过事件来通知脚本的,所以 Serice Worker 的脚本主要需要做的是为不同生命周期事件绑定好对应的处理器。核心的生命周期事件处理器如图示: 揭秘 Service Worker 技术的奥秘

安装事件 (Install)

在下载事件被浏览器响应完成后,安装事件是 Service Worker 生命周期中第一个被触发的,所以它非常重要。我们通常使用它来完成各种初始化任务,主要是资源的预加载、缓存以及持久化一些长期有效的状态。 监听到安装事件就可以预先拉取一些资源,并在应用缓存中存储它们并用给定的名字标识,参见下面的代码示例

const CACHE_NAME = "cache-v1";
const urlsToCache = ["/static/pre_fetched.txt", "/static/pre_fetched.html"];
// 监听安装事件
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      console.log("Opened cache");
      return cache.addAll(urlsToCache);
    })
  );
});

激活事件 (activate)

当 Service Worker 已经就绪,可以控制页面请求并处理功能事件如 fetchpushsync,激活事件就会被触发,我们监听激活事件的处理器就可以响应。

self.addEventListener('activate', event => {
  console.log("activate event");
});

激活事件标志着从这之后的请求将被 Service Worker 接管,所以通常用于区分 Service Worker 接管前后的分隔。

请求事件 (fetch)

fetch 事件被触发于页面对网络发起任意请求,看一个简单示例,Service Worker 劫持 fetch 请求替换图片

const CACHE_NAME = "cache-v1";
const urlsToCache = ["/static/jojo.png"];
// 监听安装事件
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      // 存入Cache
      return cache.addAll(urlsToCache);
    })
  );
});
// fetch事件
self.addEventListener("fetch", (event) => {
  console.log(`Handling fetch event for ${event.request.url}`);
  var requestUrl = new URL(event.request.url);
  // 改写响应,从Cache中返回jojo.png
  if (requestUrl.pathname === "/static/pig.png") {
    event.respondWith(caches.match("/static/jojo.png"));
  }
});
  • 在安装事件中 Service Worker 请求了 /static/jojo.png 并加入缓存。
  • 而在 fetch 事件中,我们劫持了网络请求,当资源目标为 /static/pig.png 时,service worker 没有转发请求,而是从缓存列表取出 /static/jojo.png,响应给了页面达到了图片劫持替换的效果。

第一次访问 揭秘 Service Worker 技术的奥秘 再次访问时 揭秘 Service Worker 技术的奥秘

sync事件

Service Worker 的 sync事件用于在用户设备重新连上网络时执行某些任务。这在用户离线时保存数据,然后在用户重新上线时进行网络请求的场景中特别有用。

当用户设备离线时,你可以在 Service Worker 中使用 SyncManager 接口的 register 方法注册一个同步任务:

self.addEventListener('fetch', event => {
  if (!navigator.onLine) {
    event.waitUntil(
      // 注册一个名为 'myFirstSync' 的同步任务
      self.registration.sync.register('myFirstSync')
    );
  }
});

然后,你可以在 Service Worker 中监听 sync 事件,并在事件处理函数中执行网络请求:

self.addEventListener('sync', event => {
  if (event.tag === 'myFirstSync') {
    event.waitUntil(
      // 在这里执行网络请求
      fetch('...')
    );
  }
});

push事件

Service Worker 中的 push 事件是在接收到推送通知时触发的事件。当 Service Worker 接收到推送通知时,会触发 push 事件,以便在后台处理该通知。

请求用户的通知权限

// 请求通知权限
Notification.requestPermission()
.then(function(permission) {
  if (permission === 'granted') {
    console.log('Notification permission granted.');
  } else {
    console.log('Unable to get permission to notify.');
  }
});

推送一条push消息 揭秘 Service Worker 技术的奥秘 Service Worker接收到 push 消息并通知用户

self.addEventListener("push", function (event) {
  const data = event.data.json();
  console.log("[Service Worker] Push Received.", data);

  const options = {
    body: data.body,
    icon: data.icon
  };
  
  event.waitUntil(self.registration.showNotification(data.title, options));
});

message事件

message 事件是用于通信的,处理从页面(或其他 Service Worker)发送来的消息

// 通信消息
self.addEventListener("message", (event) => {
  console.log("Received a message from page: ", event.data);
});

与渲染进程通信

Service Worker 可以通过 postMessage 与渲染进程(通常是浏览器的主线程,也就是运行你的网页的地方)进行通信

Service Worker 向渲染进程发送消息: Service Worker 可以通过 clients.matchAll 方法获取所有受其控制的客户端(Client 对象代表了一个由 Service Worker 控制的页面),然后使用 postMessage 方法向其中的一个或多个客户端发送消息。

self.addEventListener('activate', event => {
  // 发送消息到客户端
  event.waitUntil(clients.matchAll().then(all => {
    all.forEach(client => {
      client.postMessage('Hello from the service worker');
    });
  }));
});

渲染进程接收消息: 在渲染进程(页面)中,你可以监听 message 事件来接收 Service Worker 发送的消息。

navigator.serviceWorker.addEventListener('message', event => {
    console.log('Received a message from service worker: ', event.data);
});

渲染进程向 Service Worker 发送消息: 在渲染进程中,你可以通过 Service Worker 属性获取 Service Worker 注册对象,然后使用 postMessage 方法向 Service Worker 发送消息。

navigator.serviceWorker.controller.postMessage('Hello from the page');

Service Worker 接收消息: 在 Service Worker 中,你可以监听 message 事件来接收页面发送的消息。

self.addEventListener('message', event => {
    console.log('Received a message from page: ', event.data);
});

更新Service worker

下面是 Service worker 更新流程

  1. 更改 Service Worker 脚本: 当你需要更新 Service Worker 时,首先需要修改 Service Worker 脚本的内容。这可能包括添加新的事件处理函数,修改缓存策略等。
  2. 浏览器检查更新: 每次页面加载时,浏览器都会检查 Service Worker 脚本是否有更新(比如脚本内容发生变化)。这个检查是字节级别的,所以即使只是脚本中的一个字符发生了变化,浏览器也会认为 Service Worker 脚本已经更新。
  3. 安装新的 Service Worker: 如果浏览器检测到 Service Worker 脚本有更新,那么它会开始安装新的 Service Worker。新的 Service Worker 将进入 installing状态,并触发install 事件。
  4. 新的 Service Worker 进入等待状态: 安装完毕后,新的 Service Worker 会进入 waiting 状态,等待旧的 Service Worker 释放对当前页面的控制权。
  5. 激活新的 Service Worker: 当旧的 Service Worker 不再控制任何页面时,新的 Service Worker 将进入 activating 状态,并触发 activate 事件。在这个阶段,你可以清理旧版本的 Service Worker 缓存的资源。
  6. 新的 Service Worker 开始控制页面: 当新的 Service Worker 完全激活后,它将开始控制页面,处理 fetchmessage 事件。

需要注意的是,如果你希望新的 Service Worker 在页面下次加载时立即生效,可以在 Service Worker 脚本中调用 self.skipWaiting() 方法。这将跳过 waiting 状态,直接激活新的 Service Worker。同样,如果你希望新的 Service Worker 在激活后立即开始控制页面,可以调用 clients.claim() 方法。 另外,如果在开发过程中需要频繁更新 Service Worker,你可能需要在浏览器的开发者工具中启用 "Update on reload" 选项,这将在每次页面重新加载时强制更新 Service Worker。 揭秘 Service Worker 技术的奥秘

常见缓存策略

Service Worker 可以实现多种缓存策略,下面是一些常见的策略:

  1. Cache First(缓存优先): 这种策略首先尝试从缓存中获取请求的资源。如果缓存中有对应的资源,那么直接使用缓存中的资源;如果缓存中没有对应的资源,那么向网络发送请求。这种策略适合用于那些不经常变化的资源,例如 CSSJavaScript 文件或者图片等。 揭秘 Service Worker 技术的奥秘

  2. Network First(网络优先): 这种策略首先尝试向网络发送请求。只有当网络请求失败(例如网络不可用)时,才会从缓存中获取资源。这种策略适合用于那些需要实时更新的资源。揭秘 Service Worker 技术的奥秘

  3. Cache Only(只用缓存): 这种策略只使用缓存中的资源,不向网络发送请求。如果缓存中没有对应的资源,那么请求失败。这种策略适合用于离线应用。 揭秘 Service Worker 技术的奥秘

  4. Network Only(只用网络): 这种策略只向网络发送请求,不使用缓存。这种策略适合用于那些无法被缓存的资源。 揭秘 Service Worker 技术的奥秘

  5. Stale While Revalidate(先用缓存,后台更新): 这种策略首先从缓存中获取资源并返回,然后在后台向网络发送请求,更新缓存。这种策略可以快速响应请求,同时保证缓存中的资源是最新的。 揭秘 Service Worker 技术的奥秘

以上都是常见的缓存策略,但在实际使用中,可能需要根据具体的应用需求,选择合适的策略,或者组合使用多种策略。例如,对于 CSSJavaScript 文件,可能使用 Cache First 策略;而对于新闻或者文章,可能使用 Network First 或者 Stale While Revalidate 策略。

兼容性

Service Worker是现代浏览器的一个高级特性,它依赖于 fetch APICache ApiPromise 等。可以看出 Service Worker 在主流浏览器中已经得到了较好的支持,但仍有一些浏览器版本存在兼容性问题,如 Internet ExplorerSafari 10 或更早的版本。

揭秘 Service Worker 技术的奥秘 使用Service Worker的必要条件:

  • 浏览器支持(在这里测试你的浏览器是否支持)
  • 必须使用 HTTPS

注意事项

  • 出于安全考量,Service worker 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险,如果允许访问这些强大的 API,此类攻击将会变得很严重
  • Service Worker 是独立于渲染上下文的独立线程,所以它们都是无法直接操作 DOM 或者 window 对象的。如果我们有和其他 Worker 或者页面交互的需求,可以使用 postMessagemessage 事件来进行进程/线程间通信
  • 在 Service Worker 中使用 fetch API 来转发请求,请求中默认不会包含 cookie 等中的用户认证信息。如果需要为转发请求附带认证信息,在 fetch 请求中添加 credentials 的参数:
fetch(url, {
  credentials: 'include'
})
  • 如果目标资源支持 CORS,在构建请求需要附带参数 {mode: 'cors'}
  • 30XHTTP 状态码尚不支持离线请求重定向,这是一个已知的issue。建议在官方支持离线重定向前,根据你的使用场景寻找其他方案
  • 在使用 Service Worker 代理HTTP的响应体时,务必记住 clone response,而不要直接消费掉响应体。 原因是 HTTP response 是一个流,它的内容只能被消费一次。 只要我们仍然希望既能让浏览器正确的获得响应体中的内容,又能是它被缓存或者在 Service Worker 作内容检查,请不要忘记复制一个响应体。

使用场景

  1. 全静态站点 如果一个网站只包含静态数据而无需服务,我们可以缓存所有的 html 页面,css 样式,脚本和图片等资源,来使得这个页面在初次打开后可以被完全地离线访问。例如前面提到的宝可梦图鉴
  2. 预加载 为了优化首屏渲染,页面上非必要的资源通常被延迟加载直到它们被需要。这类资源使用Server Worker来加载既可以使得在需要被加载时有良好的体验,又不会影响到首屏性能。 Demo / Demo prefetch video
  3. 应变响应 有时候 HTTP 请求可能会因为不确定因素失败(如服务器离线,网络中断等),此时为用户提供一个应变的响应比如展示上一次成功加载的资源/数据。 Service worker 可以帮助验证请求是否成功,如果失败就提供应变策略的回复。 Demo
  4. 仿造响应 仿造响应是非常有用。它可以帮助我们隔离部分特定的请求来使用给定的回复,或者我们可以用它来测试一些尚不可用,或者不能稳定重现问题的资源或者 REST API.
  5. 窗口缓存 Service Worker 来承担缓存数据的责任,页面可以直接使用 window.cache 来访问缓存。 通过窗口缓存作为媒介可以间接实现 service worker 向页面的数据传递,也可以将 Service Worker 用作缓存的生产者而页面作为消费者。

更多使用场景可以参考官方示例:googlechrome.github.io/samples/ser…

价值

使用 Service Worker 技术构建 Web 应用程序,可以带来以下价值:

  • 离线访问: Service Worker 可以将 Web 应用程序离线缓存,使得用户在离线情况下仍然可以访问应用程序,大大提高了用户体验和可用性。
  • 高性能和低延迟: Service Worker 可以缓存资源和响应请求,从而降低网络延迟和提高应用程序的性能。
  • 推送通知: Service Worker 可以以后台模式运行并接收推送通知消息,为用户提供实时的更新和信息推送。
  • 更好的用户体验: Service Worker 可以提高应用程序的响应速度和性能,并实现像离线访问、推送通知等功能,从而提升了应用程序的用户体验。

合理优化和使用 Service Worker 技术,可以为 Web 应用程序带来更好的用户体验和更高的性能表现。

结语

最后们可以得出一些关键的结论: 首先 Service Worker 是一种强大的网络技术,它能够使开发人员在用户的浏览器中运行后台脚本,提供离线体验,以及系统通知等功能。 其次,通过拦截和处理网络请求,Service Worker 可以有效地创建可靠的性能体验,即使在网络不稳定或完全离线的情况下也能如此。 然而,也有一些挑战,例如更新 Service Worker 的复杂性,需要制定明智的缓存策略,以及有些浏览器的兼容性问题等。总的来说,尽管 Service Worker 技术有其挑战,但其提供的丰富功能和优化用户体验的能力使其在 Web 开发中具有较大的潜力。

参考文献