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(require.context('../components/', true, /* webpackInclude: /\.js$/ */));

以上代码将仅包含 ../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('PrintSizePlugin', (compilation, callback) => {
    9      for (let filename in compilation.assets) {
    10        let source = compilation.assets[filename].source();
    11        console.log(`The size of ${filename} is ${source.length} bytes.`);
    12      }
    13      callback();
    14    });
    15  }
    16}
    17
    18module.exports = PrintSizePlugin;

    在 webpack 配置中使用这个 plugin:

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

这两个例子展示了如何通过编写 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框架中。