关于 Webpack 的学习与使用

You,30 min read

关于 Webpack 的学习与使用

概念

入口 (entry)

入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 [依赖图(dependency graph)] 的开始。

进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的

默认值是 ./src/index.js

但你可以通过在 webpack configuration (opens in a new tab) 中配置 entry 属性,来指定一个(或多个)不同的入口起点

输出(output)

output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。

主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中

// 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',
  },
};
 

loader

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。

loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 [模块],以供应用程序使用,以及被添加到依赖图中。

// webpack.config.js
 
const path = require('path');
 
module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [
     { 
   // 识别出哪些文件会被转换
      test: /\.txt$/, 
      // 定义出在进行转换时,应该使用哪个 loader
      use: 'raw-loader' 
     }
    ],
  },
};
 

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用) raw-loader 转换一下。”

插件(plugin)

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
 
module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
  plugins: [
   // `html-webpack-plugin` 为应用程序生成一个 HTML 文件,
   // 并自动将生成的所有 bundle 注入到此文件中
   new HtmlWebpackPlugin({ 
    template: './src/index.html' 
   })
 ],
};

模式(mode)

通过选择 developmentproduction 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production

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

浏览器兼容性(browser compatibility)

Webpack 支持所有符合 ES5 标准 (opens in a new tab) 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要 提前加载 polyfill (opens in a new tab)

环境(environment)

Webpack 5 运行于 Node.js v10.13.0+ 的版本

入口起点(entry points)

单个入口(简写)语法

用法:entry: string | [string]

module.exports = {
    // 简写
 entry: './path/to/my/entry/file.js',
 // 等效 默认值是./src/index.js
 entry: {
     main: './path/to/my/entry/file.js',
 }
};

我们也可以将一个文件路径数组传递给 entry 属性,这将创建一个所谓的 "multi-main entry"

在你想要一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时,这种方式就很有用

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

对象语法

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

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

对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式

webpack-merge 自定义扩展 (opens in a new tab)

“webpack 配置的可扩展” 是指,这些配置可以重复使用,并且可以与其他配置组合使用。这是一种流行的技术,用于将关注点从环境(environment)、构建目标(build target)、运行时(runtime)中分离。然后使用专门的工具(如 [webpack-merge])将它们合并起来。

当你通过插件生成入口时,你可以传递空对象 {} 给 entry

描述入口的对象

用于描述入口的对象。你可以使用如下属性:

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

常见场景

分离app(应用程序)和vendor(第三方库)入口

// 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 我们想要配置 2 个单独的入口点(例如上面的示例)。

为什么? 这样你就可以在 vendor.js 中存入未做修改的必要 library 或文件(例如 Bootstrap, jQuery, 图片等),然后将它们打包在一起成为单独的 chunk。内容哈希保持不变,这使浏览器可以独立地缓存它们,从而减少了加载时间

在 webpack < 4 的版本中,通常将 vendor 作为一个单独的入口起点添加到 entry 选项中,以将其编译为一个单独的文件(与 CommonsChunkPlugin 结合使用)。
而在 webpack 4 中不鼓励这样做。而是使用 [optimization.splitChunks] 选项,将 vendor 和 app(应用程序) 模块分开,并为其创建一个单独的文件。不要 为 vendor 或其他不是执行起点创建 entry

webpack4之后可以使用optimization.splitChunks 将vendor和app分离

多页面应用程序

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

这是什么? 我们告诉 webpack 需要三个独立分离的依赖图(如上面的示例)。

为什么? 在多页面应用程序中,server 会拉取一个新的 HTML 文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事,例如使用 [optimization.splitChunks] 为页面间共享的应用程序代码创建 bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益

多页应用中可以有哪些工程优化手段?

可以使用 optimization.splitChunks 为页面间共享的应用程序代码创建bundle 由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益

输出(output)

可以通过配置 output 选项,告知 webpack 如何向硬盘写入编译文件。

注意,即使可以存在多个 entry 起点,但只能指定一个 output 配置

用法

在 webpack 配置中,output 属性的最低要求是,将它的值设置为一个对象,然后为将输出文件的文件名配置为一个 output.filename (opens in a new tab)

// 此配置将一个单独的 `bundle.js` 文件输出到 `dist` 目录中
module.exports = {
  output: {
    filename: 'bundle.js',
  },
};

多个入口起点

如果配置中创建出多于一个 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用 占位符(substitutions) (opens in a new tab) 来确保每个文件具有唯一的名称

使用 '[name].bundle.js' [] 为占位符 来确保每个文件具有唯一的名称

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

高级进阶

以下是对资源使用 CDN 和 hash 的复杂示例

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

publicPath 表示资源(assets)被引用的根路径,在生产环境下生效;可以是相对路径,也可以是绝对路径 path 目录对应一个 绝对路径

output config (opens in a new tab)

如果在编译时,不知道最终输出文件的 publicPath 是什么地址,则可以将其留空,并且在运行时通过入口起点文件中的 __webpack_public_path__ 动态设置

__webpack_public_path__ = myRuntimePublicPath;
 
// 应用程序入口的其余部分

