可打印

概念

其核心是,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包器。当 webpack 处理你的应用程序时,它会在内部从一个或多个入口点构建一个依赖图,然后将项目所需的每个模块组合成一个或多个 bundles,这些 bundles 是用于提供内容的静态资产。

自 4.0.0 版本以来,webpack 不再需要配置文件来打包你的项目。然而,它具有极高的可配置性,以更好地满足你的需求。

要开始使用,你只需要了解其核心概念

本文档旨在对这些概念进行高层次概述,同时提供指向详细概念特定用例的链接。

为了更好地理解模块打包器背后的思想及其内部工作原理,请查阅这些资源

入口

入口点指示 webpack 应该使用哪个模块来开始构建其内部依赖图。Webpack 将找出该入口点直接或间接依赖于哪些其他模块和库。

默认值为./src/index.js,但你可以通过在 webpack 配置中设置entry属性来指定不同的(或多个)入口点。例如

webpack.config.js

module.exports = {
  entry: './path/to/my/entry/file.js',
};

输出

output 属性告诉 webpack 将它创建的 bundles 发射到哪里,以及如何命名这些文件。对于主输出文件,默认是 ./dist/main.js,对于任何其他生成的文件,默认是 ./dist 文件夹。

你可以通过在配置中指定一个 output 字段来配置这部分过程

webpack.config.js

const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
};

在上面的例子中,我们使用 output.filenameoutput.path 属性来告诉 webpack 我们的 bundle 的名称以及我们希望它被发射到的位置。如果你想知道顶部导入的 path 模块,它是一个核心 Node.js 模块,用于操作文件路径。

加载器

开箱即用,webpack 只理解 JavaScript 和 JSON 文件。加载器(Loaders) 允许 webpack 处理其他类型的文件,并将它们转换为有效的模块,这些模块可以被你的应用程序使用并添加到依赖图中。

在高层次上,加载器在你的 webpack 配置中有两个属性

  1. test 属性标识应该转换的文件。
  2. use 属性指示应该使用哪个加载器进行转换。

webpack.config.js

const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
};

上述配置为单个模块定义了一个 rules 属性,其中包含两个必需属性:testuse。这告诉 webpack 的编译器如下

“嘿,webpack 编译器,当你遇到一个在 require()/import 语句中解析为 '.txt' 文件的路径时,在将其添加到 bundle 之前,使用 raw-loader 对其进行转换。”

你可以在加载器部分查看包含加载器时的进一步自定义。

插件

虽然加载器用于转换某些类型的模块,但插件可用于执行更广泛的任务,例如 bundle 优化、资产管理和环境变量注入。

要使用插件,你需要 require() 它并将其添加到 plugins 数组中。大多数插件可以通过选项进行自定义。由于你可以在配置中多次使用同一个插件以实现不同目的,因此你需要使用 new 操作符调用它来创建一个实例。

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};

在上面的例子中,html-webpack-plugin 为你的应用程序生成一个 HTML 文件,并自动将所有生成的 bundle 注入到此文件中。

在你的 webpack 配置中使用插件很简单。然而,有许多值得进一步探索的用例。在此了解更多

模式

通过将 mode 参数设置为 developmentproductionnone,你可以启用 webpack 内置的与每个环境对应的优化。默认值为 production

module.exports = {
  mode: 'production',
};

在此了解有关模式配置以及每个值下发生的优化的更多信息。

浏览器兼容性

Webpack 支持所有兼容 ES5 的浏览器(不支持 IE8 及以下版本)。Webpack 需要 Promise 来实现 import()require.ensure()。如果你想支持旧版浏览器,则需要在使用这些表达式之前加载一个 polyfill

环境

Webpack 5 运行在 Node.js 10.13.0+ 版本上。

入口点

入门中所述,有多种方法可以在 webpack 配置中定义 entry 属性。我们将向你展示配置 entry 属性的方式,并解释它对你为什么有用。

单入口(简写)语法

用法: entry: string | [string]

webpack.config.js

module.exports = {
  entry: './path/to/my/entry/file.js',
};

entry 属性的单入口简写语法是以下内容的简写

webpack.config.js

module.exports = {
  entry: {
    main: './path/to/my/entry/file.js',
  },
};

我们还可以将文件路径数组传递给 entry 属性,这会创建一个所谓的“多主入口”。当你希望将多个依赖文件一起注入并将其依赖关系图形化到一个“块”中时,这很有用。

webpack.config.js

module.exports = {
  entry: ['./src/file_1.js', './src/file_2.js'],
  output: {
    filename: 'bundle.js',
  },
};

当您希望为具有单个入口点(即库)的应用程序或工具快速设置 webpack 配置时,单入口语法是一个不错的选择。但是,使用此语法扩展或扩展配置的灵活性不大。

对象语法

用法: entry: { <entryChunkName> string | [string] } | {}

webpack.config.js

module.exports = {
  entry: {
    app: './src/app.js',
    adminApp: './src/adminApp.js',
  },
};

对象语法更冗长。但是,这是在应用程序中定义入口/入口点最可伸缩的方式。

EntryDescription 对象

入口点描述对象。你可以指定以下属性。

  • dependOn:当前入口点所依赖的入口点。它们必须在此入口点加载之前加载。

  • filename:指定磁盘上每个输出文件的名称。

  • import:启动时加载的模块。

  • library:指定库选项以从当前入口打包库。

  • runtime:运行时块的名称。设置后将创建一个新的运行时块。自 webpack 5.43.0 起,可以将其设置为 false 以避免创建新的运行时块。

  • publicPath:指定此入口的输出文件在浏览器中引用时的公共 URL 地址。另请参阅 output.publicPath

