Service Worker 中的 ES 模块

importScripts() 的现代替代方案。

背景

ES 模块一直深受开发者喜爱。除了许多其他优势之外,它们还提供了一种通用模块格式,其中共享代码可以发布一次,并在浏览器和 Node.js 等替代运行时中运行。虽然所有现代浏览器都提供一些 ES 模块支持,但并非所有浏览器都支持在可以运行代码的所有位置提供支持。具体而言,在浏览器的 Service Worker 中导入 ES 模块的支持才刚刚开始普及。

本文详细介绍了常见浏览器中 Service Worker 中 ES 模块支持的当前状态,以及一些需要避免的陷阱,以及发布向后兼容的 Service Worker 代码的最佳实践。

使用场景

在 Service Worker 中使用 ES 模块的理想用例是加载现代库或配置代码,这些代码与其他支持 ES 模块的运行时共享。

在 ES 模块之前,尝试以这种方式共享代码需要使用较旧的“通用”模块格式(例如 UMD),其中包含不必要的样板,并编写对全局公开的变量进行更改的代码。

通过 ES 模块导入的脚本如果内容发生更改,可以触发 Service Worker 更新流程,这与 importScripts() 的行为一致。

当前限制

仅限静态导入

ES 模块可以通过以下两种方式之一导入:静态导入(使用 import ... from '...' 语法)或动态导入(使用 import() 方法)。在 Service Worker 中,目前仅支持静态语法。

此限制类似于对 importScripts() 用量施加的类似限制。对 importScripts() 的动态调用在 Service Worker 中不起作用,并且所有 importScripts() 调用(本质上是同步的)都必须在 Service Worker 完成其 install 阶段之前完成。此限制可确保浏览器在安装期间了解并能够隐式缓存 Service Worker 实现所需的所有 JavaScript 代码。

最终,此限制可能会被解除,并且可能会允许动态 ES 模块导入。目前,请确保仅在 Service Worker 中使用静态语法。

其他工作人员呢?

对“专用”工作器中的 ES 模块(使用 new Worker('...', {type: 'module'}) 构建的模块)的支持更为广泛,自版本 80 起,Chrome 和 Edge 以及最新版本的 Safari 都支持这些模块。专用 worker 支持静态和动态 ES 模块导入。

自版本 83 起,Chrome 和 Edge 已支持共享 worker 中的 ES 模块,但目前没有其他浏览器提供支持。

不支持导入映射

导入映射允许运行时环境重写模块说明符,例如,在可从中加载 ES 模块的首选 CDN 的网址前面加上前缀。

虽然 Chrome 和 Edge 89 版及更高版本支持导入映射,但目前无法与 Service Worker 搭配使用。

浏览器支持

从版本 91 开始,Chrome 和 Edge 支持 Service Worker 中的 ES 模块。

Safari 在 Technology Preview 122 版本中添加了支持,开发者应该会在未来的 Safari 稳定版中看到此功能。

示例代码

以下是在 Web 应用的 window 上下文中使用共享 ES 模块,同时注册使用同一 ES 模块的 Service Worker 的基本示例:

// Inside config.js:
export const cacheName = 'my-cache';
// Inside your web app:
<script type="module">
  import {cacheName} from './config.js';
  // Do something with cacheName.

  await navigator.serviceWorker.register('es-module-sw.js', {
    type: 'module',
  });
</script>
// Inside es-module-sw.js:
import {cacheName} from './config.js';

self.addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open(cacheName);
    // ...
  })());
});

向后兼容性

如果所有浏览器都支持 Service Worker 中的 ES 模块,则上述示例可以正常运行,但截至本文撰写时,情况并非如此。

为了适应没有内置支持的浏览器,您可以通过与 ES 模块兼容的打包器运行 Service Worker 脚本,以创建包含所有内嵌模块代码的 Service Worker,该 Service Worker 将在旧版浏览器中运行。或者,如果您尝试导入的模块已采用 IIFE 或 UMD 格式捆绑提供,则可以使用 importScripts() 导入这些模块。

当您有两个版本的 Service Worker 可用时(一个使用 ES 模块,另一个不使用),您需要检测当前浏览器支持的内容,并注册相应的 Service Worker 脚本。检测支持的最佳实践目前正在不断变化,但您可以关注此 GitHub 问题中的讨论,以获取建议。