Service Worker 中的 ES 模块

importScripts() 的新式替代方案。

背景

ES 模块一直是开发者的最爱。除了许多其他优势之外,它们还承诺提供通用模块格式,这样共享的代码只需发布一次,即可在浏览器和 Node.js 等替代运行时中运行。虽然所有新型浏览器都提供某种 ES 模块支持,但并非所有浏览器都支持在所有可以运行代码的位置运行 ES 模块。具体而言,对在浏览器的 Service Worker 中导入 ES 模块的支持才刚刚开始广泛提供。

本文详细介绍了各大常用浏览器中服务工件对 ES 模块的支持情况,以及一些需要避免的注意事项,以及发布向后兼容的服务工件代码的最佳实践。

使用场景

在服务工件中使用 ES 模块的理想用例是加载与支持 ES 模块的其他运行时共享的现代库或配置代码。

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

如果通过 ES 模块导入的脚本的内容发生变化,则可以触发服务工作器更新流程,与 importScripts()行为一致。

当前限制

仅限静态导入

ES 模块可以通过以下两种方式导入:使用 import ... from '...' 语法进行静态导入,或使用 import() 方法进行动态导入。在服务工件内,目前仅支持静态语法。

此限制类似于对 importScripts() 使用施加的类似限制。对 importScripts() 的动态调用无法在服务工件内运行,并且所有 importScripts() 调用(本质上是同步调用)都必须在服务工件完成其 install 阶段之前完成。此限制可确保浏览器知道服务工件实现期间所需的所有 JavaScript 代码,并能够隐式缓存这些代码。

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

其他工人呢?

“专用”Worker 中的 ES 模块(即使用 new Worker('...', {type: 'module'}) 构建的模块)的支持更为广泛,从 Chrome 80 版开始,Chrome 和 Edge 都支持此类模块,最新版 Safari 也支持。专用工作器支持静态和动态 ES 模块导入。

版本 83开始,Chrome 和 Edge 支持共享工作器中的 ES 模块,但目前没有其他浏览器提供此支持。

不支持导入映射

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

虽然 Chrome 和 Edge 89 版及更高版本支持导入映射,但目前无法与服务工作器搭配使用

浏览器支持

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

Safari 在技术预览版 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);
    // ...
  })());
});

向后兼容性

如果所有浏览器都支持服务工作器中的 ES 模块,上述示例将正常运行,但在撰写本文时,情况并非如此。

为了适应不提供内置支持的浏览器,您可以通过与 ES 模块兼容的捆绑工具运行服务工件脚本,以创建包含所有模块代码的内嵌服务工件,并在旧版浏览器中运行。或者,如果您尝试导入的模块已以 IIFEUMD 格式捆绑提供,您可以使用 importScripts() 导入它们。

当您有两个版本的服务工件可用(一个使用 ES 模块,另一个不使用)后,您需要检测当前浏览器支持的版本,并注册相应的服务工件脚本。检测支持的最佳实践目前尚未确定,但您可以关注此 GitHub 问题中的讨论,获取相关建议。

_照片由 Vlado Paunovic 拍摄,选自 Unsplash 网站_