webpack.config.js

module.exports = {
  entry: {
    a2: 'dependingfile.js',
    b2: {
      dependOn: 'a2',
      import: './src/app.js',
    },
  },
};

runtimedependOn 不应在单个入口点上同时使用,因此以下配置无效并将抛出错误

webpack.config.js

module.exports = {
  entry: {
    a2: './a',
    b2: {
      runtime: 'x2',
      dependOn: 'a2',
      import: './b',
    },
  },
};

确保 runtime 不能指向现有入口点名称,例如,以下配置会抛出错误

module.exports = {
  entry: {
    a1: './a',
    b1: {
      runtime: 'a1',
      import: './b',
    },
  },
};

此外,dependOn 不能是循环的,以下示例也会抛出错误

module.exports = {
  entry: {
    a3: {
      import: './a',
      dependOn: 'b3',
    },
    b3: {
      import: './b',
      dependOn: 'a3',
    },
  },
};

场景

以下是入口配置及其实际用例的列表

分离应用程序和供应商入口

webpack.config.js

module.exports = {
  entry: {
    main: './src/app.js',
    vendor: './src/vendor.js',
  },
};

webpack.prod.js

module.exports = {
  output: {
    filename: '[name].[contenthash].bundle.js',
  },
};

webpack.dev.js

module.exports = {
  output: {
    filename: '[name].bundle.js',
  },
};

这有什么作用? 我们告诉 webpack 我们需要两个独立的入口点(如上例所示)。

为什么? 这样,你可以在 vendor.js 中导入未修改的所需库或文件(例如 Bootstrap、jQuery、图像等),它们将被打包成自己的块。内容哈希保持不变,这允许浏览器单独缓存它们,从而减少加载时间。

多页应用程序

webpack.config.js

module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js',
  },
};

这有什么作用? 我们告诉 webpack 我们需要三个独立的依赖图(如上例所示)。

为什么? 在多页应用程序中,服务器将为你获取一个新的 HTML 文档。页面会重新加载此新文档,并且资产会被重新下载。然而,这给了我们一个独特的机会来做一些事情,比如使用optimization.splitChunks在每个页面之间创建共享应用程序代码的 bundle。在入口点之间重用大量代码/模块的多页应用程序可以从这些技术中受益匪浅,因为入口点的数量会增加。

输出

配置 output 配置选项告诉 webpack 如何将编译后的文件写入磁盘。请注意,虽然可以有多个 entry 点,但只指定一个 output 配置。

用法

webpack 配置中 output 属性的最低要求是将其值设置为一个对象,并提供一个output.filename用于输出文件

webpack.config.js

module.exports = {
  output: {
    filename: 'bundle.js',
  },
};

此配置将把单个 bundle.js 文件输出到 dist 目录。

多个入口点

如果您的配置创建了多个“块”(例如有多个入口点或使用 CommonsChunkPlugin 等插件),则应使用替换以确保每个文件都具有唯一的名称。

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js',
  },
  output: {
    filename: '[name].js',
    path: __dirname + '/dist',
  },
};

// writes to disk: ./dist/app.js, ./dist/search.js

高级

这是一个使用 CDN 和资产哈希的更复杂的示例

config.js

module.exports = {
  //...
  output: {
    path: '/home/proj/cdn/assets/[fullhash]',
    publicPath: 'https://cdn.example.com/assets/[fullhash]/',
  },
};

在编译时无法确定输出文件的最终 publicPath 的情况下,可以将其留空,并在运行时通过入口点文件中的 __webpack_public_path__ 变量动态设置

__webpack_public_path__ = myRuntimePublicPath;

// rest of your application entry

加载器

加载器是应用于模块源代码的转换。它们允许你在 import 或“加载”文件时预处理它们。因此,加载器有点像其他构建工具中的“任务”,并提供了一种处理前端构建步骤的强大方法。加载器可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或者将内联图像作为数据 URL 加载。加载器甚至允许你直接从 JavaScript 模块中 import CSS 文件!

示例

例如,你可以使用加载器告诉 webpack 加载 CSS 文件或将 TypeScript 转换为 JavaScript。为此,你首先需要安装所需的加载器

npm install --save-dev css-loader ts-loader

然后指示 webpack 对每个 .css 文件使用 css-loader,对所有 .ts 文件使用 ts-loader

webpack.config.js

module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' },
      { test: /\.ts$/, use: 'ts-loader' },
    ],
  },
};

使用加载器

在你的应用程序中有两种使用加载器的方式

  • 配置(推荐):在你的 webpack.config.js 文件中指定它们。
  • 内联:在每个 import 语句中明确指定它们。

请注意,加载器可以在 webpack v4 下通过 CLI 使用,但该功能在 webpack v5 中已弃用。

配置

module.rules 允许你在 webpack 配置中指定多个加载器。这是一种简洁地显示加载器的方式,有助于保持代码整洁。它还为你提供了每个相应加载器的完整概述。

加载器从右到左(或从下到上)进行评估/执行。在下面的示例中,执行从 sass-loader 开始,继续到 css-loader,最后以 style-loader 结束。有关加载器顺序的更多信息,请参阅“加载器特性”

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {
              modules: true,
            },
          },
          { loader: 'sass-loader' },
        ],
      },
    ],
  },
};

内联

可以在 import 语句或任何等效的“导入”方法中指定加载器。使用 ! 将加载器与资源分开。每个部分都相对于当前目录进行解析。

