Guide to Webpack 5

webpack 使用指南。pnpm 安装的包没有类型提示,使用三斜线指令可以解决。

webpack-dev-server

clean-webpack-plugin

Deprecate plugin in favor output.clean

From webpack v5, you can remove the clean-webpack-plugin plugin and use the output.clean option in your webpack config:

js
1 output: {
2    filename: 'utils.min.js',
3    clean: true,
4 }

pnpm 没有 webpack 的类型声明

随便搞个 d.ts 然后加个 三斜线指令,就会有提示了。

ts
1/// <reference path="/node_modules/webpack/types.d.ts"/>

Template strings

https://webpack.js.org/configuration/output/#template-strings

在 Compilation-level 可用的替换项:

  • [fullhash]:此次编译过程中,所有模块内容生成的哈希值的总和。如果项目中的任何部分发生改变,这个值就会改变。
  • [hash]:与 [fullhash] 相同,但已被弃用。

在 Chunk-level 可用的替换项:

  • [id]:块(chunk)的唯一标识,通常是一个数字或字符串。
  • [name]:块的名称。在你的 webpack 配置中,你可以为每个入口起一个名字,如果设置了,[name] 就是这个名字。如果没有设置,那就是块的 id
  • [chunkhash]:基于 chunk 内容的哈希值,如果块中包含的模块有任何更改,该值将更改。
  • [contenthash]:对于特定类型的块内容(例如,只包含 CSS 的块)计算的哈希值。只有当这个类型的内容改变时,这个值才会改变。

在 Module-level 可用的替换项:

  • [id]:模块的唯一标识,通常是一个数字或字符串。
  • [moduleid]:与 [id] 相同,但已被弃用。
  • [hash]:基于模块内容的哈希值。
  • [modulehash]:与 [hash] 相同,但已被弃用。
  • [contenthash]:模块内容的哈希值。

在 File-level 可用的替换项:

  • [file]:文件名和路径,不包括查询参数或片段标识符。
  • [query]:查询参数,以 '?' 开头。
  • [fragment]:片段标识符,以 '#' 开头。
  • [base]:只包括文件名和扩展名,不包括路径。
  • [filebase]:与 [base] 相同,但已被弃用。
  • [path]:文件的路径,不包括文件名。
  • [name]:只有文件名,不包括扩展名和路径。
  • [ext]:文件扩展名,以 '.' 开头(在 output.filename 中不可用)。

在 URL-level 可用的替换项:

  • [url]:URL,包括协议、主机、端口、路径、查询参数、片段标识符。

以上这些替换项使我们能够根据不同的需求灵活定义文件名,如考虑缓存策略,做到只有当文件内容改变时,浏览器才会下载新的文件。

特殊的注释语法

在 Webpack 中,webpackChunkName 是一个特殊的注释语法,用于控制代码分割(code splitting)生成的 chunk 文件的名称。这个名字通常用于动态 import() 语句。例如:

javascript
1import(/* webpackChunkName: "my-chunk-name" */ "./my-module");

在这个例子中,my-chunk-name 就是 chunk 文件的名称。Webpack 在生成 chunk 文件时会使用这个名称,生成的文件名将类似于 my-chunk-name.bundle.js

然而,webpackChunkName 不支持使用 hash。这是因为,webpackChunkName 的值在编译时需要是已知的静态字符串,而 hash 是在编译过程中生成的,依赖于模块的内容。因此,不能在 webpackChunkName 中使用 hash。

你可以使用 output.chunkFilename 配置项来控制 chunk 文件名的格式,包括 hash。例如:

javascript
1module.exports = {
2  //...
3  output: {
4    chunkFilename: "[name].[contenthash].js",
5  },
6};

在这个配置中,[name] 会被替换为 chunk 的名称(也就是 webpackChunkName 的值),[contenthash] 会被替换为 chunk 内容的 hash。所以,虽然不能直接在 webpackChunkName 中使用 hash,但你可以通过 output.chunkFilename 来控制 hash 在文件名中的使用。