loader

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。

因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。 loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS 文件!

示例

例如,你可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。

为此,首先安装相对应的 loader

npm install --save-dev css-loader ts-loader
 
## or
yarn add css-loader ts-loader -D

然后指示 webpack 对每个 .css 使用 css-loader (opens in a new tab),以及对所有 .ts 文件使用 ts-loader (opens in a new tab)

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

使用loader

在你的应用程序中,有两种使用 loader 的方式:

Configuration

module.rules (opens in a new tab) 允许你在 webpack 配置中指定多个 loader。

这种方式是展示 loader 的一种简明方式,并且有助于使代码变得简洁和易于维护。同时让你对各个 loader 有个全局概览:

loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。

在下面的示例中,从 sass-loader 开始执行,然后继续执行 css-loader,最后以 style-loader 为结束。查看 loader 功能 (opens in a new tab) 章节,了解有关 loader 顺序的更多信息

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

内联方式(不推荐)

可以在 import 语句或任何 与 "import" 方法同等的引用方式 (opens in a new tab) 中指定 loader。

使用 ! 将资源中的 loader 分开。每个部分都会相对于当前目录解析

通过为内联 import 语句添加前缀,可以覆盖 配置 (opens in a new tab) 中的所有 loader, preLoader 和 postLoader:

 
// 使用 `!` 前缀,将禁用所有已配置的 normal loader(普通 loader)
import Styles from '!style-loader!css-loader?modules!./styles.css';
 
// 使用 `!!` 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
 
// 使用 `-!` 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders
import Styles from '-!style-loader!css-loader?modules!./styles.css';
 
 

选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?{"key":"value","foo":"bar"}

loader 特性

可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。

用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言转译(或编译)和 更多其他特性 (opens in a new tab)

解析 loader

loader 遵循标准 模块解析 (opens in a new tab) 规则。多数情况下,loader 将从 模块路径 (opens in a new tab) 加载(通常是从 npm installnode_modules 进行加载)。

我们预期 loader 模块导出为一个函数,并且编写为 Node.js 兼容的 JavaScript。

通常使用 npm 进行管理 loader,但是也可以将应用程序中的文件作为自定义 loader。

按照约定,loader 通常被命名为 xxx-loader(例如 json-loader)。更多详细信息,请查看 编写一个 loader (opens in a new tab)

plugin

插件 是 webpack 的 支柱 (opens in a new tab) 功能。Webpack 自身也是构建于你在 webpack 配置中用到的 相同的插件系统 之上!

插件目的在于解决 loader (opens in a new tab) 无法实现的其他事。Webpack 提供很多开箱即用的 插件 (opens in a new tab)

如果在插件中使用了 webpack-sources (opens in a new tab) 的 package,请使用 require('webpack').sources 替代 require('webpack-sources'),以避免持久缓存的版本冲突

剖析

webpack 插件是一个具有 apply (opens in a new tab) 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。

ConsoleLogOnBuildWebpackPlugin.js

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
 
class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack 构建正在启动!');
    });
  }
}
 
module.exports = ConsoleLogOnBuildWebpackPlugin;

compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中重复使用

用法

由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins 属性传入一个 new 实例。

取决于你的 webpack 用法,对应有多种使用插件的方式

配置方式

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 访问内置的插件
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 引入一个名为 my-first-webpack.bundle.js 的 JS 文件

Node API 方式

在使用 Node API 时,还可以通过配置中的 plugins 属性传入插件

some-node-script.js

const webpack = require('webpack'); // 访问 webpack 运行时(runtime)
const configuration = require('./webpack.config.js');
 
let compiler = webpack(configuration);
 
new webpack.ProgressPlugin().apply(compiler);
 
compiler.run(function (err, stats) {
  // ...
});

你知道吗:以上看到的示例和 webpack 运行时(runtime)本身 (opens in a new tab) 极其类似。webpack 源码 (opens in a new tab) 中隐藏有大量使用示例,你可以将其应用在自己的配置和脚本中

配置(Configuration)

你可能已经注意到,很少有 webpack 配置看起来完全相同。这是因为 webpack 的配置文件是 JavaScript 文件,文件内导出了一个 webpack 配置的对象 (opens in a new tab) webpack 会根据该配置定义的属性进行处理。

webpack 中配置规范 遵循CommonJS模块规范

由于 webpack 遵循 CommonJS 模块规范,因此,你可以在配置中使用

请在合适的场景,使用这些功能。

webpack 中应该注意的配置

虽然技术上可行,但还是应避免如下操作

接下来的示例中,展示了 webpack 配置如何实现既可表达,又可灵活配置,这主要得益于 配置即为代码

基本配置

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

查看配置章节 (opens in a new tab)中所有支持的配置选项

多个target

除了可以将单个配置导出为 object,function (opens in a new tab) 或 Promise (opens in a new tab) 以外,还可以将其导出为多个配置。

查看导出多个配置 (opens in a new tab)

使用其他配置语言

Webpack 支持由多种编程和数据语言编写的配置文件。

查看配置语言 (opens in a new tab)

模块(Modules)