import Styles from 'style-loader!css-loader?modules!./styles.css';

可以通过在内联 import 语句前添加前缀来覆盖配置中的任何加载器、preLoaders 和 postLoaders

  • 前缀 ! 将禁用所有已配置的普通加载器

    import Styles from '!style-loader!css-loader?modules!./styles.css';
  • 前缀 !! 将禁用所有已配置的加载器(preLoaders、loaders、postLoaders)

    import Styles from '!!style-loader!css-loader?modules!./styles.css';
  • 前缀 -! 将禁用所有已配置的 preLoaders 和 loaders,但不会禁用 postLoaders

    import Styles from '-!style-loader!css-loader?modules!./styles.css';

选项可以通过查询参数(例如 ?key=value&foo=bar)或 JSON 对象(例如 ?{"key":"value","foo":"bar"})传递。

加载器特性

  • 加载器可以链式调用。链中的每个加载器都会对处理后的资源应用转换。链式调用按相反顺序执行。第一个加载器将其结果(已应用转换的资源)传递给下一个加载器,以此类推。最后,webpack 期望链中的最后一个加载器返回 JavaScript。
  • 加载器可以是同步的或异步的。
  • 加载器在 Node.js 中运行,可以做那里所有可能的事情。
  • 加载器可以通过 options 对象进行配置(使用 query 参数设置选项仍然受支持但已弃用)。
  • 普通模块除了普通的 main 外,还可以通过 package.json 中的 loader 字段导出加载器。
  • 插件可以为加载器提供更多功能。
  • 加载器可以发射额外的任意文件。

加载器通过其预处理函数提供了一种自定义输出的方式。用户现在可以更灵活地包含细粒度逻辑,例如压缩、打包、语言翻译以及更多

解析加载器

加载器遵循标准的模块解析。在大多数情况下,它将从模块路径(例如 npm install, node_modules)加载。

一个加载器模块应该导出一个函数,并用与 Node.js 兼容的 JavaScript 编写。它们最常通过 npm 管理,但你也可以在应用程序中将自定义加载器作为文件。按照惯例,加载器通常命名为 xxx-loader(例如 json-loader)。有关更多信息,请参阅“编写加载器”

插件

插件是 webpack 的骨干。webpack 本身就是建立在与你在 webpack 配置中使用的相同的插件系统之上的!

它们还用于执行加载器无法执行的任何其他任务。webpack 开箱即用提供了许多此类插件

结构

一个 webpack 插件是一个 JavaScript 对象,它有一个 apply 方法。这个 apply 方法由 webpack 编译器调用,从而可以访问整个编译生命周期。

ConsoleLogOnBuildWebpackPlugin.js

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('The webpack build process is starting!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

建议编译器钩子的 tap 方法的第一个参数应为插件名称的驼峰式版本。建议为此使用常量,以便可以在所有钩子中重复使用。

用法

由于插件可以接受参数/选项,你必须将一个 new 实例传递给 webpack 配置中的 plugins 属性。

根据你使用 webpack 的方式,有多种方法可以使用插件。

配置

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); //to access built-in plugins
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    filename: 'my-first-webpack.bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: 'babel-loader',
      },
    ],
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};

ProgressPlugin 用于自定义编译期间的进度报告方式,而 HtmlWebpackPlugin 将为你的应用程序生成一个 HTML 文件,并使用 script 标签自动注入所有生成的 bundle。

Node API

使用 Node API 时,你也可以通过配置中的 plugins 属性传递插件。

some-node-script.js

const webpack = require('webpack'); //to access webpack runtime
const configuration = require('./webpack.config.js');

let compiler = webpack(configuration);

new webpack.ProgressPlugin().apply(compiler);

compiler.run(function (err, stats) {
  // ...
});

配置

你可能已经注意到,很少有 webpack 配置看起来完全相同。这是因为 webpack 的配置文件是一个导出 webpack 配置的 JavaScript 文件。然后 webpack 根据其定义的属性处理此配置。

因为它是一个标准的 Node.js CommonJS 模块,你可以做以下事情

  • 通过 require(...) 导入其他文件
  • 通过 require(...) 使用 npm 上的实用程序
  • 使用 JavaScript 控制流表达式,例如 ?: 运算符
  • 对常用值使用常量或变量
  • 编写并执行函数以生成部分配置

酌情使用这些功能。

虽然技术上可行,但应避免以下做法

  • 使用 webpack CLI 时访问 CLI 参数(而是编写自己的 CLI,或使用 --env
  • 导出非确定性值(两次调用 webpack 应生成相同的输出文件)
  • 编写过长的配置(而是将配置拆分为多个文件)

下面的示例描述了 webpack 的配置如何既具有表现力又可配置,因为它是代码

入门配置

webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './foo.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'foo.bundle.js',
  },
};

参见配置部分以了解所有支持的配置选项

多目标

除了将单个配置作为对象、函数Promise导出外,还可以导出多个配置。

参见导出多个配置

使用其他配置语言

Webpack 接受用多种编程和数据语言编写的配置文件。

参见配置语言

模块

模块化编程中,开发人员将程序分解为称为模块的离散功能块。

每个模块的表面积都小于一个完整的程序,使得验证、调试和测试变得微不足道。编写良好的模块提供了可靠的抽象和封装边界,因此每个模块在整个应用程序中都具有连贯的设计和明确的目的。