在 webpack 中,通过魔法注释(magic comment)/* webpackChunkName: "name" */,你可以为动态导入的代码块 (chunk) 设置一个特定的名字。这通常用于按需加载或代码分割。

如果你为多个代码块设置了相同的 webpackChunkName,这些代码块会被打包到同一个文件中。这可以用作特性,比如当你想要多个异步代码块组合在一起时。

举个例子:

javascript
1// 如果你这样做:
2import(/* webpackChunkName: "group-a" */ "./moduleA");
3import(/* webpackChunkName: "group-a" */ "./moduleB");
4
5// 两者会被打包到同一个 chunk 文件中,如:group-a.js
6
7// 而如果这样做:
8import(/* webpackChunkName: "moduleA" */ "./moduleA");
9import(/* webpackChunkName: "moduleB" */ "./moduleB");
10
11// 则会生成两个不同的 chunk 文件,例如:moduleA.js 和 moduleB.js

所以,如果 webpackChunkName 重复,并不会导致错误,而是 webpack 故意这么设计的,以便你可以将多个模块组合到一个代码块中。

Magic Comments

Webpack 提供了一种称为"魔法注释"(Magic Comments)的机制,允许在源代码中插入特殊的注释,从而影响 webpack 的构建行为。下面列举了几种常用的魔法注释:

  1. webpackChunkName: 此注释用于指定分割出的块(chunk)的名称。例如:
javascript
1import(/* webpackChunkName: "my-chunk-name" */ "./module");

以上代码会导致生成一个名为 "my-chunk-name" 的块,包含导入的模块。

  1. webpackMode: 此注释允许你修改模块的加载模式。可用的模式有 lazy(默认值,只有在模块需要时才请求)、lazy-once(所有请求的模块都在一个网络请求中)、eager(无需网络请求,但需要 promise 解析)和 weak(试图使用同步导入,如果不可用则拒绝)。例如:
javascript
1import(/* webpackMode: "eager" */ "./module");
  1. webpackPrefetch / webpackPreload: 这两个注释允许你控制浏览器的预获取/预加载行为。webpackPrefetch 会在浏览器闲置时加载资源,webpackPreload 则会和主代码并行加载。例如:
javascript
1import(/* webpackPrefetch: true */ "LoginModal");

以上代码会在浏览器空闲时提前获取 "LoginModal" 模块。

  1. webpackInclude / webpackExclude: 这两个注释可以限制 webpack 处理 require.context() 时的模块路径。例如:
javascript
1function importAll(r) {
2  r.keys().forEach(r);
3}
4
5importAll(
6  require.context("../components/", true /* webpackInclude: /\.js$/ */),
7);

以上代码将仅包含 ../components/ 目录下所有 .js 文件。

  1. webpackExports: 这个注释可以用来在导入动态模块时,只导入某些输出,从而减少代码大小。例如:
javascript
1import(/* webpackExports: ["export1", "export2"] */ "./module");

这些魔法注释都提供了对 webpack 构建行为的更细粒度的控制,可以在需要的时候选择使用。

组合使用

Webpack 的魔法注释可以组合使用。这将给你更大的灵活性来调整模块的行为。例如,你可能想要对一个被动态导入的模块进行预取(prefetching),并同时为这个模块指定一个特定的 chunk 名称。这可以通过在同一个注释中使用两个魔法注释来完成,如下所示:

javascript
1import(
2  /* webpackChunkName: "my-chunk-name", webpackPrefetch: true */
3  "./module"
4);

在这个例子中,webpackChunkName 注释将为 chunk 指定名称 "my-chunk-name",webpackPrefetch 注释则会使得这个模块在浏览器空闲时被预取。

但需要注意的是,并非所有魔法注释都可以共同使用,例如 webpackIncludewebpackExclude 就不能同时使用。所以在使用时,需要结合实际需求和 webpack 文档来判断使用哪种组合。

filename 和 chunkFilename

