知海

现代 Web 平台特性

webpackjsorg-main指南-教程

现代 Web 平台特性

本指南介绍使用 Web ComponentsImport Maps 和配合 Service Workers渐进式 Web 应用(PWA)的实用 webpack 模式。每个部分都说明了问题所在,提供了可直接复制的最小配置,并指出了当前相对于未来 webpack 改进的局限性。

T> 熟悉代码分离缓存[contenthash])和 SplitChunksPlugin 会有所帮助。

在 webpack 中使用 Web Components

问题

如果多个 JavaScript 包对同一个标签名执行 customElements.define(),浏览器会抛出 DOMExceptionFailed 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 块,而不是在 mainadmin 中分别内联重复的注册代码。

局限性与未来展望

仅靠代码分离并不会改变浏览器的规则:每个文档中的标签名仍然必须恰好注册一次。除了块图控制之外,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.outputModuleoutput.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.outputModuleoutput.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-pluginGenerateSW 插件可以为你生成完整的 service worker,这很方便。但当需要对 service worker 代码进行完全控制时(自定义路由、skipWaiting 行为,或与 [contenthash] 及其他插件的协调),InjectManifest 更为合适:你编写 worker 代码,Workbox 在构建时根据 webpack 的资源列表注入预缓存清单。

解决方案

为产出的资源使用 [contenthash],并添加来自 workbox-webpack-pluginInjectManifest。你的源码模板导入 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] 输出与预缓存对齐的一种完善方案。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。