Node.js 几乎从一开始就支持模块化编程。然而,在 Web 上,对模块的支持来得比较慢。存在多种工具支持 Web 上的模块化 JavaScript,具有各种优点和限制。Webpack 建立在从这些系统中汲取的经验教训之上,并将模块的概念应用于项目中的任何文件。

什么是 webpack 模块

Node.js 模块不同,webpack 模块可以以多种方式表达其依赖项。以下是一些示例

  • 一个 ES2015 import 语句
  • 一个 CommonJS require() 语句
  • 一个 AMD definerequire 语句
  • CSS/Sass/Less 文件中的 @import 语句
  • 样式表中的图像 URL url(...) 或 HTML <img src=...> 文件。

支持的模块类型

Webpack 原生支持以下模块类型

此外,webpack 通过加载器支持用各种语言和预处理器编写的模块。加载器向 webpack 描述了如何处理非原生模块,并将这些依赖项包含到你的 bundles 中。webpack 社区为各种流行的语言和语言处理器构建了加载器,包括

以及许多其他!总的来说,webpack 提供了一个强大而丰富的自定义 API,允许人们将 webpack 用于任何技术栈,同时在你的开发、测试和生产工作流程中保持不持任何意见

有关完整列表,请参阅加载器列表自己编写

模块解析

解析器是一个库,它通过模块的绝对路径来帮助定位模块。模块可以作为另一个模块的依赖项被引用,例如

import foo from 'path/to/module';
// or
require('path/to/module');

依赖模块可以来自应用程序代码或第三方库。解析器帮助 webpack 找到需要包含在 bundle 中的模块代码,以响应每个 require/import 语句。webpack 使用 enhanced-resolve 来解析打包模块时的文件路径。

webpack 中的解析规则

使用 enhanced-resolve,webpack 可以解析三种文件路径

绝对路径

import '/home/me/file';

import 'C:\\Users\\me\\file';

由于我们已经有了文件的绝对路径,因此不需要进一步解析。

相对路径

import '../src/file1';
import './file2';

在这种情况下,importrequire 发生的源文件目录被视为上下文目录。import/require 中指定的相对路径与此上下文路径连接,以生成模块的绝对路径。

模块路径

import 'module';
import 'module/lib/file';

模块会在 resolve.modules 中指定的所有目录中搜索。你可以通过使用 resolve.alias 配置选项为其创建别名,从而将原始模块路径替换为替代路径。

  • 如果包包含 package.json 文件,则会按顺序查找 resolve.exportsFields 配置选项中指定的字段,并且 package.json 中的第一个此类字段根据包导出指南确定包中可用的导出。

一旦根据上述规则解析了路径,解析器会检查路径是指向文件还是目录。如果路径指向文件

  • 如果路径带有文件扩展名,则文件会立即打包。
  • 否则,文件扩展名将使用 resolve.extensions 选项进行解析,该选项告诉解析器哪些扩展名是可接受的,例如 .js.jsx

如果路径指向文件夹,则采取以下步骤来查找具有正确扩展名的正确文件

  • 如果文件夹包含 package.json 文件,则会按顺序查找 resolve.mainFields 配置选项中指定的字段,并且 package.json 中的第一个此类字段决定文件路径。
  • 如果没有 package.json,或者 resolve.mainFields 没有返回有效路径,则会按顺序查找 resolve.mainFiles 配置选项中指定的文件名,以查看导入/所需目录中是否存在匹配的文件名。
  • 然后,文件扩展名以类似的方式使用 resolve.extensions 选项进行解析。

Webpack 根据您的构建目标为这些选项提供了合理的默认值

解析加载器

这遵循与文件解析相同的规则。但是 resolveLoader 配置选项可用于为加载器设置单独的解析规则。

缓存

每次文件系统访问都会被缓存,以便对同一个文件的多个并行或串行请求更快。在watch 模式下,只有修改过的文件才会从缓存中逐出。如果 watch 模式关闭,则缓存会在每次编译前清除。

请参阅解析 API 以了解有关上述配置选项的更多信息。

模块联邦

动机

多个独立的构建应该形成一个单一的应用程序。这些独立的构建充当容器,并且可以在它们之间公开和消费代码,从而创建一个单一的、统一的应用程序。

这通常被称为微前端,但并不仅限于此。

低层概念

我们区分本地模块和远程模块。本地模块是当前构建的一部分的常规模块。远程模块是不属于当前构建但在运行时从远程容器加载的模块。

加载远程模块被视为异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口点之间的下一个块加载操作中。没有块加载操作就无法使用远程模块。

块加载操作通常是 import() 调用,但也支持较旧的构造,如 require.ensurerequire([...])

容器是通过容器入口创建的,它暴露了对特定模块的异步访问。暴露的访问分为两个步骤

  1. 加载模块(异步)
  2. 评估模块(同步)。

步骤 1 将在块加载期间完成。步骤 2 将在模块评估期间完成,并与其他(本地和远程)模块交错进行。这样,评估顺序不受模块从本地转换为远程或反向转换的影响。

容器可以嵌套。容器可以使用来自其他容器的模块。容器之间也可能存在循环依赖。

高层概念

每个构建都充当一个容器,也消费其他构建作为容器。这样,每个构建都能够通过从其容器加载来访问任何其他暴露的模块。

共享模块是既可被覆盖又作为覆盖提供给嵌套容器的模块。它们通常指向每个构建中的同一个模块,例如,同一个库。

packageName 选项允许设置一个包名来查找 requiredVersion。默认情况下,它会自动从模块请求中推断出来,当需要禁用自动推断时,将 requiredVersion 设置为 false