在 Webpack 的配置中,filenamechunkFilename 是两个用于定义输出文件名称模板的重要选项。

  1. filename:这个选项用于指定入口文件的名称。在单入口场景中,filename 可以是一个静态的字符串,例如 'bundle.js'。在多入口场景中,filename 可以包含一些占位符(placeholders)来确保文件名称的唯一性,例如 'js/[name].[contenthash].js'
  2. chunkFilename:这个选项用于指定非入口的 chunk 文件的名称,比如通过动态导入(import())或者由于设置了 optimization.splitChunks 而产生的新的 chunk。同样,chunkFilename 也可以包含占位符。

两者的主要区别在于,filename 用于主入口(entry point)的文件,而 chunkFilename 用于额外的、按需加载的代码块。

例如,假设我们的配置如下:

javascript
1output: {
2  filename: 'bundle.js',
3  chunkFilename: '[id].js'
4}

如果我们的项目有一个入口文件 index.js,并且在这个文件中动态导入了 anotherModule.js,那么在构建时,webpack 将会产生两个文件:bundle.js(由 index.js 生成)和 0.js(由 anotherModule.js 生成,假设它的 chunk id 是 0)。

Loader and Plugin

  1. Loader

    假设我们要编写一个 loader,它的功能是将 JS 文件中的所有 console.log 语句删除。这就需要用到 abstract syntax tree (AST) 进行代码分析。

    javascript
    1// remove-console-loader.js
    2const { getOptions } = require("loader-utils");
    3const { parse, print, visit } = require("recast");
    4
    5module.exports = function (source) {
    6  const options = getOptions(this);
    7  const { removeMethods = ["console.log"] } = options;
    8
    9  const ast = parse(source);
    10  visit(ast, {
    11    visitCallExpression(path) {
    12      const node = path.node;
    13      if (
    14        node.callee.type === "MemberExpression" &&
    15        removeMethods.includes(print(node).code)
    16      ) {
    17        path.replace();
    18      }
    19      return false;
    20    },
    21  });
    22
    23  return print(ast).code;
    24};

    在 webpack 的配置中使用这个 loader:

    javascript
    1// webpack.config.js
    2module.exports = {
    3  module: {
    4    rules: [
    5      {
    6        test: /\.js$/,
    7        use: [
    8          {
    9            loader: path.resolve("remove-console-loader.js"),
    10          },
    11        ],
    12      },
    13    ],
    14  },
    15};
  2. Plugin

    假设我们要编写一个 plugin,其功能是将每次构建后的输出文件大小打印出来。

    javascript
    1// print-size-plugin.js
    2const { RawSource } = require("webpack-sources");
    3const fs = require("fs");
    4const path = require("path");
    5
    6class PrintSizePlugin {
    7  apply(compiler) {
    8    compiler.hooks.emit.tapAsync(
    9      "PrintSizePlugin",
    10      (compilation, callback) => {
    11        for (let filename in compilation.assets) {
    12          let source = compilation.assets[filename].source();
    13          console.log(`The size of ${filename} is ${source.length} bytes.`);
    14        }
    15        callback();
    16      },
    17    );
    18  }
    19}
    20
    21module.exports = PrintSizePlugin;

    在 webpack 配置中使用这个 plugin:

    javascript
    1// webpack.config.js
    2const PrintSizePlugin = require("./print-size-plugin");
    3
    4module.exports = {
    5  plugins: [new PrintSizePlugin()],
    6};

这两个例子展示了如何通过编写 loader 和 plugin 来实现更复杂的功能。这些例子虽然更复杂,但仍然只是 webpack 提供的可扩展性的冰山一角。根据你的需求,你可以编写更复杂的 loader 和 plugin 来定制你的构建过程。

执行顺序

在 Webpack 中,loader 和 plugin 的应用顺序是有区别的。它们的执行顺序取决于它们在配置中的位置和 webpack 的执行流程。

Loader 的应用顺序:

  1. 对于一个模块(通常是一个文件),Webpack 在解析模块时,根据模块的文件类型(匹配 test 条件)选择相应的 loader 进行转换。
  2. 如果使用了多个 loader,Webpack 会根据 use 数组中的顺序,依次对模块进行转换。每个 loader 都会接收前一个 loader 处理后的结果。
  3. 最后一个 loader 将返回转换后的 JavaScript 代码,供 Webpack 继续处理。