模块化编程 (opens in a new tab)中,开发者将程序分解为功能离散的 chunk,并称之为 模块

每个模块都拥有小于完整程序的体积,使得验证、调试及测试变得轻而易举。

精心编写的 模块 提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的

何为 webpack 模块

与 Node.js 模块 (opens in a new tab)相比,webpack 模块 能以各种方式表达它们的依赖关系。下面是一些示例:

支持的模块类型

Webpack 天生支持如下模块类型:

通过 loader 可以使 webpack 支持多种语言和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生_模块_,并将相关依赖引入到你的 bundles中。 webpack 社区已经为各种流行的语言和预处理器创建了 loader,其中包括:

当然还有更多!总得来说,webpack 提供了可定制,强大且丰富的 API,允许在 任何技术栈 中使用,同时支持在开发、测试和生产环境的工作流中做到 无侵入性

关于 loader 的相关信息,请参考 loader 列表 (opens in a new tab) 或 自定义 loader (opens in a new tab)

模块解析 (Module Resolution)

resolver 是一个帮助寻找模块绝对路径的库。

一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:

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

所依赖的模块可以是来自应用程序的代码或第三方库。

resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。

当打包模块时,webpack 使用 enhanced-resolve (opens in a new tab) 来解析文件路径。

webpack 中的解析规则

使用 enhanced-resolve,webpack 能解析三种文件路径:

绝对路径

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

由于已经获得文件的绝对路径,因此不需要再做进一步解析

相对路径

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

在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录。在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径

模块路径

在 resolve.modules (opens in a new tab) 中指定的所有目录中检索模块。 你可以通过配置别名的方式来替换初始模块路径,具体请参照 resolve.alias (opens in a new tab) 配置选项。

一旦根据上述规则解析路径后,resolver 将会检查路径是指向文件还是文件夹。如果路径指向文件:

如果路径指向一个文件夹,则进行如下步骤寻找具有正确扩展名的文件:

Webpack 会根据构建目标,为这些选项提供合理的默认 (opens in a new tab)配置

解析 loader

loader 的解析规则也遵循特定的规范。但是 resolveLoader (opens in a new tab) 配置项可以为 loader 设置独立的解析规则

缓存

每次文件系统访问文件都会被缓存,以便于更快触发对同一文件的多个并行或串行请求。

在 watch 模式 (opens in a new tab) 下,只有修改过的文件会被从缓存中移出。如果关闭 watch 模式,则会在每次编译前清理缓存。

欲了解更多上述配置信息,请查阅 Resolve API (opens in a new tab)

模块联邦(Module Federation)⌛️

依赖图(Dependency Graph)

每当一个文件依赖另一个文件时,webpack 都会将文件视为直接存在 依赖关系

这使得 webpack 可以获取非代码资源,如 images 或 web 字体等。并会把它们作为 依赖 提供给应用程序。

当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。

从 入口 (opens in a new tab) 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle —— 通常只有一个 —— 可由浏览器加载

对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当浏览器发起请求时,它能最大程度的减少应用的等待时间。而对于 HTTP/2 来说,你还可以使用代码分割 (opens in a new tab)进行进一步优化

随着http/2 协议的普及我们可以通过 代码分割 来最大化并行加载大型前端应用的bundle

Further Reading

target

由于 JavaScript 既可以编写服务端代码也可以编写浏览器代码,所以 webpack 提供了多种部署 target,你可以在 webpack 的配置选项 (opens in a new tab)中进行设置。

webpack 的 target 属性,不要和 output.libraryTarget 属性混淆。有关 output 属性的更多信息,请参阅 output 指南 (opens in a new tab)

用法

想设置 target 属性,只需在 webpack 配置中设置 target 字段

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

在上述示例中,target 设置为 node,webpack 将在类 Node.js 环境编译代码。(使用 Node.js 的 require 加载 chunk,而不加载任何内置模块,如 fs 或 path)。

每个 target 都包含各种 deployment(部署)/environment(环境)特定的附加项,以满足其需求。具体请参阅 target 可用值 (opens in a new tab)

多 target

虽然 webpack 不支持 向 target 属性传入多个字符串,但是可以通过设置两个独立配置,来构建对 library 进行同构

const path = require('path');
const serverConfig = {
  target: 'node',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.node.js',
  },
  //…
};
 
const clientConfig = {
  target: 'web', // <=== 默认为 'web',可省略
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.js',
  },
  //…
};
 
module.exports = [serverConfig, clientConfig];

上述示例中,将会在 dist 文件夹下创建 lib.js 和 lib.node.js 文件

资源

从上面选项可以看出,你可以选择部署不同的 target。下面是可以参考的示例和资源:

manifest

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

  1. 你或你的团队编写的源码。
  2. 你的源码会依赖的任何第三方的 library 或 "vendor" 代码。
  3. webpack 的 runtime 和 manifest,管理所有模块的交互。

本文将重点介绍这三个部分中的最后部分:runtime 和 manifest,特别是 manifest。

模块热更新(HMR:Hot Module Replace)

为什么选择 webpack

揭示内部原理

2026 © Lizhenyui.