构建块

ContainerPlugin(低层)

该插件会创建一个额外的容器入口,并暴露指定的模块。

ContainerReferencePlugin(低层)

此插件将特定容器的引用作为外部模块添加,并允许从这些容器导入远程模块。它还会调用这些容器的 override API 来向它们提供覆盖。本地覆盖(通过 __webpack_override__ 或当构建也是容器时的 override API)和指定的覆盖都提供给所有引用的容器。

ModuleFederationPlugin(高层)

ModuleFederationPlugin 结合了 ContainerPluginContainerReferencePlugin

概念目标

  • 应该能够暴露和消费 webpack 支持的任何模块类型。
  • 分块加载应并行加载所有需要的内容(web:单次往返服务器)。
  • 从消费者到容器的控制
    • 覆盖模块是单向操作。
    • 兄弟容器不能互相覆盖模块。
  • 概念应与环境无关。
    • 可用于 web、Node.js 等。
  • 共享中的相对和绝对请求
    • 即使未使用,也将始终提供。
    • 将相对于 config.context 解析。
    • 默认情况下不使用 requiredVersion
  • 共享中的模块请求
    • 仅在使用时提供。
    • 将匹配构建中所有使用的相同模块请求。
    • 将提供所有匹配的模块。
    • 将从图表中该位置的 package.json 中提取 requiredVersion
    • 当您有嵌套的 node_modules 时,可以提供和使用多个不同的版本。
  • 共享中带有尾部 / 的模块请求将匹配所有带有此前缀的模块请求。

用例

按页面分离构建

单页应用程序的每个页面都从容器构建中暴露在一个单独的构建中。应用程序 shell 也是一个单独的构建,将所有页面作为远程模块引用。这样,每个页面都可以单独部署。当路由更新或添加新路由时,应用程序 shell 会被部署。应用程序 shell 将常用库定义为共享模块,以避免在页面构建中重复。

组件库作为容器

许多应用程序共享一个公共组件库,该库可以构建为一个容器,并暴露每个组件。每个应用程序都从组件库容器消费组件。组件库的更改可以单独部署,而无需重新部署所有应用程序。应用程序会自动使用组件库的最新版本。

动态远程容器

容器接口支持 getinit 方法。init 是一个兼容 async 的方法,它接受一个参数:共享作用域对象。此对象用作远程容器中的共享作用域,并填充来自主机的提供模块。它可以用于在运行时动态地将远程容器连接到主机容器。

init.js

(async () => {
  // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
  await __webpack_init_sharing__('default');
  const container = window.someContainer; // or get the container somewhere else
  // Initialize the container, it may provide shared modules
  await container.init(__webpack_share_scopes__.default);
  const module = await container.get('./module');
})();

容器尝试提供共享模块,但如果共享模块已被使用,则警告和提供的共享模块将被忽略。容器仍可能将其用作回退。

这样你就可以动态加载一个 A/B 测试,它提供一个不同版本的共享模块。

示例

init.js

function loadComponent(scope, module) {
  return async () => {
    // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}

loadComponent('abtests', 'test123');

查看完整实现

基于 Promise 的动态远程

通常,远程是使用 URL 配置的,如本示例所示

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@https://:3001/remoteEntry.js',
      },
    }),
  ],
};

但是你也可以向此远程传递一个 promise,它将在运行时解析。你应该使用任何符合上述 get/init 接口的模块来解析此 promise。例如,如果你想通过查询参数传递要使用的联邦模块版本,你可以这样做

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: `promise new Promise(resolve => {
      const urlParams = new URLSearchParams(window.location.search)
      const version = urlParams.get('app1VersionParam')
      // This part depends on how you plan on hosting and versioning your federated modules
      const remoteUrlWithVersion = 'https://:3001/' + version + '/remoteEntry.js'
      const script = document.createElement('script')
      script.src = remoteUrlWithVersion
      script.onload = () => {
        // the injected script has loaded and is available on window
        // we can now resolve this Promise
        const proxy = {
          get: (request) => window.app1.get(request),
          init: (...arg) => {
            try {
              return window.app1.init(...arg)
            } catch(e) {
              console.log('remote container already initialized')
            }
          }
        }
        resolve(proxy)
      }
      // inject this script with the src set to the versioned remoteEntry.js
      document.head.appendChild(script);
    })
    `,
      },
      // ...
    }),
  ],
};

请注意,使用此 API 时,您必须解析一个包含 get/init API 的对象。

动态公共路径

提供主机 API 以设置公共路径

可以通过从远程模块暴露一个方法,允许主机在运行时设置远程模块的 publicPath。

当您将独立部署的子应用程序挂载到主机域的子路径上时,这种方法特别有用。

场景

您有一个托管在 https://my-host.com/app/* 的主机应用和一个托管在 https://foo-app.com 的子应用。子应用也挂载在主机域上,因此,https://foo-app.com 预计可以通过 https://my-host.com/app/foo-app 访问,并且 https://my-host.com/app/foo-app/* 请求通过代理重定向到 https://foo-app.com/*

示例

webpack.config.js(远程)

module.exports = {
  entry: {
    remote: './public-path',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote', // this name needs to match with the entry name
      exposes: ['./public-path'],
      // ...
    }),
  ],
};

public-path.js(远程)

export function set(value) {
  __webpack_public_path__ = value;
}

src/index.js(主机)

const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');

//bootstrap app  e.g. import('./bootstrap.js')

从脚本推断公共路径