例如:

javascript
1module: {
2  rules: [
3    {
4      test: /\.css$/,
5      use: ['style-loader', 'css-loader'],
6    },
7  ],
8},

在上面的配置中,首先 css-loader 将解析 CSS 文件,然后将结果传递给 style-loaderstyle-loader 会将解析后的 CSS 样式添加到页面上。

Plugin 的应用顺序:

  1. Plugin 的执行顺序是在 loader 执行之后。
  2. 当所有模块的 loader 转换完成后,Webpack 开始执行插件。
  3. 插件通过 Webpack 的事件钩子机制来注入自定义行为,例如在文件输出、优化代码等阶段执行特定的任务。

例如:

javascript
1plugins: [
2  new HtmlWebpackPlugin(),
3  new CleanWebpackPlugin(),
4],

在上面的配置中,首先在所有模块转换完成后,Webpack 会执行 HtmlWebpackPluginCleanWebpackPlugin 的插件功能。

总结:Loader 主要用于模块文件的转换,根据文件类型按顺序执行,而 Plugin 则主要用于在整个构建过程的不同阶段执行特定的任务。因此,Loader 的执行顺序在每个模块的加载和转换过程中,而 Plugin 的执行顺序在整个构建过程的不同阶段。

在浏览器环境下使用 Webpack 或 Next.js,如何处理依赖于 Node.js 的 fs 模块的库?

在构建面向浏览器的 Web 项目时,开发者可能会遇到依赖于 Node.js 的 fs 模块的库。由于浏览器不支持 fs 模块,因此可能出现兼容性问题。以下是针对 Webpack 和 Next.js 的解决方案:

对于 Webpack 的解决方案:

  1. 设置 node 选项: 通过 Webpack 的配置文件,将 fs 设置为 "empty" 或者 false,从而忽略或替换fs 模块的引用。

    具体代码示例:

    javascript
    1// webpack.config.js
    2module.exports = {
    3  // 其他配置项
    4  node: {
    5    fs: "empty",
    6  },
    7};
  2. 使用别名: 你可以使用别名将 fs 映射到一个自定义的文件。

    具体代码示例:

    javascript
    1// webpack.config.js
    2module.exports = {
    3  // 其他配置项
    4  resolve: {
    5    alias: {
    6      fs: path.resolve(__dirname, "path/to/customFs.js"),
    7    },
    8  },
    9};

对于 Next.js 的解决方案:

  1. 自定义 Webpack 配置: 在 Next.js 的 next.config.js 文件中,你可以自定义 Webpack 配置来处理 fs 模块的引用。

    具体代码示例:

    javascript
    1// next.config.js
    2module.exports = {
    3  webpack: (config, { isServer }) => {
    4    if (!isServer) {
    5      config.resolve.fallback.fs = false; // 在客户端构建中替换 fs
    6    }
    7
    8    return config;
    9  },
    10};

通过这些方案,开发者可以解决在 Webpack 或 Next.js 项目中引入了 fs 模块的库的兼容性问题。这有助于确保项目的正确构建和运行,无论是在纯 Webpack 环境还是在 Next.js 框架中。

source-assets

https://webpack.js.org/guides/asset-modules/#source-assets

Asset Modules allow one to use asset files (fonts, icons, etc) without configuring additional loaders.

Prior to webpack 5 it was common to use:

Asset Modules types replace all of these loaders by adding 4 new module types:

  • asset/resource emits a separate file and exports the URL. Previously achievable by using file-loader.
  • asset/inline exports a data URI of the asset. Previously achievable by using url-loader.
  • asset/source exports the source code of the asset. Previously achievable by using raw-loader.
  • asset automatically chooses between exporting a data URI and emitting a separate file. Previously achievable by using url-loader with asset size limit.

When using the old assets loaders (i.e. file-loader/url-loader/raw-loader) along with Asset Modules in webpack 5, you might want to stop Asset Modules from processing your assets again as that would result in asset duplication. This can be done by setting the asset's module type to 'javascript/auto'.