现代 Web 平台特性
现代 Web 平台特性
本指南介绍使用 Web Components、Import Maps 和配合 Service Workers 的 渐进式 Web 应用(PWA)的实用 webpack 模式。每个部分都说明了问题所在,提供了可直接复制的最小配置,并指出了当前相对于未来 webpack 改进的局限性。
T> 熟悉代码分离、缓存([contenthash])和 SplitChunksPlugin 会有所帮助。
在 webpack 中使用 Web Components
问题
如果多个 JavaScript 包对同一个标签名执行 customElements.define(),浏览器会抛出 DOMException:Failed to execute 'define' on 'CustomElementRegistry'。当注册元素的模块被重复引入时经常发生这种情况:独立的入口点或异步块各自包含了一份注册代码的副本,导致两个包都针对同一标签执行了 define。
解决方案
使用 optimization.splitChunks,让定义元素的模块位于一个共享块中并只加载一次。调整 cacheGroups,将你的元素定义(或专用文件夹,如 src/elements/)强制归入同一个块。有关一般思路,请参阅防止重复。
webpack.config.js
js
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: {
main: "./src/main.js",
admin: "./src/admin.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
// 将共享的自定义元素模块放入一个异步块中
customElements: {
test: /[\\/]src[\\/]elements[\\/]/,
name: "custom-elements",
chunks: "all",
enforce: true,
},
},
},
},
};
确保两个入口都导入同一个注册模块(例如 ./elements/my-element.js),这样 webpack 才能生成一个 custom-elements.js 块,而不是在 main 和 admin 中分别内联重复的注册代码。
局限性与未来展望
仅靠代码分离并不会改变浏览器的规则:每个文档中的标签名仍然必须恰好注册一次。除了块图控制之外,webpack 目前尚未提供开箱即用的“仅注册一次此自定义元素”的原生机制。对自定义元素注册的跨构建去重原生支持已在规划中;在此之前,请依赖共享块和单一注册模块。
在 webpack 中使用 Import Maps
问题
Import maps 让浏览器解析裸说明符(例如从 importmap.json 或内联的 <script type="importmap"> 中解析 import "lodash-es")。如果 webpack 打包了这些依赖,则无需为它们使用 import map。如果你希望浏览器从某个 URL(CDN 或 /vendor/)加载依赖,而应用代码继续使用裸导入,则将这些模块标记为 externals,这样 webpack 就会生成与你的 import map 匹配的 import 语句。
解决方案
启用 ES 模块输出(experiments.outputModule 和 output.module),为静态导入设置 externalsType: "module",并在 externals 中列出每个裸说明符,其字符串需与浏览器通过 import map 解析的内容一致。
webpack.config.js
js
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
mode: "production",
experiments: {
outputModule: true,
},
entry: "./src/index.js",
externalsType: "module",
externals: {
"lodash-es": "lodash-es",
},
output: {
module: true,
filename: "[name].mjs",
path: path.resolve(__dirname, "dist"),
clean: true,
},
};
importmap.json(与 HTML 一起部署;URL 必须与你的部署环境匹配)
本地供应商文件:
json
{
"imports": {
"lodash-es": "/vendor/lodash-es.js"
}
}
CDN(无需自行托管):
json
{
"imports": {
"lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4/+esm"
}
}
键 "lodash-es" 必须同时匹配**externals 中的键和源码中的说明符**(import … from "lodash-es")。值则是浏览器加载的 URL —— 可以是本地路径或 CDN URL;webpack 不会验证该文件。
index.html(顺序很重要:import map 必须在 bundle 之前)
html
<script type="importmap" src="/importmap.json"></script>
<script type="module" src="/dist/main.mjs"></script>
W> experiments.outputModule 和 output.module 仍处于实验阶段。在生产环境中依赖它们之前,请查看最新的 webpack 发布说明。
局限性与未来展望
webpack 不会为你生成或更新 importmap.json。你必须手动维护该映射,以确保说明符和 URL 与 externals 及服务器目录结构保持一致。webpack 5 目前不支持自动生成 import map;未来的工具可能会减少这一手动步骤。
渐进式 Web 应用(PWA)与 Service Workers
问题
长期缓存需要 HTML 稳定的 URL,但脚本和样式需要带版本的 URL。在 output.filename 中使用 [contenthash] 会使这些 URL 在每次构建时发生变化。Service Worker 的预缓存列表必须在每次构建后列出准确的 URL,否则离线外壳将指向不存在的文件。
workbox-webpack-plugin 的 GenerateSW 插件可以为你生成完整的 service worker,这很方便。但当需要对 service worker 代码进行完全控制时(自定义路由、skipWaiting 行为,或与 [contenthash] 及其他插件的协调),InjectManifest 更为合适:你编写 worker 代码,Workbox 在构建时根据 webpack 的资源列表注入预缓存清单。
解决方案
为产出的资源使用 [contenthash],并添加来自 workbox-webpack-plugin 的 InjectManifest。你的源码模板导入 workbox-precaching 并调用 precacheAndRoute(self.__WB_MANIFEST);插件会将 self.__WB_MANIFEST 替换为 webpack 资源列表(包含带哈希的文件名)。
安装:
bash
npm install workbox-webpack-plugin workbox-precaching --save-dev
webpack.config.js
js
import path from "node:path";
import { fileURLToPath } from "node:url";
import HtmlWebpackPlugin from "html-webpack-plugin";
import { InjectManifest } from "workbox-webpack-plugin";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: "./src/index.js",
output: {
filename: "[name].[contenthash].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({ title: "PWA + content hashes" }),
new InjectManifest({
swSrc: path.resolve(__dirname, "src/service-worker.js"),
swDest: "service-worker.js",
}),
],
};
src/service-worker.js(预缓存模板)
js
import { precacheAndRoute } from "workbox-precaching";
// 构建时会被替换为 webpack 的预缓存清单(带哈希的资源 URL)。
precacheAndRoute(globalThis.__WB_MANIFEST);
在你的应用中(例如在 src/index.js 中)通过 navigator.serviceWorker.register("/service-worker.js") 注册生成的 service-worker.js,并从 dist/ 目录以正确的作用域提供该文件。
局限性与未来展望
你必须保持 InjectManifest 与输出文件名及插件同步;当你不需要自定义 worker 时,GenerateSW 仍然是更简单的路径。webpack 没有内置的 service worker 预缓存生成器;与哈希资源更紧密的集成可能会在未来的版本中实现。在此之前,Workbox 的 InjectManifest 是将 [contenthash] 输出与预缓存对齐的一种完善方案。
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