可以从脚本标签中的 document.currentScript.src 推断出 publicPath,并在运行时使用 __webpack_public_path__ 模块变量设置它。

示例

webpack.config.js(远程)

module.exports = {
  entry: {
    remote: './setup-public-path',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote', // this name needs to match with the entry name
      // ...
    }),
  ],
};

setup-public-path.js(远程)

// derive the publicPath with your own logic and set it with the __webpack_public_path__ API
__webpack_public_path__ = document.currentScript.src + '/../';

故障排除

Uncaught Error: Shared module is not available for eager consumption

应用程序正在急切地执行一个作为全向主机运行的应用程序。有以下选项可供选择

你可以在 Module Federation 的高级 API 中将依赖项设置为 eager,这不会将模块放入异步块中,而是同步提供它们。这允许我们在初始块中使用这些共享模块。但请注意,所有提供和回退模块都将始终被下载。建议只在应用程序的一个点(例如 shell)提供它。

我们强烈建议使用异步边界。它将拆分较大块的初始化代码,以避免任何额外的往返并通常提高性能。

例如,你的入口点看起来像这样

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

让我们创建 bootstrap.js 文件并将入口的内容移入其中,然后将该 bootstrap 导入到入口中

index.js

+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));

bootstrap.js

+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));

此方法有效,但可能存在限制或缺点。

通过 ModuleFederationPlugin 将依赖项设置为 eager: true

webpack.config.js

// ...
new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      eager: true,
    },
  },
});

Uncaught Error: Module "./Button" does not exist in container.

它可能不会说 "./Button",但错误消息会类似。如果您正在从 webpack beta.16 升级到 webpack beta.17,通常会看到此问题。

在 ModuleFederationPlugin 中。将 exposes 从

new ModuleFederationPlugin({
  exposes: {
-   'Button': './src/Button'
+   './Button':'./src/Button'
  }
});

Uncaught TypeError: fn is not a function

您可能缺少远程容器,请确保已添加。如果已为要使用的远程加载了容器,但仍然看到此错误,请也将主机容器的远程容器文件添加到 HTML 中。

来自不同远程模块之间的冲突

如果要从不同的远程加载多个模块,建议为远程构建设置 output.uniqueName 选项,以避免多个 webpack 运行时之间的冲突。

依赖图

每当一个文件依赖于另一个文件时,webpack 就将其视为一个依赖项。这使得 webpack 能够获取非代码资产,例如图像或网络字体,并将其作为应用程序的依赖项

当 webpack 处理你的应用程序时,它从命令行或其配置文件中定义的模块列表开始。从这些入口点开始,webpack 递归地构建一个包含你的应用程序所需所有模块的依赖图,然后将所有这些模块打包成少量 bundles——通常只有一个——供浏览器加载。

目标

由于 JavaScript 可以为服务器和浏览器编写,webpack 提供了多种部署目标,你可以在 webpack 配置中进行设置。

用法

要设置 target 属性,请在 webpack 配置中设置目标值

webpack.config.js

module.exports = {
  target: 'node',
};

在上面的示例中,使用 node webpack 将编译用于 Node.js 类似环境(使用 Node.js require 加载 chunk,不触及任何内置模块,如 fspath)。

每个目标都有各种部署/环境特定的附加功能,支持以满足其需求。查看可用的目标

多目标

尽管 webpack 支持将多个字符串传递给 target 属性,但你可以通过打包两个独立的配置来创建同构库

webpack.config.js

const path = require('path');
const serverConfig = {
  target: 'node',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.node.js',
  },
  //…
};

const clientConfig = {
  target: 'web', // <=== can be omitted as default is 'web'
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.js',
  },
  //…
};

module.exports = [serverConfig, clientConfig];

上面的示例将在你的 dist 文件夹中创建 lib.jslib.node.js 文件。

资源

从上面的选项可以看出,有多种部署目标可供选择。下面是你可以参考的示例和资源列表。

清单(Manifest)

在典型的使用 webpack 构建的应用程序或网站中,有三种主要类型的代码

  1. 你和你的团队可能编写的源代码。
  2. 你的源代码所依赖的任何第三方库或“供应商”代码。
  3. 一个 webpack 运行时和清单,用于协调所有模块的交互。

本文将重点关注这三部分的最后一项:运行时,特别是清单。

运行时

运行时,连同清单数据,是 webpack 在浏览器中运行时连接模块化应用程序所需的所有代码。它包含连接模块所需的加载和解析逻辑,无论这些模块是否已加载到浏览器中,以及延迟加载尚未加载的模块的逻辑。

清单

一旦你的应用程序以 index.html 文件的形式进入浏览器,应用程序所需的某些 bundle 和各种其他资产必须以某种方式加载和链接。你精心布局的 /src 目录现在已被 webpack 的 optimization 打包、压缩,甚至可能被分割成更小的块以供懒加载。那么 webpack 如何管理所有所需模块之间的交互呢?这就是清单数据的作用所在...

当编译器进入、解析和映射出你的应用程序时,它会记录所有模块的详细信息。这些数据集合被称为“清单”,运行时将使用它来解析和加载模块,一旦它们被打包并发送到浏览器。无论你选择哪种模块语法,那些 importrequire 语句现在都已成为指向模块标识符的 __webpack_require__ 方法。使用清单中的数据,运行时将能够找出从何处检索标识符背后的模块。

问题

现在你对 webpack 的幕后工作原理有了一些了解。“但这对我有什么影响呢?”你可能会问。大多数时候,它没有。运行时将利用清单完成它的工作,一旦你的应用程序进入浏览器,一切似乎都会神奇地正常运行。然而,如果你决定通过利用浏览器缓存来提高项目的性能,这个过程突然变得很重要,你需要理解它。

通过在 bundle 文件名中使用内容哈希,你可以向浏览器指示文件内容何时发生更改,从而使缓存失效。但是,一旦你开始这样做,你将立即注意到一些奇怪的行为。某些哈希会发生变化,即使它们的内容显然没有变化。这是由于运行时和清单的注入造成的,它们在每次构建时都会发生变化。

请参阅我们的《输出管理》指南的清单部分,了解如何提取清单,并阅读下面的指南,了解有关长期缓存的复杂性。

热模块替换

热模块替换(HMR)在应用程序运行时交换、添加或删除模块,而无需完全重新加载。这可以通过几种方式显著加快开发速度

  • 保留在完全重新加载过程中丢失的应用程序状态。
  • 通过仅更新更改的内容,节省宝贵的开发时间。
  • 当对源代码中的 CSS/JS 进行修改时,即时更新浏览器,这几乎与直接在浏览器开发工具中更改样式相当。

工作原理

让我们从不同的视角来了解 HMR 的确切工作原理...

在应用程序中

以下步骤允许模块在应用程序中进行交换

  1. 应用程序要求 HMR 运行时检查更新。
  2. 运行时异步下载更新并通知应用程序。
  3. 应用程序随后要求运行时应用更新。
  4. 运行时同步应用更新。

您可以设置 HMR,使此过程自动发生,或者您可以选择要求用户交互才能进行更新。

在编译器中

除了正常的资产外,编译器还需要发出一个“更新”以允许从旧版本更新到新版本。“更新”由两部分组成

  1. 更新后的清单 (JSON)
  2. 一个或多个更新后的块 (JavaScript)

清单包含新的编译哈希和所有更新块的列表。每个块都包含所有更新模块的新代码(或表示模块已删除的标志)。

编译器确保模块 ID 和块 ID 在这些构建之间保持一致。它通常将这些 ID 存储在内存中(例如,使用webpack-dev-server),但也可以将它们存储在 JSON 文件中。

在模块中

HMR 是一个可选功能,仅影响包含 HMR 代码的模块。一个例子是通过 style-loader 修补样式。为了使修补工作正常,style-loader 实现了 HMR 接口;当它通过 HMR 接收到更新时,它将旧样式替换为新样式。

同样,在模块中实现 HMR 接口时,你可以描述模块更新时应发生的情况。但是,在大多数情况下,不必在每个模块中编写 HMR 代码。如果模块没有 HMR 处理器,更新将冒泡。这意味着一个处理器可以更新一个完整的模块树。如果树中的单个模块更新,则重新加载整个依赖项集。

有关 module.hot 接口的详细信息,请参阅HMR API 页面

在运行时中

这里的事情变得有点技术性……如果你对内部不感兴趣,请随意跳转到HMR API 页面HMR 指南

对于模块系统运行时,会发出额外的代码以跟踪模块的 parentschildren。在管理方面,运行时支持两种方法:checkapply

check 会向更新清单发出 HTTP 请求。如果此请求失败,则没有可用的更新。如果成功,则会将更新的块列表与当前已加载的块列表进行比较。对于每个已加载的块,都会下载相应的更新块。所有模块更新都存储在运行时中。当所有更新块都已下载并准备好应用时,运行时将切换到 ready 状态。

apply 方法会将所有已更新的模块标记为无效。对于每个无效模块,模块内部或其父级必须有一个更新处理程序。否则,无效标志会冒泡并使父级也无效。每次冒泡都会继续,直到到达应用程序的入口点或带有更新处理程序的模块(两者中先到的那个)。如果它从入口点冒泡,则该过程失败。

之后,所有无效模块都会被处理(通过 dispose 处理程序)并卸载。当前哈希值会更新,并且所有 accept 处理程序都会被调用。运行时会切换回 idle 状态,一切恢复正常。

开始使用

HMR 在开发中可以作为 LiveReload 的替代品使用。webpack-dev-server 支持一种 hot 模式,在这种模式下,它会在尝试重新加载整个页面之前,尝试使用 HMR 进行更新。详情请参阅热模块替换指南

为什么选择 webpack

为了理解为什么应该使用 webpack,让我们回顾一下在打包工具出现之前,我们是如何在 Web 上使用 JavaScript 的。

在浏览器中运行 JavaScript 有两种方式。首先,为每个功能包含一个脚本;这种解决方案难以扩展,因为加载过多脚本可能导致网络瓶颈。第二种选择是使用一个包含所有项目代码的大型 .js 文件,但这会导致作用域、大小、可读性和可维护性方面的问题。

IIFEs - 立即执行函数表达式

IIFEs 解决了大型项目的范围问题;当脚本文件被 IIFE 包裹时,您可以安全地连接或组合文件,而无需担心作用域冲突。

IIFE 的使用催生了 Make、Gulp、Grunt、Broccoli 或 Brunch 等工具。这些工具被称为任务运行器,它们将所有项目文件连接在一起。

然而,更改一个文件意味着您必须重新构建整个项目。连接文件使得在不同文件之间重用脚本变得更容易,但却使构建优化更加困难。您如何判断代码是否实际被使用?

即使您只使用了 lodash 中的一个函数,也必须添加整个库,然后将其压缩在一起。您如何对代码的依赖项进行摇树优化 (treeshake)?大规模地进行代码块的懒加载可能很困难,并且需要开发人员大量的手动工作。

JavaScript 模块的诞生归功于 Node.js

Webpack 运行在 Node.js 上,Node.js 是一种 JavaScript 运行时,可以在浏览器环境之外的计算机和服务器中使用。

Node.js 发布后,一个新时代开始了,并带来了新的挑战。既然 JavaScript 不在浏览器中运行,Node 应用程序应该如何加载新的代码块呢?不再有可以添加的 HTML 文件和脚本标签了。

CommonJS 问世并引入了 require,它允许您在当前文件中加载和使用模块。这通过按需导入每个模块,开箱即用地解决了作用域问题。

npm + Node.js + 模块 – 大规模分发

JavaScript 正在以一种语言、一个平台以及一种快速开发和创建快速应用程序的方式,席卷全球。

但浏览器不支持 CommonJS。没有实时绑定。存在循环引用问题。同步模块解析和加载速度慢。尽管 CommonJS 是 Node.js 项目的一个很好的解决方案,但浏览器不支持模块,因此创建了 Browserify、RequireJS 和 SystemJS 等打包工具,使我们能够编写在浏览器中运行的 CommonJS 模块。

ESM - ECMAScript 模块

对于 Web 项目而言,好消息是模块正成为 ECMAScript 标准中的一项官方功能。然而,浏览器支持尚不完善,并且打包仍然比这些早期的模块实现更快,目前也更受推荐。

自动依赖收集

老式的任务运行器乃至 Google Closure Compiler 都要求您预先手动声明所有依赖项。而像 webpack 这样的打包工具则根据导入和导出的内容自动构建并推断您的依赖图。这一点以及其他插件加载器共同带来了出色的开发者体验。

如果能这样就好了……

...拥有一个不仅能让我们编写模块,还能支持任何模块格式(至少在 ESM 普及之前)并同时处理资源和资产的工具,岂不美哉?

这就是 webpack 存在的原因。它是一个工具,允许您打包 JavaScript 应用程序(同时支持 ESM 和 CommonJS),并且可以扩展以支持许多不同的资产,例如图像、字体和样式表。

Webpack 关注性能和加载时间;它总是在改进或添加新功能,例如异步块加载和预取,以提供您的项目和用户最佳的体验。

幕后探秘

本节描述了 webpack 的内部原理,对插件开发者可能有所帮助

打包是一个函数,它接收一些文件并输出另一些文件。

但在输入和输出之间,它还包含模块入口点、块(chunk)、块组(chunk group)以及许多其他中间部分。

主要部分

项目中使用的每个文件都是一个模块

./index.js

import app from './app.js';

./app.js

export default 'the app';

通过相互使用,模块形成一个图(ModuleGraph)。

在打包过程中,模块被组合成块(chunk)。块组合成块组(chunk group),并通过模块相互连接形成一个图(ChunkGraph)。当你描述一个入口点时——在底层,你会创建一个包含一个块的块组。

./webpack.config.js

module.exports = {
  entry: './index.js',
};

创建了一个名为 main 的块组(main 是入口点的默认名称)。这个块组包含 `./index.js` 模块。当解析器处理 `./index.js` 内部的导入时,新的模块会被添加到这个块中。

另一个例子

./webpack.config.js

module.exports = {
  entry: {
    home: './home.js',
    about: './about.js',
  },
};

创建了两个名为 homeabout 的块组。每个块组都包含一个带模块的块——home 对应 `./home.js`,about 对应 `./about.js`

一个块组中可能有多个块。例如,SplitChunksPlugin 将一个块拆分成一个或多个块。

块(Chunk)

块有两种形式

  • initial 是入口点的主块。这个块包含了您为入口点指定的所有模块及其依赖项。
  • non-initial 是一个可能被懒加载的块。它在使用动态导入SplitChunksPlugin时出现。

每个块都有一个对应的**资产**。资产是输出文件——即打包的结果。

webpack.config.js

module.exports = {
  entry: './src/index.jsx',
};

./src/index.jsx

import React from 'react';
import ReactDOM from 'react-dom';

import('./app.jsx').then((App) => {
  ReactDOM.render(<App />, root);
});

创建了名为 main 的初始块。它包含

  • ./src/index.jsx
  • react
  • react-dom

以及它们的所有依赖项,除了 `./app.jsx`

由于 `./app.jsx` 是动态导入的模块,因此为其创建了一个非初始块。

输出

  • /dist/main.js - 一个 initial
  • /dist/394.js - non-initial

默认情况下,non-initial 块没有名称,因此使用唯一的 ID 而非名称。在使用动态导入时,我们可以通过使用“魔法”注释来显式指定块名称。

import(
  /* webpackChunkName: "app" */
  './app.jsx'
).then((App) => {
  ReactDOM.render(<App />, root);
});

输出

  • /dist/main.js - 一个 initial
  • /dist/app.js - non-initial

输出

输出文件的名称受配置中两个字段的影响

  • output.filename - 用于 initial 块文件
  • output.chunkFilename - 用于 non-initial 块文件
  • 在某些情况下,块同时用作 initialnon-initial。在这些情况下,会使用 output.filename

这些字段中提供了一些占位符。最常用的是

  • [id] - 块 ID (例如 [id].js -> 485.js)
  • [name] - 块名称 (例如 [name].js -> app.js)。如果一个块没有名称,则会使用其 ID
  • [contenthash] - 输出文件内容的 md4-哈希值 (例如 [contenthash].js -> 4ea6ff1de66c537eb9b2.js)

1 贡献者

webpack