本节包含帮助您理解和掌握 webpack 提供的各种工具和功能的指南。第一篇指南将带您了解如何入门。
这些指南会随着内容的深入而变得更加高级。大多数指南都可作为起点,完成后您应该会更自在地深入查阅实际的文档。
Webpack 用于编译 JavaScript 模块。一旦安装,您可以通过其命令行界面 (CLI) 或API 与 webpack 交互。如果您刚接触 webpack,请阅读核心概念和这份比较,以了解为什么您可能希望使用它而非社区中的其他工具。
首先,让我们创建一个目录,初始化 npm,本地安装 webpack,并安装 webpack-cli
(用于在命令行上运行 webpack 的工具)
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
在整个指南中,我们将使用 diff
块来展示我们对目录、文件和代码所做的更改。例如
+ this is a new line you shall copy into your code
- and this is a line to be removed from your code
and this is a line not to touch.
现在我们将创建以下目录结构、文件及其内容
项目
webpack-demo
|- package.json
|- package-lock.json
+ |- index.html
+ |- /src
+ |- index.js
src/index.js
function component() {
const element = document.createElement('div');
// Lodash, currently included via a script, is required for this line to work
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
<script src="https://unpkg.com/lodash@4.17.20"></script>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
我们还需要调整我们的 package.json
文件,以确保我们将包标记为 private
,并移除 main
入口。这是为了防止您的代码意外发布。
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
- "main": "index.js",
+ "private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2"
}
}
在此示例中,<script>
标签之间存在隐式依赖。我们的 index.js
文件依赖于 lodash
在其运行前已包含在页面中。这是因为 index.js
从未明确声明需要 lodash
;它假设全局变量 _
存在。
以这种方式管理 JavaScript 项目存在问题
让我们改用 webpack 来管理这些脚本。
首先,我们将稍微调整目录结构,将“源代码”(./src
)与“分发”代码(./dist
)分开。“源代码”是我们将编写和编辑的代码。“分发”代码是我们构建过程的最小化和优化后的 output
,最终将加载到浏览器中。按如下方式调整目录结构
项目
webpack-demo
|- package.json
|- package-lock.json
+ |- /dist
+ |- index.html
- |- index.html
|- /src
|- index.js
要将 lodash
依赖与 index.js
打包在一起,我们需要在本地安装该库
npm install --save lodash
现在,让我们在脚本中导入 lodash
src/index.js
+import _ from 'lodash';
+
function component() {
const element = document.createElement('div');
- // Lodash, currently included via a script, is required for this line to work
+ // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
现在,由于我们将打包脚本,我们必须更新 index.html
文件。让我们移除 lodash 的 <script>
标签,因为我们现在通过 import
导入它,并修改另一个 <script>
标签以加载打包文件,而不是原始的 ./src
文件
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
- <script src="https://unpkg.com/lodash@4.17.20"></script>
</head>
<body>
- <script src="./src/index.js"></script>
+ <script src="main.js"></script>
</body>
</html>
在此设置中,index.js
明确要求 lodash
存在,并将其绑定为 _
(无全局作用域污染)。通过声明模块所需的依赖项,webpack 可以使用此信息构建依赖图。然后它使用该图生成一个优化的打包文件,其中脚本将按正确的顺序执行。
话虽如此,让我们运行 npx webpack
,它将以 src/index.js
中的脚本作为入口点,并生成 dist/main.js
作为输出。npx
命令(Node 8.2/npm 5.2.0 或更高版本自带)会运行我们一开始安装的 webpack 包的 webpack 二进制文件(./node_modules/.bin/webpack
)
$ npx webpack
[webpack-cli] Compilation finished
asset main.js 69.3 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1851 ms
在浏览器中打开 dist
目录下的 index.html
,如果一切顺利,您应该会看到以下文本:'Hello webpack'
。
import
和 export
语句已在 ES2015 中标准化。目前大多数浏览器都支持它们,但有些浏览器不识别新语法。不过不用担心,webpack 开箱即用支持它们。
在幕后,webpack 实际上会“转译”代码,以便旧版浏览器也能运行。如果您检查 dist/main.js
,您可能会看到 webpack 是如何做到的,这非常巧妙!除了 import
和 export
,webpack 还支持各种其他模块语法,有关更多信息,请参阅模块 API。
请注意,webpack 不会更改除 import
和 export
语句之外的任何代码。如果您使用其他ES2015 特性,请务必通过 webpack 的加载器系统使用 Babel 等转译器。
从版本 4 开始,webpack 不需要任何配置,但大多数项目需要更复杂的设置,这就是 webpack 支持配置文件的原因。这比在终端中手动输入大量命令要高效得多,所以让我们创建一个
项目
webpack-demo
|- package.json
|- package-lock.json
+ |- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
现在,让我们再次运行构建,但使用我们的新配置文件
$ npx webpack --config webpack.config.js
[webpack-cli] Compilation finished
asset main.js 69.3 KiB [compared for emit] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1934 ms
配置文件比命令行界面使用提供了更大的灵活性。我们可以通过这种方式指定加载器规则、插件、解析选项和许多其他增强功能。有关更多信息,请参阅配置文档。
鉴于从命令行界面运行本地 webpack 副本并不那么有趣,我们可以设置一个快捷方式。让我们通过添加一个npm 脚本来调整我们的 package.json
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
现在可以使用 npm run build
命令来代替我们之前使用的 npx
命令。请注意,在 scripts
中,我们可以像使用 npx
一样,按名称引用本地安装的 npm 包。这种约定是大多数基于 npm 的项目中的标准,因为它允许所有贡献者使用同一组常用脚本。
现在运行以下命令,看看您的脚本别名是否有效
$ npm run build
...
[webpack-cli] Compilation finished
asset main.js 69.3 KiB [compared for emit] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1940 ms
既然您已经完成了基本构建,您应该继续阅读下一篇指南资源管理
,了解如何使用 webpack 管理图像和字体等资源。此时,您的项目应该如下所示
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- main.js
|- index.html
|- /src
|- index.js
|- /node_modules
如果您想了解更多关于 webpack 的设计,您可以查阅基本概念和配置页面。此外,API 部分深入探讨了 webpack 提供的各种接口。
如果您一直从头开始遵循指南,您现在应该有一个显示“Hello webpack”的小项目。现在让我们尝试合并一些其他资源,例如图像,看看它们如何处理。
在 webpack 之前,前端开发人员会使用像 Grunt 和 Gulp 这样的工具来处理这些资源,并将它们从 /src
文件夹移动到 /dist
或 /build
目录。JavaScript 模块也使用了同样的想法,但像 webpack 这样的工具会动态打包所有依赖项(创建所谓的依赖图)。这非常棒,因为现在每个模块都明确声明了它的依赖项,我们将避免打包未使用的模块。
webpack 最酷的功能之一是,除了 JavaScript 之外,您还可以包含任何其他类型的文件,只要有加载器或内置的资源模块支持。这意味着上面列出的适用于 JavaScript 的相同好处(例如,明确的依赖项)可以应用于构建网站或 Web 应用程序所使用的所有内容。让我们从 CSS 开始,因为您可能已经熟悉该设置。
在我们开始之前,让我们对项目进行一些小的改动
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
- <title>Getting Started</title>
+ <title>Asset Management</title>
</head>
<body>
- <script src="main.js"></script>
+ <script src="bundle.js"></script>
</body>
</html>
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
- filename: 'main.js',
+ filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
为了从 JavaScript 模块中 import
CSS 文件,您需要安装 style-loader
和 css-loader
并将其添加到您的module
配置中
npm install --save-dev style-loader css-loader
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
};
模块加载器可以链式调用。链中的每个加载器都会对处理后的资源应用转换。链以反向顺序执行。第一个加载器将其结果(已应用转换的资源)传递给下一个,依此类推。最后,webpack 期望链中的最后一个加载器返回 JavaScript。
应保持上述加载器顺序:'style-loader'
在前,然后是'css-loader'
。如果不遵循此约定,webpack 可能会抛出错误。
这使您能够将 import './style.css'
导入到依赖该样式的文件中。现在,当该模块运行时,一个包含字符串化 CSS 的 <style>
标签将被插入到您的 HTML 文件的 <head>
中。
让我们通过向项目添加一个新的 style.css
文件并在我们的 index.js
中导入它来尝试一下
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- style.css
|- index.js
|- /node_modules
src/style.css
.hello {
color: red;
}
src/index.js
import _ from 'lodash';
+import './style.css';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.classList.add('hello');
return element;
}
document.body.appendChild(component());
现在运行您的构建命令
$ npm run build
...
[webpack-cli] Compilation finished
asset bundle.js 72.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 539 KiB
modules by path ./node_modules/ 538 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
modules by path ./src/ 965 bytes
./src/index.js + 1 modules 639 bytes [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 326 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 2231 ms
再次在浏览器中打开 dist/index.html
,您应该会看到 Hello webpack 现在被设置为红色样式。要查看 webpack 所做的工作,请检查页面(不要查看页面源代码,因为它不会显示结果,因为 <style>
标签是由 JavaScript 动态创建的)并查看页面的 head 标签。它应该包含我们在 index.js
中导入的样式块。
请注意,您可以(在大多数情况下应该)最小化 CSS 以在生产环境中获得更好的加载时间。此外,几乎任何您能想到的 CSS 方言都有加载器——例如 postcss、sass 和 less。
现在我们正在引入 CSS,但我们的图像(如背景和图标)呢?从 webpack 5 开始,使用内置的资源模块,我们也可以轻松地将它们整合到我们的系统中
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
+ {
+ test: /\.(png|svg|jpg|jpeg|gif)$/i,
+ type: 'asset/resource',
+ },
],
},
};
现在,当您 import MyImage from './my-image.png'
时,该图像将被处理并添加到您的 output
目录中,并且 MyImage
变量将包含处理后该图像的最终 URL。如上所示,当使用 css-loader
时,对于 CSS 中的 url('./my-image.png')
,也会发生类似的过程。加载器会识别这是一个本地文件,并将 './my-image.png'
路径替换为图像在 output
目录中的最终路径。html-loader
以相同的方式处理 <img src="./my-image.png" />
。
让我们向项目中添加一张图片,看看它是如何工作的,您可以使用任何您喜欢的图片
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- icon.png
|- style.css
|- index.js
|- /node_modules
src/index.js
import _ from 'lodash';
import './style.css';
+import Icon from './icon.png';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
+ // Add the image to our existing div.
+ const myIcon = new Image();
+ myIcon.src = Icon;
+
+ element.appendChild(myIcon);
+
return element;
}
document.body.appendChild(component());
src/style.css
.hello {
color: red;
+ background: url('./icon.png');
}
让我们创建一个新的构建,然后再次打开 index.html
文件
$ npm run build
...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
asset bundle.js 73.4 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 540 KiB (javascript) 9.88 KiB (asset)
modules by path ./node_modules/ 539 KiB
modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB
./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
./node_modules/css-loader/dist/runtime/getUrl.js 830 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
modules by path ./src/ 1.45 KiB (javascript) 9.88 KiB (asset)
./src/index.js + 1 modules 794 bytes [built] [code generated]
./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 648 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 1972 ms
如果一切顺利,您现在应该会看到您的图标作为重复背景,以及在我们的 Hello webpack 文本旁边的一个 img
元素。如果您检查此元素,您会看到实际的文件名已更改为类似 29822eaa871e8eadeaa4.png
的名称。这意味着 webpack 在 src
文件夹中找到了我们的文件并对其进行了处理!
那么像字体这样的其他资源呢?资源模块将通过它们加载的任何文件输出到您的构建目录。这意味着我们可以将它们用于任何类型的文件,包括字体。让我们更新 webpack.config.js
来处理字体文件
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
+ {
+ test: /\.(woff|woff2|eot|ttf|otf)$/i,
+ type: 'asset/resource',
+ },
],
},
};
向您的项目添加一些字体文件
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- my-font.woff
+ |- my-font.woff2
|- icon.png
|- style.css
|- index.js
|- /node_modules
配置好加载器并就位字体后,您可以通过 @font-face
声明来合并它们。本地的 url(...)
指令将被 webpack 捕获,就像处理图像一样
src/style.css
+@font-face {
+ font-family: 'MyFont';
+ src: url('./my-font.woff2') format('woff2'),
+ url('./my-font.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+}
+
.hello {
color: red;
+ font-family: 'MyFont';
background: url('./icon.png');
}
现在运行一个新的构建,让我们看看 webpack 是否处理了我们的字体
$ npm run build
...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
assets by info 33.2 KiB [immutable]
asset 55055dbfc7c6a83f60ba.woff 18.8 KiB [emitted] [immutable] [from: src/my-font.woff] (auxiliary name: main)
asset 8f717b802eaab4d7fb94.woff2 14.5 KiB [emitted] [immutable] [from: src/my-font.woff2] (auxiliary name: main)
asset bundle.js 73.7 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 541 KiB (javascript) 43.1 KiB (asset)
javascript modules 541 KiB
modules by path ./node_modules/ 539 KiB
modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB 2 modules
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
modules by path ./src/ 1.98 KiB
./src/index.js + 1 modules 794 bytes [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 1.21 KiB [built] [code generated]
asset modules 126 bytes (javascript) 43.1 KiB (asset)
./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
./src/my-font.woff2 42 bytes (javascript) 14.5 KiB (asset) [built] [code generated]
./src/my-font.woff 42 bytes (javascript) 18.8 KiB (asset) [built] [code generated]
webpack 5.4.0 compiled successfully in 2142 ms
再次打开 dist/index.html
,看看我们的 Hello webpack 文本是否已更改为新字体。如果一切顺利,您应该会看到这些更改。
另一种可以加载的有用资源是数据,例如 JSON 文件、CSV、TSV 和 XML。JSON 支持实际上是内置的,类似于 NodeJS,这意味着 import Data from './data.json'
默认情况下将起作用。要导入 CSV、TSV 和 XML,您可以使用 csv-loader
和 xml-loader
。让我们处理这三种类型的加载
npm install --save-dev csv-loader xml-loader
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
+ {
+ test: /\.(csv|tsv)$/i,
+ use: ['csv-loader'],
+ },
+ {
+ test: /\.xml$/i,
+ use: ['xml-loader'],
+ },
],
},
};
向您的项目添加一些数据文件
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- data.xml
+ |- data.csv
|- my-font.woff
|- my-font.woff2
|- icon.png
|- style.css
|- index.js
|- /node_modules
src/data.xml
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Mary</to>
<from>John</from>
<heading>Reminder</heading>
<body>Call Cindy on Tuesday</body>
</note>
src/data.csv
to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you
现在您可以 import
这四种数据类型(JSON、CSV、TSV、XML)中的任何一种,您导入的 Data
变量将包含用于消费的解析后的 JSON
src/index.js
import _ from 'lodash';
import './style.css';
import Icon from './icon.png';
+import Data from './data.xml';
+import Notes from './data.csv';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
// Add the image to our existing div.
const myIcon = new Image();
myIcon.src = Icon;
element.appendChild(myIcon);
+ console.log(Data);
+ console.log(Notes);
+
return element;
}
document.body.appendChild(component());
重新运行 npm run build
命令并打开 dist/index.html
。如果您查看开发工具中的控制台,您应该能够看到导入的数据被记录到控制台!
// No warning
import data from './data.json';
// Warning shown, this is not allowed by the spec.
import { foo } from './data.json';
可以通过使用自定义解析器而不是特定的 webpack 加载器,将任何 toml
、yaml
或 json5
文件作为 JSON 模块导入。
假设您在 src
文件夹下有 data.toml
、data.yaml
和 data.json5
文件
src/data.toml
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z
src/data.yaml
title: YAML Example
owner:
name: Tom Preston-Werner
organization: GitHub
bio: |-
GitHub Cofounder & CEO
Likes tater tots and beer.
dob: 1979-05-27T07:32:00.000Z
src/data.json5
{
// comment
title: 'JSON5 Example',
owner: {
name: 'Tom Preston-Werner',
organization: 'GitHub',
bio: 'GitHub Cofounder & CEO\n\
Likes tater tots and beer.',
dob: '1979-05-27T07:32:00.000Z',
},
}
首先安装 toml
、yamljs
和 json5
包
npm install toml yamljs json5 --save-dev
并在您的 webpack 配置中配置它们
webpack.config.js
const path = require('path');
+const toml = require('toml');
+const yaml = require('yamljs');
+const json5 = require('json5');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
+ {
+ test: /\.toml$/i,
+ type: 'json',
+ parser: {
+ parse: toml.parse,
+ },
+ },
+ {
+ test: /\.yaml$/i,
+ type: 'json',
+ parser: {
+ parse: yaml.parse,
+ },
+ },
+ {
+ test: /\.json5$/i,
+ type: 'json',
+ parser: {
+ parse: json5.parse,
+ },
+ },
],
},
};
src/index.js
import _ from 'lodash';
import './style.css';
import Icon from './icon.png';
import Data from './data.xml';
import Notes from './data.csv';
+import toml from './data.toml';
+import yaml from './data.yaml';
+import json from './data.json5';
+
+console.log(toml.title); // output `TOML Example`
+console.log(toml.owner.name); // output `Tom Preston-Werner`
+
+console.log(yaml.title); // output `YAML Example`
+console.log(yaml.owner.name); // output `Tom Preston-Werner`
+
+console.log(json.title); // output `JSON5 Example`
+console.log(json.owner.name); // output `Tom Preston-Werner`
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
// Add the image to our existing div.
const myIcon = new Image();
myIcon.src = Icon;
element.appendChild(myIcon);
console.log(Data);
console.log(Notes);
return element;
}
document.body.appendChild(component());
重新运行 npm run build
命令并打开 dist/index.html
。您应该能够看到导入的数据被记录到控制台!
上面提到的一切中最酷的部分是,以这种方式加载资源允许您以更直观的方式对模块和资源进行分组。您不再需要依赖一个包含所有内容的全局 /assets
目录,而是可以将资源与使用它们的代码进行分组。例如,这样的结构可能会很有用
- |- /assets
+ |– /components
+ | |– /my-component
+ | | |– index.jsx
+ | | |– index.css
+ | | |– icon.svg
+ | | |– img.png
这种设置使您的代码更具可移植性,因为现在所有紧密耦合的东西都放在一起。假设您想在另一个项目中使用 /my-component
,将其复制或移动到那里的 /components
目录中。只要您安装了所有外部依赖项并且您的配置定义了相同的加载器,您就可以正常使用了。
然而,假设您墨守成规,或者您有一些在多个组件(视图、模板、模块等)之间共享的资源。仍然可以将这些资源存储在基本目录中,甚至可以使用别名来使它们更易于 import
。
在接下来的指南中,我们将不会使用本指南中用到的所有不同资源,所以让我们进行一些清理,以便为指南的下一部分输出管理做好准备
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
- |- data.csv
- |- data.json5
- |- data.toml
- |- data.xml
- |- data.yaml
- |- icon.png
- |- my-font.woff
- |- my-font.woff2
- |- style.css
|- index.js
|- /node_modules
webpack.config.js
const path = require('path');
-const toml = require('toml');
-const yaml = require('yamljs');
-const json5 = require('json5');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- module: {
- rules: [
- {
- test: /\.css$/i,
- use: ['style-loader', 'css-loader'],
- },
- {
- test: /\.(png|svg|jpg|jpeg|gif)$/i,
- type: 'asset/resource',
- },
- {
- test: /\.(woff|woff2|eot|ttf|otf)$/i,
- type: 'asset/resource',
- },
- {
- test: /\.(csv|tsv)$/i,
- use: ['csv-loader'],
- },
- {
- test: /\.xml$/i,
- use: ['xml-loader'],
- },
- {
- test: /\.toml$/i,
- type: 'json',
- parser: {
- parse: toml.parse,
- },
- },
- {
- test: /\.yaml$/i,
- type: 'json',
- parser: {
- parse: yaml.parse,
- },
- },
- {
- test: /\.json5$/i,
- type: 'json',
- parser: {
- parse: json5.parse,
- },
- },
- ],
- },
};
src/index.js
import _ from 'lodash';
-import './style.css';
-import Icon from './icon.png';
-import Data from './data.xml';
-import Notes from './data.csv';
-import toml from './data.toml';
-import yaml from './data.yaml';
-import json from './data.json5';
-
-console.log(toml.title); // output `TOML Example`
-console.log(toml.owner.name); // output `Tom Preston-Werner`
-
-console.log(yaml.title); // output `YAML Example`
-console.log(yaml.owner.name); // output `Tom Preston-Werner`
-
-console.log(json.title); // output `JSON5 Example`
-console.log(json.owner.name); // output `Tom Preston-Werner`
function component() {
const element = document.createElement('div');
- // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.classList.add('hello');
-
- // Add the image to our existing div.
- const myIcon = new Image();
- myIcon.src = Icon;
-
- element.appendChild(myIcon);
-
- console.log(Data);
- console.log(Notes);
return element;
}
document.body.appendChild(component());
并移除我们之前添加的那些依赖项
npm uninstall css-loader csv-loader json5 style-loader toml xml-loader yamljs
让我们继续阅读输出管理
到目前为止,我们已手动将所有资源包含在 index.html
文件中,但随着您的应用程序的增长,一旦您开始在文件名中使用哈希值并输出多个打包文件,手动管理 index.html
文件将变得困难。然而,存在一些插件可以使这个过程更容易管理。
首先,让我们稍微调整一下项目
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
让我们向 src/print.js
文件添加一些逻辑
src/print.js
export default function printMe() {
console.log('I get called from print.js!');
}
并在我们的 src/index.js
文件中使用该函数
src/index.js
import _ from 'lodash';
+import printMe from './print.js';
function component() {
const element = document.createElement('div');
+ const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ btn.innerHTML = 'Click me and check the console!';
+ btn.onclick = printMe;
+
+ element.appendChild(btn);
+
return element;
}
document.body.appendChild(component());
我们还要更新 dist/index.html
文件,为 webpack 拆分入口点做准备
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
- <title>Asset Management</title>
+ <title>Output Management</title>
+ <script src="./print.bundle.js"></script>
</head>
<body>
- <script src="bundle.js"></script>
+ <script src="./index.bundle.js"></script>
</body>
</html>
现在调整配置。我们将把 src/print.js
添加为一个新的入口点 (print
),我们也将更改输出,使其根据入口点名称动态生成打包文件名
webpack.config.js
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ index: './src/index.js',
+ print: './src/print.js',
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
让我们运行 npm run build
,看看会生成什么
...
[webpack-cli] Compilation finished
asset index.bundle.js 69.5 KiB [emitted] [minimized] (name: index) 1 related asset
asset print.bundle.js 316 bytes [emitted] [minimized] (name: print)
runtime modules 1.36 KiB 7 modules
cacheable modules 530 KiB
./src/index.js 406 bytes [built] [code generated]
./src/print.js 83 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1996 ms
我们可以看到 webpack 生成了我们的 print.bundle.js
和 index.bundle.js
文件,这些文件我们也已在 index.html
文件中指定。如果您在浏览器中打开 index.html
,您可以看到点击按钮时会发生什么。
但是,如果我们更改其中一个入口点的名称,甚至添加一个新的入口点,会发生什么呢?生成的打包文件将在构建时被重命名,但我们的 index.html
文件仍将引用旧名称。让我们使用HtmlWebpackPlugin
来解决这个问题。
首先安装插件并调整 webpack.config.js
文件
npm install --save-dev html-webpack-plugin
webpack.config.js
const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Output Management',
+ }),
+ ],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
在进行构建之前,您应该知道 HtmlWebpackPlugin
默认会生成自己的 index.html
文件,即使我们已经在 dist/
文件夹中有一个了。这意味着它将用新生成的文件替换我们的 index.html
文件。让我们看看当我们运行 npm run build
时会发生什么
...
[webpack-cli] Compilation finished
asset index.bundle.js 69.5 KiB [compared for emit] [minimized] (name: index) 1 related asset
asset print.bundle.js 316 bytes [compared for emit] [minimized] (name: print)
asset index.html 253 bytes [emitted]
runtime modules 1.36 KiB 7 modules
cacheable modules 530 KiB
./src/index.js 406 bytes [built] [code generated]
./src/print.js 83 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 2189 ms
如果您在代码编辑器中打开 index.html
,您会看到 HtmlWebpackPlugin
为您创建了一个全新的文件,并且所有打包文件都已自动添加。
如果您想了解 HtmlWebpackPlugin
提供的所有特性和选项,那么您应该在HtmlWebpackPlugin
仓库中阅读相关内容。
/dist
文件夹正如您在过去的指南和代码示例中可能已经注意到的那样,我们的 /dist
文件夹变得相当混乱。Webpack 会为您生成文件并将它们放入 /dist
文件夹中,但它不会跟踪您的项目实际使用了哪些文件。
通常,在每次构建之前清理 /dist
文件夹是一个好习惯,这样只会生成使用的文件。让我们使用output.clean
选项来处理这个问题。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
+ clean: true,
},
};
现在运行 npm run build
并检查 /dist
文件夹。如果一切顺利,您现在应该只看到构建生成的文件,而没有旧文件了!
您可能想知道 webpack 及其插件是如何“知道”正在生成哪些文件的。答案在于 webpack 用于跟踪所有模块如何映射到输出打包文件的清单。如果您有兴趣以其他方式管理 webpack 的output
,那么清单将是一个很好的起点。
可以使用WebpackManifestPlugin
将清单数据提取到 JSON 文件中以供消费。
我们不会通过一个完整的示例来展示如何在您的项目中使用此插件,但您可以阅读概念页面和缓存指南,以了解这如何与长期缓存联系起来。
现在您已经了解了如何动态地向 HTML 添加打包文件,让我们深入研究开发指南。或者,如果您想深入探讨更高级的主题,我们建议您前往代码分割指南。
如果您一直遵循这些指南,您应该对 webpack 的一些基础知识有了扎实的理解。在我们继续之前,让我们研究一下如何设置开发环境,以便让我们的生活更轻松一些。
让我们首先将mode
设置为 'development'
,并将 title
设置为 'Development'
。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
+ mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
当 webpack 打包您的源代码时,将错误和警告追溯到其原始位置可能会变得困难。例如,如果您将三个源文件(a.js
、b.js
和 c.js
)打包成一个文件(bundle.js
),并且其中一个源文件包含错误,则堆栈跟踪将指向 bundle.js
。这并不总是有用,因为您可能想知道错误究竟来自哪个源文件。
为了更容易地跟踪错误和警告,JavaScript 提供了源映射,它将您的编译代码映射回您的原始源代码。如果错误源自 b.js
,源映射会准确地告诉您这一点。
在源映射方面有许多不同的选项可用。务必查看它们,以便您可以根据需要进行配置。
在本指南中,让我们使用 inline-source-map
选项,这对于说明目的很有用(但不适用于生产环境)
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
+ devtool: 'inline-source-map',
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
现在让我们确保有可以调试的东西,所以在我们的 print.js
文件中创建一个错误
src/print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ cosnole.log('I get called from print.js!');
}
运行 npm run build
,它应该编译成这样
...
[webpack-cli] Compilation finished
asset index.bundle.js 1.38 MiB [emitted] (name: index)
asset print.bundle.js 6.25 KiB [emitted] (name: print)
asset index.html 272 bytes [emitted]
runtime modules 1.9 KiB 9 modules
cacheable modules 530 KiB
./src/index.js 406 bytes [built] [code generated]
./src/print.js 83 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 706 ms
现在在您的浏览器中打开生成的 index.html
文件。点击按钮并查看控制台中显示的错误。错误应该显示类似这样的内容
Uncaught ReferenceError: cosnole is not defined
at HTMLButtonElement.printMe (print.js:2)
我们可以看到错误还包含对文件(print.js
)和发生错误的行号(2)的引用。这很棒,因为现在我们确切地知道在哪里查找以解决问题。
每次编译代码时手动运行 npm run build
很快就会变得很麻烦。
webpack 中有几种不同的选项可以帮助您在代码更改时自动编译代码
在大多数情况下,您可能希望使用 webpack-dev-server
,但让我们探讨所有上述选项。
您可以指示 webpack “观察”依赖图中的所有文件以进行更改。如果其中一个文件更新,代码将重新编译,这样您就不必手动运行完整的构建。
让我们添加一个 npm 脚本来启动 webpack 的观察模式
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "watch": "webpack --watch",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
现在从命令行运行 npm run watch
,看看 webpack 如何编译您的代码。您会看到它不会退出命令行,因为脚本当前正在观察您的文件。
现在,当 webpack 正在观察您的文件时,让我们移除之前引入的错误
src/print.js
export default function printMe() {
- cosnole.log('I get called from print.js!');
+ console.log('I get called from print.js!');
}
现在保存您的文件并检查终端窗口。您应该会看到 webpack 自动重新编译了更改的模块!
唯一的缺点是您必须刷新浏览器才能看到更改。如果也能自动发生就更好了,所以让我们尝试 webpack-dev-server
,它会做到这一点。
webpack-dev-server
为您提供了一个基本的 Web 服务器和使用实时重载的能力。让我们设置它
npm install --save-dev webpack-dev-server
更改您的配置文件以告诉开发服务器在哪里查找文件
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
+ devServer: {
+ static: './dist',
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
这告诉 webpack-dev-server
从 dist
目录在 localhost:8080
上提供文件。
让我们也添加一个脚本来轻松运行开发服务器
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
+ "start": "webpack serve --open",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
现在我们可以从命令行运行 npm start
,我们会看到浏览器自动加载我们的页面。如果您现在更改任何源文件并保存它们,Web 服务器将在代码编译后自动重新加载。试试看吧!
webpack-dev-server
附带许多可配置选项。请查阅文档了解更多信息。
webpack-dev-middleware
是一个包装器,它将 webpack 处理的文件发射到服务器。它在 webpack-dev-server
内部使用,但也可作为一个单独的包使用,以便在需要时进行更多自定义设置。我们将看一个将 webpack-dev-middleware
与 express 服务器结合的示例。
让我们安装 express
和 webpack-dev-middleware
以便开始
npm install --save-dev express webpack-dev-middleware
现在我们需要对我们的 webpack 配置文件进行一些调整,以确保中间件正常工作
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
+ publicPath: '/',
},
};
publicPath
也将用于我们的服务器脚本中,以确保文件在 https://:3000
上正确提供。我们稍后将指定端口号。下一步是设置我们的自定义 express 服务器
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
+ |- server.js
|- /dist
|- /src
|- index.js
|- print.js
|- /node_modules
server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// Serve the files on port 3000.
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
现在添加一个 npm 脚本,让运行服务器变得更容易一些
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
"start": "webpack serve --open",
+ "server": "node server.js",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"express": "^4.17.1",
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-middleware": "^4.0.2",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
现在在您的终端中运行 npm run server
,它应该会给您一个类似这样的输出
Example app listening on port 3000!
...
<i> [webpack-dev-middleware] asset index.bundle.js 1.38 MiB [emitted] (name: index)
<i> asset print.bundle.js 6.25 KiB [emitted] (name: print)
<i> asset index.html 274 bytes [emitted]
<i> runtime modules 1.9 KiB 9 modules
<i> cacheable modules 530 KiB
<i> ./src/index.js 406 bytes [built] [code generated]
<i> ./src/print.js 83 bytes [built] [code generated]
<i> ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
<i> webpack 5.4.0 compiled successfully in 709 ms
<i> [webpack-dev-middleware] Compiled successfully.
<i> [webpack-dev-middleware] Compiling...
<i> [webpack-dev-middleware] assets by status 1.38 MiB [cached] 2 assets
<i> cached modules 530 KiB (javascript) 1.9 KiB (runtime) [cached] 12 modules
<i> webpack 5.4.0 compiled successfully in 19 ms
<i> [webpack-dev-middleware] Compiled successfully.
现在打开您的浏览器并访问 https://:3000
。您应该会看到您的 webpack 应用程序正在运行并正常工作!
当使用代码的自动编译时,您在保存文件时可能会遇到问题。一些编辑器具有“安全写入”功能,这可能会干扰重新编译。
要在一些常用编辑器中禁用此功能,请参阅以下列表
atomic_save: 'false'
添加到您的用户首选项中。Preferences > Appearance & Behavior > System Settings
中取消勾选“使用安全写入”。:set backupcopy=yes
添加到您的设置中。现在您已经学会了如何自动编译代码并运行开发服务器,您可以查看下一篇指南,该指南将涵盖代码分割。
代码分割是 webpack 最引人注目的特性之一。此功能允许您将代码分割成各种打包文件,然后可以按需或并行加载。它可用于实现更小的打包文件并控制资源加载优先级,如果使用得当,可以对加载时间产生重大影响。
有三种通用的代码分割方法可用
entry
配置手动分割代码。SplitChunksPlugin
来去重和分割代码块。这是迄今为止最简单、最直观的代码分割方式。然而,它更手动,并且有一些我们将会遇到的陷阱。让我们看看如何将另一个模块从主打包文件中分割出来
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
another-module.js
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ' '));
webpack.config.js
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
这将产生以下构建结果
...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms
如前所述,这种方法存在一些陷阱
这两点中的第一点对我们的示例来说绝对是个问题,因为 lodash
也被导入到 ./src/index.js
中,因此会在两个打包文件中重复。让我们在下一节中消除这种重复。
dependOn
选项允许在代码块之间共享模块
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
如果要在单个 HTML 页面上使用多个入口点,也需要 optimization.runtimeChunk: 'single'
,否则我们可能会遇到此处描述的问题。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
以下是构建结果
...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms
如您所见,除了 shared.bundle.js
、index.bundle.js
和 another.bundle.js
之外,还生成了一个 runtime.bundle.js
文件。
尽管 webpack 允许每页使用多个入口点,但应尽可能避免,而应优先使用带有多个导入的入口点:entry: { page: ['./analytics', './app'] }
。这样可以在使用 async
脚本标签时获得更好的优化和一致的执行顺序。
SplitChunksPlugin
允许我们将公共依赖项提取到现有入口代码块或全新的代码块中。让我们用它来消除上一个示例中 lodash
依赖项的重复
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
配置optimization.splitChunks
选项后,我们现在应该看到重复的依赖项已从我们的 index.bundle.js
和 another.bundle.js
中移除。该插件应该注意到我们已将 lodash
分离到一个单独的代码块中,并从我们的主打包文件中移除冗余部分。但是,重要的是要注意,公共依赖项只有在满足 webpack 指定的大小阈值时才会被提取到单独的代码块中。
让我们运行 npm run build
看看是否成功了
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms
以下是社区提供的一些其他用于代码分割的有用插件和加载器
mini-css-extract-plugin
:有助于将 CSS 从主应用程序中分割出来。webpack 在动态代码分割方面支持两种类似的技术。第一种也是推荐的方法是使用符合ECMAScript 动态导入提案的import()
语法。遗留的、webpack 特有的方法是使用require.ensure
。让我们尝试使用这两种方法中的第一种...
在我们开始之前,让我们从上面示例的配置中移除多余的entry
和optimization.splitChunks
,因为它们在此次演示中不需要
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
- another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- optimization: {
- splitChunks: {
- chunks: 'all',
- },
- },
};
我们还将更新我们的项目以移除现在未使用的文件
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
现在,我们不再静态导入 lodash
,而是使用动态导入来分离一个代码块
src/index.js
-import _ from 'lodash';
-
-function component() {
+function getComponent() {
- const element = document.createElement('div');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import('lodash')
+ .then(({ default: _ }) => {
+ const element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- return element;
+ return element;
+ })
+ .catch((error) => 'An error occurred while loading the component');
}
-document.body.appendChild(component());
+getComponent().then((component) => {
+ document.body.appendChild(component);
+});
我们需要 default
的原因是,自 webpack 4 以来,当导入 CommonJS 模块时,import
不再解析为 module.exports
的值,而是为 CommonJS 模块创建一个人工命名空间对象。有关其原因的更多信息,请阅读webpack 4: import() and CommonJs。
让我们运行 webpack,看看 lodash
被分离到单独的打包文件中
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
./src/index.js 434 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms
由于 import()
返回一个 Promise,因此它可以与异步函数一起使用。以下是如何简化代码的方法
src/index.js
-function getComponent() {
+async function getComponent() {
+ const element = document.createElement('div');
+ const { default: _ } = await import('lodash');
- return import('lodash')
- .then(({ default: _ }) => {
- const element = document.createElement('div');
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- return element;
- })
- .catch((error) => 'An error occurred while loading the component');
+ return element;
}
getComponent().then((component) => {
document.body.appendChild(component);
});
Webpack 4.6.0+ 增加了对预取和预加载的支持。
在声明导入时使用这些内联指令,webpack 可以输出“资源提示”,告诉浏览器
一个例子是有一个 HomePage
组件,它渲染一个 LoginButton
组件,然后点击后按需加载一个 LoginModal
组件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
这将导致 <link rel="prefetch" href="login-modal-chunk.js">
被添加到页面的头部,这将指示浏览器在空闲时预取 login-modal-chunk.js
文件。
预加载指令与预取相比有许多不同之处
一个例子是有一个 Component
,它总是依赖于一个应该在单独代码块中的大型库。
让我们想象一个 ChartComponent
组件,它需要一个庞大的 ChartingLibrary
。它在渲染时显示一个 LoadingIndicator
,并立即按需导入 ChartingLibrary
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
当请求使用 ChartComponent
的页面时,charting-library-chunk
也通过 <link rel="preload">
被请求。假设页面代码块更小且完成更快,页面将显示 LoadingIndicator
,直到已请求的 charting-library-chunk
完成。这将稍微提高加载时间,因为它只需要一次往返,而不是两次。特别是在高延迟环境中。
有时您需要对预加载进行自己的控制。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下会很有用。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// Do something with the error.
// For example, we can retry the request in case of any net error
});
如果脚本加载在 webpack 自身开始加载该脚本之前失败(如果该脚本不在页面上,webpack 会创建一个脚本标签来加载其代码),那么 catch 处理程序将不会启动,直到chunkLoadTimeout
过期。这种行为可能出乎意料。但这是可以解释的——webpack 不能抛出任何错误,因为 webpack 不知道脚本失败了。webpack 会在错误发生后立即向脚本添加 onerror 处理程序。
为了防止此类问题,您可以添加自己的 onerror 处理程序,它会在发生任何错误时移除脚本
<script
src="https://example.com/dist/dynamicComponent.js"
async
onerror="this.remove()"
></script>
在这种情况下,出错的脚本将被移除。Webpack 将创建自己的脚本,并且任何错误都将立即处理,而不会出现任何超时。
一旦您开始分割代码,分析输出来检查模块的去向会很有用。官方分析工具是一个很好的起点。还有一些其他社区支持的选项
请参阅懒加载以获取 import()
如何在实际应用程序中使用的更具体示例,并参阅缓存以了解如何更有效地分割代码。
因此,我们正在使用 webpack 打包我们的模块化应用程序,生成一个可部署的 /dist
目录。一旦 /dist
的内容部署到服务器,客户端(通常是浏览器)将访问该服务器以获取站点及其资源。最后一步可能很耗时,这就是浏览器使用一种称为缓存的技术的原因。这使得站点加载更快,并减少了不必要的网络流量。然而,当您需要获取新代码时,它也可能导致麻烦。
本指南侧重于确保 webpack 编译生成的文件可以保持缓存,除非其内容已更改所需的配置。
我们可以使用 output.filename
替换设置来定义输出文件的名称。Webpack 提供了一种使用括号字符串(称为替换)来模板化文件名的方法。[contenthash]
替换将根据资产的内容添加一个唯一的哈希值。当资产的内容更改时,[contenthash]
也会更改。
让我们使用入门中的示例和输出管理中的插件来设置我们的项目,这样我们就不必手动维护 index.html
文件了
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- /node_modules
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Caching',
}),
],
output: {
- filename: 'bundle.js',
+ filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
使用此配置运行我们的构建脚本 npm run build
应该会产生以下输出
...
Asset Size Chunks Chunk Names
main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
如您所见,打包文件的名称现在反映了其内容(通过哈希值)。如果我们再次运行构建而不进行任何更改,我们期望文件名保持不变。然而,如果我们再次运行它,我们可能会发现情况并非如此
...
Asset Size Chunks Chunk Names
main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
这是因为 webpack 在入口代码块中包含了一些样板代码,特别是运行时和清单。
正如我们在代码分割中了解到的,SplitChunksPlugin
可用于将模块分割成单独的打包文件。Webpack 提供了一项优化功能,可以使用optimization.runtimeChunk
选项将运行时代码分割到单独的代码块中。将其设置为 single
以创建所有代码块的单个运行时打包文件
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
让我们再次运行构建,看看提取的运行时打包文件
Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main
index.html 275 bytes [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
+ 1 hidden module
提取第三方库(例如 lodash
或 react
)到单独的 vendor 代码块也是一个好习惯,因为它们比我们的本地源代码更不容易更改。这一步将允许客户端从服务器请求更少的内容以保持最新。这可以通过使用SplitChunksPlugin
的示例 2 中演示的cacheGroups
选项来完成。让我们添加带有以下参数的 optimization.splitChunks
和 cacheGroups
,然后进行构建
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
runtimeChunk: 'single',
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ },
+ },
},
};
让我们再次运行构建,看看我们新的 vendor 打包文件
...
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
vendors.a42c3ca0d742766d7a28.js 69.4 KiB 1 [emitted] vendors
main.abf44fedb7d11d4312d7.js 240 bytes 2 [emitted] main
index.html 353 bytes [emitted]
...
我们现在可以看到我们的 main 打包文件不包含 node_modules
目录中的 vendor 代码,并且大小已减小到 240 字节!
让我们向项目添加另一个模块 print.js
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
print.js
+ export default function print(text) {
+ console.log(text);
+ };
src/index.js
import _ from 'lodash';
+ import Print from './print';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
运行另一次构建,我们期望只有我们 main 打包文件的哈希值会改变,然而...
...
Asset Size Chunks Chunk Names
runtime.1400d5af64fc1b7b3a45.js 5.85 kB 0 [emitted] runtime
vendor.a7561fb0e9a071baadb9.js 541 kB 1 [emitted] [big] vendor
main.b746e3eb72875af2caa9.js 1.22 kB 2 [emitted] main
index.html 352 bytes [emitted]
...
... 我们可以看到这三个都改变了。这是因为默认情况下每个module.id
都会根据解析顺序递增。这意味着当解析顺序改变时,ID 也会改变。回顾一下
module.id
改变了。第一个和最后一个是预期的,我们想要修复的是 vendor 哈希值。让我们使用带有 'deterministic'
选项的optimization.moduleIds
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
+ moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
现在,无论添加任何新的本地依赖项,我们的 vendor 哈希值在构建之间都应该保持一致
...
Asset Size Chunks Chunk Names
main.216e852f60c8829c2289.js 340 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...
最后,让我们修改 src/index.js
以暂时移除那个额外的依赖项
src/index.js
import _ from 'lodash';
- import Print from './print';
+ // import Print from './print';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.onclick = Print.bind(null, 'Hello webpack!');
+ // element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
最后再次运行我们的构建
...
Asset Size Chunks Chunk Names
main.ad717f2466ce655fff5c.js 274 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...
我们可以看到两次构建都在 vendor 打包文件的文件名中产生了 55e79e5927a639d21a1b
。
缓存可能很复杂,但对应用程序或站点用户的益处使其值得付出努力。请参阅下面的“延伸阅读”部分了解更多信息。
除了应用程序,webpack 还可以用于打包 JavaScript 库。以下指南旨在帮助库作者简化他们的打包策略。
让我们假设我们正在编写一个小型库 webpack-numbers
,它允许用户将数字 1 到 5 从数字表示转换为文本表示,反之亦然,例如 2 到 'two'。
基本的项目结构将如下所示
项目
+ |- webpack.config.js
+ |- package.json
+ |- /src
+ |- index.js
+ |- ref.json
使用 npm 初始化项目,然后安装 webpack
、webpack-cli
和 lodash
npm init -y
npm install --save-dev webpack webpack-cli lodash
我们将 lodash
安装为 devDependencies
而不是 dependencies
,因为我们不想将其打包到我们的库中,否则我们的库很容易变得臃肿。
src/ref.json
[
{
"num": 1,
"word": "One"
},
{
"num": 2,
"word": "Two"
},
{
"num": 3,
"word": "Three"
},
{
"num": 4,
"word": "Four"
},
{
"num": 5,
"word": "Five"
},
{
"num": 0,
"word": "Zero"
}
]
src/index.js
import _ from 'lodash';
import numRef from './ref.json';
export function numToWord(num) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.num === num ? ref.word : accum;
},
''
);
}
export function wordToNum(word) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.word === word && word.toLowerCase() ? ref.num : accum;
},
-1
);
}
让我们从这个基本的 webpack 配置开始
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
},
};
在上面的示例中,我们告诉 webpack 将 src/index.js
打包到 dist/webpack-numbers.js
中。
到目前为止,一切都应该与打包应用程序相同,接下来就是不同的部分——我们需要通过output.library
选项从入口点暴露导出。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
+ library: "webpackNumbers",
},
};
我们将入口点暴露为 webpackNumbers
,以便用户可以通过脚本标签使用它
<script src="https://example.org/webpack-numbers.js"></script>
<script>
window.webpackNumbers.wordToNum('Five');
</script>
然而,它只在通过脚本标签引用时才有效,不能在 CommonJS、AMD、Node.js 等其他环境中使用。
作为库作者,我们希望它能在不同环境中兼容,即用户应该能够以下面列出的多种方式使用打包后的库
CommonJS 模块 require:
const webpackNumbers = require('webpack-numbers');
// ...
webpackNumbers.wordToNum('Two');
AMD 模块 require:
require(['webpackNumbers'], function (webpackNumbers) {
// ...
webpackNumbers.wordToNum('Two');
});
脚本标签:
<!DOCTYPE html>
<html>
...
<script src="https://example.org/webpack-numbers.js"></script>
<script>
// ...
// Global variable
webpackNumbers.wordToNum('Five');
// Property in the window object
window.webpackNumbers.wordToNum('Five');
// ...
</script>
</html>
让我们将 output.library
选项的 type
更新为'umd'
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
- library: 'webpackNumbers',
+ globalObject: 'this',
+ library: {
+ name: 'webpackNumbers',
+ type: 'umd',
+ },
},
};
现在 webpack 将打包一个可在 CommonJS、AMD 和脚本标签环境中工作的库。
现在,如果您运行 npx webpack
,您会发现创建了一个相当大的打包文件。如果您检查该文件,您会看到 lodash
已与您的代码一起打包。在这种情况下,我们更愿意将 lodash
视为一个对等依赖项。这意味着消费者应该已经安装了 lodash
。因此,您会希望将此外部库的控制权交给您的库的消费者。
这可以通过使用externals
配置来完成
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: {
name: "webpackNumbers",
type: "umd"
},
},
+ externals: {
+ lodash: {
+ commonjs: 'lodash',
+ commonjs2: 'lodash',
+ amd: 'lodash',
+ root: '_',
+ },
+ },
};
这意味着您的库期望消费者环境中存在名为 lodash
的依赖项。
对于使用依赖项中多个文件的库
import A from 'library/one';
import B from 'library/two';
// ...
您无法通过在 externals
中指定 library
将它们从打包文件中排除。您需要逐个排除它们,或者使用正则表达式。
module.exports = {
//...
externals: [
'library/one',
'library/two',
// Everything that starts with "library/"
/^library\/.+$/,
],
};
按照生产指南中提到的步骤优化您的生产输出。我们还将生成打包文件的路径添加到 package.json
中的包的 main
字段中
package.json
{
...
"main": "dist/webpack-numbers.js",
...
}
或者,按照本指南将其添加为标准模块
{
...
"module": "src/index.js",
...
}
键 main
指的是package.json
中的标准,而 module
指的是一个提案,旨在允许 JavaScript 生态系统升级使用 ES2015 模块而不破坏向后兼容性。
现在您可以将其发布为 npm 包,并在unpkg.com 上找到它,以便分发给您的用户。
为了在您的 webpack.config.js
中区分开发构建和生产构建,您可以使用环境变量。
webpack 命令行环境变量选项 --env
允许您根据需要传入任意数量的环境变量。环境变量将在您的 webpack.config.js
中可访问。例如,--env production
或 --env goal=local
。
npx webpack --env goal=local --env production --progress
您必须对 webpack 配置进行一项更改。通常,module.exports
指向配置对象。要使用 env
变量,您必须将 module.exports
转换为函数
webpack.config.js
const path = require('path');
module.exports = (env) => {
// Use env.<YOUR VARIABLE> here:
console.log('Goal: ', env.goal); // 'local'
console.log('Production: ', env.production); // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
};
本指南包含一些提高构建/编译性能的有用提示。
无论您是在开发环境还是生产环境中运行构建脚本,以下最佳实践都应该有所帮助。
使用最新的 webpack 版本。我们一直在进行性能改进。最新推荐的 webpack 版本是
保持 Node.js 的最新状态也有助于提高性能。此外,保持您的包管理器(例如 npm
或 yarn
)的最新状态也有助于提高性能。新版本创建更有效的模块树并提高解析速度。
将加载器应用于所需的最少模块。而不是
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
},
],
},
};
使用 include
字段,只对实际需要通过加载器转换的模块应用加载器
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
每个额外的加载器/插件都有启动时间。尽量使用最少的工具。
以下步骤可以提高解析速度
resolve.modules
、resolve.extensions
、resolve.mainFiles
、resolve.descriptionFiles
中的项数,因为它们会增加文件系统调用的数量。npm link
或 yarn link
),请设置 resolve.symlinks: false
。resolve.cacheWithContext: false
。使用 DllPlugin
将不经常更改的代码移动到单独的编译中。这将提高应用程序的编译速度,尽管它会增加构建过程的复杂性。
减小编译的总大小以提高构建性能。尝试保持代码块较小。
SplitChunksPlugin
。SplitChunksPlugin
。thread-loader
可用于将昂贵的加载器分流到工作池。
在 webpack 配置中使用cache
选项。在 package.json
中的 "postinstall"
时清除缓存目录。
对其进行分析,以免在此处引入性能问题。
可以通过从 webpack 配置中移除 ProgressPlugin
来缩短构建时间。请记住,ProgressPlugin
对于快速构建可能不会提供太多价值,因此请确保您正在利用使用它的好处。
以下步骤在开发中特别有用。
使用 webpack 的观察模式。不要使用其他工具来观察文件并调用 webpack。内置的观察模式将跟踪时间戳并将此信息传递给编译,以进行缓存失效。
在某些设置中,观察会回退到轮询模式。观察大量文件时,这可能会导致大量的 CPU 负载。在这种情况下,您可以使用 watchOptions.poll
增加轮询间隔。
以下实用程序通过在内存中编译和提供资源而不是写入磁盘来提高性能
webpack-dev-server
webpack-hot-middleware
webpack-dev-middleware
Webpack 4 默认情况下会输出大量数据,通过其 stats.toJson()
。除非在增量步骤中必要,否则应避免检索 stats
对象的某些部分。webpack-dev-server
在 v3.1.3 之后包含了一项实质性的性能修复,以最小化每次增量构建步骤从 stats
对象中检索的数据量。
请注意不同 devtool
设置之间的性能差异。
'eval'
具有最佳性能,但不支持转译后的代码。cheap-source-map
变体性能更好。eval-source-map
变体。某些实用程序、插件和加载器仅在构建生产环境时才有意义。例如,在开发过程中使用 TerserPlugin
最小化和混淆代码通常没有意义。这些工具通常应在开发中排除
TerserPlugin
[fullhash]
/[chunkhash]
/[contenthash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
Webpack 只向文件系统发出更新的代码块。对于某些配置选项(HMR、output.chunkFilename
中的 [name]
/[chunkhash]
/[contenthash]
、[fullhash]
),除了更改的代码块之外,入口代码块也会失效。
确保入口代码块的生成成本低廉,方法是保持其体积小。以下配置为运行时代码创建了一个额外的代码块,因此生成成本低廉
module.exports = {
// ...
optimization: {
runtimeChunk: true,
},
};
Webpack 会进行额外的算法工作以优化输出的大小和加载性能。这些优化对于较小的代码库来说性能良好,但在较大的代码库中可能会代价高昂
module.exports = {
// ...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
};
Webpack 能够生成输出打包文件中的路径信息。但是,这会对打包数千个模块的项目造成垃圾回收压力。在 options.output.pathinfo
设置中将其关闭
module.exports = {
// ...
output: {
pathinfo: false,
},
};
在 Node.js 8.9.10 - 9.11.1 版本中,ES2015 Map 和 Set 实现存在性能回退。Webpack 大量使用这些数据结构,因此此回退影响编译时间。
更早和更晚的 Node.js 版本不受影响。
为了在使用 ts-loader
时缩短构建时间,请使用 transpileOnly
加载器选项。此选项本身会关闭类型检查。要再次获得类型检查,请使用ForkTsCheckerWebpackPlugin
。这通过将类型检查和 ESLint linting 移动到单独的进程来加速 TypeScript 类型检查和 ESLint linting。
module.exports = {
// ...
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
};
以下步骤在生产中特别有用。
源映射非常昂贵。您真的需要它们吗?
以下工具存在某些可能降低构建性能的问题
fork-ts-checker-webpack-plugin
在单独的进程中进行类型检查。happyPackMode: true
/ transpileOnly: true
模式下使用 ts-loader
。node-sass
有一个会阻塞 Node.js 线程池中线程的 bug。当与 thread-loader
一起使用时,请设置 workerParallelJobs: 2
。Webpack 能够为其加载的所有脚本添加一个 nonce
。要激活此功能,请设置一个 __webpack_nonce__
变量并将其包含在您的入口脚本中。然后将为每个唯一的页面视图生成并提供一个唯一的基于哈希的 nonce
(这就是为什么 __webpack_nonce__
在入口文件中指定而不是在配置中指定的原因)。请注意,__webpack_nonce__
应该始终是一个 base64 编码的字符串。
在入口文件中
// ...
__webpack_nonce__ = 'c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=';
// ...
请注意,CSP 默认不启用。需要随文档发送相应的 Content-Security-Policy
头部或 <meta http-equiv="Content-Security-Policy" ...>
meta 标签,以指示浏览器启用 CSP。以下是一个包含 CDN 白名单 URL 的 CSP 头部示例:
Content-Security-Policy: default-src 'self'; script-src 'self'
https://trusted.cdn.com;
有关 CSP 和 nonce
属性的更多信息,请参阅本页面底部的延伸阅读部分。
Webpack 还能够使用受信任类型加载动态构建的脚本,以遵守 CSP require-trusted-types-for
指令限制。请参阅 output.trustedTypes
配置选项。
如果您有一个更高级的项目,并使用 Vagrant 在虚拟机中运行开发环境,您通常也会希望在虚拟机中运行 webpack。
首先,确保 Vagrantfile
有一个静态 IP;
Vagrant.configure("2") do |config|
config.vm.network :private_network, ip: "10.10.10.61"
end
接下来,在您的项目中安装 webpack
, webpack-cli
, @webpack-cli/serve
和 webpack-dev-server
;
npm install --save-dev webpack webpack-cli @webpack-cli/serve webpack-dev-server
确保有一个 webpack.config.js
文件。如果您还没有,请使用此作为最小示例来开始。
module.exports = {
context: __dirname,
entry: './app.js',
};
并创建一个 index.html
文件。脚本标签应指向您的打包文件。如果配置中未指定 output.filename
,则默认为 bundle.js
。
<!DOCTYPE html>
<html>
<head>
<script src="/bundle.js" charset="utf-8"></script>
</head>
<body>
<h2>Hey!</h2>
</body>
</html>
请注意,您还需要创建一个 app.js
文件。
现在,让我们运行服务器
webpack serve --host 0.0.0.0 --client-web-socket-url ws://10.10.10.61:8080/ws --watch-options-poll
默认情况下,服务器只能从 localhost 访问。我们将从我们的主机 PC 访问它,所以我们需要更改 --host
以允许这样做。
webpack-dev-server
会在您的打包文件中包含一个脚本,该脚本连接到 WebSocket,以便在您的任何文件发生更改时重新加载。--client-web-socket-url
标志确保脚本知道去哪里查找 WebSocket。服务器默认使用端口 8080
,所以我们在这里也应该指定它。
--watch-options-poll
确保 webpack 可以检测到您文件中的更改。默认情况下,webpack 监听文件系统触发的事件,但 VirtualBox 在这方面存在许多问题。
服务器现在应该可以在 http://10.10.10.61:8080
访问。如果您更改了 app.js
,它应该会实时重新加载。
为了模拟更接近生产的环境,也可以使用 nginx 代理 webpack-dev-server
。
在您的 nginx 配置文件中,添加以下内容:
server {
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
error_page 502 @start-webpack-dev-server;
}
location @start-webpack-dev-server {
default_type text/plain;
return 502 "Please start the webpack-dev-server first.";
}
}
proxy_set_header
行很重要,因为它们允许 WebSocket 正常工作。
然后启动 webpack-dev-server
的命令可以更改为:
webpack serve --client-web-socket-url ws://10.10.10.61:8080/ws --watch-options-poll
这使得服务器只能在 127.0.0.1
上访问,这很好,因为 nginx 会负责使其在您的主机 PC 上可用。
我们使 Vagrant 盒子可以通过静态 IP 访问,然后使 webpack-dev-server
公开可访问,以便可以通过浏览器访问。然后我们解决了 VirtualBox 不发送文件系统事件导致服务器在文件更改时无法重新加载的常见问题。
ES6 模块
CommonJS
AMD
如果您的请求包含表达式,则会创建一个上下文,因此在编译时无法知道确切的模块。
例如,假设我们有以下包含 .ejs
文件的文件夹结构:
example_directory
│
└───template
│ │ table.ejs
│ │ table-row.ejs
│ │
│ └───directory
│ │ another.ejs
当以下 require()
调用被评估时:
require('./template/' + name + '.ejs');
Webpack 解析 require()
调用并提取一些信息:
Directory: ./template
Regular expression: /^.*\.ejs$/
上下文模块
生成一个上下文模块。它包含对该目录中所有可以通过与正则表达式匹配的请求进行 require 的模块的引用。上下文模块包含一个将请求转换为模块 ID 的映射。
示例映射
{
"./table.ejs": 42,
"./table-row.ejs": 43,
"./directory/another.ejs": 44
}
上下文模块还包含一些用于访问映射的运行时逻辑。
这意味着支持动态 require,但会导致所有匹配的模块都包含在 bundle 中。
您可以使用 require.context()
函数创建自己的上下文。
它允许您传入要搜索的目录、一个指示是否也应搜索子目录的标志以及一个用于匹配文件的正则表达式。
Webpack 在构建时解析代码中的 require.context()
。
语法如下:
require.context(
directory,
(useSubdirectories = true),
(regExp = /^\.\/.*$/),
(mode = 'sync')
);
示例
require.context('./test', false, /\.test\.js$/);
// a context with files from the test directory that can be required with a request ending with `.test.js`.
require.context('../', true, /\.stories\.js$/);
// a context with all files in the parent folder and descending folders ending with `.stories.js`.
上下文模块导出一个(require)函数,该函数接受一个参数:请求。
导出的函数有 3 个属性:resolve
、keys
、id
。
resolve
是一个函数,返回解析请求的模块 ID。keys
是一个函数,返回上下文模块可以处理的所有可能请求的数组。如果您想 require 目录中或匹配模式的所有文件,这会很有用,例如:
function importAll(r) {
r.keys().forEach(r);
}
importAll(require.context('../components/', true, /\.js$/));
const cache = {};
function importAll(r) {
r.keys().forEach((key) => (cache[key] = r(key)));
}
importAll(require.context('../components/', true, /\.js$/));
// At build-time cache will be populated with all required modules.
id
是上下文模块的模块 ID。这对于 module.hot.accept
可能有用。本指南介绍了安装 webpack 的各种方法。
在开始之前,请确保您安装了最新版本的 Node.js。当前的长期支持 (LTS) 版本是一个理想的起点。您可能会遇到各种旧版本的问题,因为它们可能缺少 webpack 和/或其相关包所需的功能。
最新的 webpack 发布版本是
要安装最新版本或特定版本,请运行以下命令之一:
npm install --save-dev webpack
# or specific version
npm install --save-dev webpack@<version>
如果您使用的是 webpack v4 或更高版本并希望从命令行调用 webpack
,您还需要安装 CLI。
npm install --save-dev webpack-cli
我们建议大多数项目都采用本地安装。当引入重大更改时,这使得单独升级项目变得更容易。通常,webpack 通过一个或多个 npm 脚本运行,这些脚本将在本地 node_modules
目录中查找 webpack 安装。
"scripts": {
"build": "webpack --config webpack.config.js"
}
以下 NPM 安装将使 webpack
全局可用:
npm install --global webpack
如果您热衷于使用 webpack 提供的最新功能,您可以使用以下命令安装 Beta 版本,甚至直接从 webpack 仓库安装:
npm install --save-dev webpack@next
# or a specific tag/branch
npm install --save-dev webpack/webpack#<tagname/branchname>
模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需完全刷新。本页面重点介绍实现,而概念页面提供了有关其工作原理和实用性的更多详细信息。
此功能对于提高生产力非常有用。我们只需更新我们的 webpack-dev-server 配置,并使用 webpack 内置的 HMR 插件。我们还将删除 print.js
的入口点,因为它现在将被 index.js
模块使用。
自 webpack-dev-server
v4.0.0 起,模块热替换默认启用。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
- print: './src/print.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
+ hot: true,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
您也可以为 HMR 提供手动入口点:
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const webpack = require("webpack");
module.exports = {
entry: {
app: './src/index.js',
- print: './src/print.js',
+ // Runtime code for hot module replacement
+ hot: 'webpack/hot/dev-server.js',
+ // Dev server client for web socket transport, hot and live reload logic
+ client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
+ // Dev server client for web socket transport, hot and live reload logic
+ hot: false,
+ client: false,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
+ // Plugin for hot module replacement
+ new webpack.HotModuleReplacementPlugin(),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
现在让我们更新 index.js
文件,以便当检测到 print.js
内部发生更改时,我们告诉 webpack 接受更新的模块。
index.js
import _ from 'lodash';
import printMe from './print.js';
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
+
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ console.log('Accepting the updated printMe module!');
+ printMe();
+ })
+ }
开始更改 print.js
中的 console.log
语句,您应该在浏览器控制台中看到以下输出(暂时不用担心 button.onclick = printMe
的输出,我们稍后也会更新那部分)。
print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ console.log('Updating print.js...');
}
控制台
[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR] - 20
将 Webpack Dev Server 与 Node.js API 一起使用时,不要将开发服务器选项放在 webpack 配置对象上。相反,在创建时将其作为第二个参数传递。例如:
new WebpackDevServer(options, compiler)
要启用 HMR,您还需要修改 webpack 配置对象以包含 HMR 入口点。以下是一个小示例,展示了它可能的样子:
dev-server.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const webpackDevServer = require('webpack-dev-server');
const config = {
mode: 'development',
entry: [
// Runtime code for hot module replacement
'webpack/hot/dev-server.js',
// Dev server client for web socket transport, hot and live reload logic
'webpack-dev-server/client/index.js?hot=true&live-reload=true',
// Your entry
'./src/index.js',
],
devtool: 'inline-source-map',
plugins: [
// Plugin for hot module replacement
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
const compiler = webpack(config);
// `hot` and `client` options are disabled because we added them manually
const server = new webpackDevServer({ hot: false, client: false }, compiler);
(async () => {
await server.start();
console.log('dev server is running');
})();
请参阅 webpack-dev-server
Node.js API 的完整文档。
模块热替换可能很棘手。为了说明这一点,让我们回到我们正在运行的示例。如果您继续点击示例页面上的按钮,您会发现控制台仍然打印旧的 printMe
函数。
发生这种情况是因为按钮的 onclick
事件处理程序仍然绑定到原始的 printMe
函数。
为了让它与 HMR 一起工作,我们需要使用 module.hot.accept
将该绑定更新到新的 printMe
函数:
index.js
import _ from 'lodash';
import printMe from './print.js';
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe; // onclick event is bind to the original printMe function
element.appendChild(btn);
return element;
}
- document.body.appendChild(component());
+ let element = component(); // Store the element to re-render on print.js changes
+ document.body.appendChild(element);
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
- printMe();
+ document.body.removeChild(element);
+ element = component(); // Re-render the "component" to update the click handler
+ document.body.appendChild(element);
})
}
这只是一个例子,但还有许多其他例子很容易让人困惑。幸运的是,有很多加载器(下面提到了一些)可以让模块热替换变得容易得多。
借助 style-loader
,CSS 的模块热替换实际上非常简单。此加载器在幕后使用 module.hot.accept
,在 CSS 依赖项更新时修补 <style>
标签。
首先,让我们使用以下命令安装这两个加载器:
npm install --save-dev style-loader css-loader
现在让我们更新配置文件以使用加载器。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
hot: true,
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
通过将样式表导入模块来热加载样式表
项目
webpack-demo
| - package.json
| - webpack.config.js
| - /dist
| - bundle.js
| - /src
| - index.js
| - print.js
+ | - styles.css
styles.css
body {
background: blue;
}
index.js
import _ from 'lodash';
import printMe from './print.js';
+ import './styles.css';
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe; // onclick event is bind to the original printMe function
element.appendChild(btn);
return element;
}
let element = component();
document.body.appendChild(element);
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
document.body.removeChild(element);
element = component(); // Re-render the "component" to update the click handler
document.body.appendChild(element);
})
}
将 body
上的样式更改为 background: red;
,您应该立即看到页面背景颜色更改,而无需完全刷新。
styles.css
body {
- background: blue;
+ background: red;
}
社区中还有许多其他加载器和示例,可以使 HMR 与各种框架和库顺利交互...
--hmr
标志添加到您的 ng serve
命令中。摇树优化(Tree shaking)是 JavaScript 语境中常用的一个术语,用于指代消除死代码。它依赖于 ES2015 模块语法(即 import
和 export
)的静态结构。这个名称和概念由 ES2015 模块打包器 rollup 推广开来。
webpack 2 版本内置了对 ES2015 模块(又名 harmony modules)以及未使用的模块导出检测的支持。新的 webpack 4 版本扩展了此功能,可以通过 package.json
的 "sideEffects"
属性向编译器提供提示,以指示项目中的哪些文件是“纯粹的”,因此在不使用时可以安全地删除。
让我们向项目中添加一个新的工具文件 src/math.js
,它导出两个函数:
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
+ |- math.js
|- /node_modules
src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
将 mode
配置选项设置为 development,以确保打包文件未被压缩。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ mode: 'development',
+ optimization: {
+ usedExports: true,
+ },
};
有了这个,让我们更新我们的入口脚本,以利用这些新方法之一并为简单起见删除 lodash
:
src/index.js
- import _ from 'lodash';
+ import { cube } from './math.js';
function component() {
- const element = document.createElement('div');
+ const element = document.createElement('pre');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = [
+ 'Hello webpack!',
+ '5 cubed is equal to ' + cube(5)
+ ].join('\n\n');
return element;
}
document.body.appendChild(component());
请注意,我们没有 import
src/math.js
模块中的 square
方法。该函数被称为“死代码”,意味着一个未使用的 export
应该被删除。现在让我们运行 npm 脚本 npm run build
,并检查输出的打包文件:
dist/bundle.js (大约第 90 - 100 行)
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
请注意上面 unused harmony export square
的注释。如果您查看下面的代码,您会发现 square
没有被导入,但是,它仍然包含在打包文件中。我们将在下一节修复这个问题。
在一个 100% ESM 模块的世界里,识别副作用是直接了当的。然而,我们还没有到那一步,所以在此期间,有必要向 webpack 的编译器提供关于代码“纯洁性”的提示。
实现这一点的方式是 "sideEffects"
package.json 属性。
{
"name": "your-project",
"sideEffects": false
}
上述所有代码都不包含副作用,因此我们可以将该属性标记为 false
,以告知 webpack 可以安全地修剪未使用的导出。
如果您的代码确实有一些副作用,则可以改为提供一个数组:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
该数组接受针对相关文件的简单 glob 模式。它内部使用 glob-to-regexp(支持:*
, **
, {a,b}
, [a-z]
)。像 *.css
这样不包含 /
的模式将被视为 **/*.css
。
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
最后,"sideEffects"
也可以通过 module.rules
配置选项进行设置。
sideEffects
sideEffects
和 usedExports
(更广为人知的名称是摇树优化)优化是两回事。
sideEffects
效率更高,因为它允许跳过整个模块/文件和完整的子树。
usedExports
依赖于 terser 来检测语句中的副作用。这在 JavaScript 中是一项困难的任务,并且不如直接使用 sideEffects
标志有效。它也无法跳过子树/依赖项,因为规范规定副作用需要被评估。虽然导出函数运行良好,但 React 的高阶组件 (HOC) 在这方面存在问题。
让我们举个例子:
import { Button } from '@shopify/polaris';
预打包版本看起来是这样的:
import hoistStatics from 'hoist-non-react-statics';
function Button(_ref) {
// ...
}
function merge() {
var _final = {};
for (
var _len = arguments.length, objs = new Array(_len), _key = 0;
_key < _len;
_key++
) {
objs[_key] = arguments[_key];
}
for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
var obj = _objs[_i];
mergeRecursively(_final, obj);
}
return _final;
}
function withAppProvider() {
return function addProvider(WrappedComponent) {
var WithProvider =
/*#__PURE__*/
(function (_React$Component) {
// ...
return WithProvider;
})(Component);
WithProvider.contextTypes = WrappedComponent.contextTypes
? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
return FinalComponent;
};
}
var Button$1 = withAppProvider()(Button);
export {
// ...,
Button$1,
};
当 Button
未使用时,您可以有效地删除 export { Button$1 };
,这样就只剩下所有剩余的代码。那么问题是“这段代码是否有任何副作用,或者可以安全地删除它?”。很难说,尤其是因为这行 withAppProvider()(Button)
。withAppProvider
被调用,并且返回值也被调用。调用 merge
或 hoistStatics
是否有副作用?在分配 WithProvider.contextTypes
(Setter?)或读取 WrappedComponent.contextTypes
(Getter?)时是否有副作用?
Terser 实际上试图弄清楚,但在很多情况下它无法确定。这并不意味着 terser 没有做好它的工作,因为它无法弄清楚。在 JavaScript 这样的动态语言中,可靠地确定它太困难了。
但是我们可以通过使用 /*#__PURE__*/
注解来帮助 terser。它将一个语句标记为无副作用。因此,一个小的更改就可以使代码进行摇树优化:
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
这将允许删除这段代码。但是对于需要包含/评估的导入仍然存在疑问,因为它们可能包含副作用。
为了解决这个问题,我们在 package.json
中使用了 "sideEffects"
属性。
它类似于 /*#__PURE__*/
,但作用于模块级别而不是语句级别。它("sideEffects"
属性)表示:“如果没有使用标记为无副作用的模块中的直接导出,打包器可以跳过评估该模块的副作用。”。
在 Shopify 的 Polaris 示例中,原始模块看起来像这样:
index.js
import './configure';
export * from './types';
export * from './components';
components/index.js
// ...
export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom } from './Button';
export { default as ButtonGroup } from './ButtonGroup';
// ...
package.json
// ...
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],
// ...
对于 import { Button } from "@shopify/polaris";
,这有以下含义:
特别是对于匹配的资源
index.js
: 没有使用直接导出,但标记了副作用 -> 包含它configure.js
: 没有使用导出,但标记了副作用 -> 包含它types/index.js
: 没有使用导出,没有标记副作用 -> 排除它components/index.js
: 没有使用直接导出,没有标记副作用,但使用了重新导出的导出 -> 跳过components/Breadcrumbs.js
: 没有使用导出,没有标记副作用 -> 排除它。这也排除了所有依赖项,例如 components/Breadcrumbs.css
,即使它们标记了副作用。components/Button.js
: 使用了直接导出,没有标记副作用 -> 包含它components/Button.css
: 没有使用导出,但标记了副作用 -> 包含它在这种情况下,只有 4 个模块包含在打包文件中:
index.js
: 几乎为空configure.js
components/Button.js
components/Button.css
在此优化之后,其他优化仍然可以应用。例如:Button.js
中的 buttonFrom
和 buttonsFrom
导出也未使用。usedExports
优化将检测到它,terser 可能会从模块中删除一些语句。
模块连接也适用。因此,这 4 个模块加上入口模块(可能还有更多依赖项)可以连接起来。最终 index.js
没有生成任何代码。
为了更好地理解 sideEffects
标志的影响,让我们看一个包含 CSS 资产的 npm 包的完整示例,以及它们在摇树优化过程中可能受到的影响。我们将创建一个虚构的 UI 组件库,名为“awesome-ui”。
我们的示例包如下:
awesome-ui/
├── package.json
├── dist/
│ ├── index.js
│ ├── components/
│ │ ├── index.js
│ │ ├── Button/
│ │ │ ├── index.js
│ │ │ └── Button.css
│ │ ├── Card/
│ │ │ ├── index.js
│ │ │ └── Card.css
│ │ └── Modal/
│ │ ├── index.js
│ │ └── Modal.css
│ └── theme/
│ ├── index.js
│ └── defaultTheme.css
package.json
{
"name": "awesome-ui",
"version": "1.0.0",
"main": "dist/index.js",
"sideEffects": false
}
dist/index.js
export * from './components';
export * from './theme';
dist/components/index.js
export { default as Button } from './Button';
export { default as Card } from './Card';
export { default as Modal } from './Modal';
dist/components/Button/index.js
import './Button.css'; // This has a side effect - it applies styles when imported!
export default function Button(props) {
// Button component implementation
return {
type: 'button',
...props,
};
}
dist/components/Button/Button.css
.awesome-ui-button {
background-color: #0078d7;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
dist/components/Card/index.js 和 dist/components/Modal/index.js 将具有相似的结构。
dist/theme/index.js
import './defaultTheme.css'; // This has a side effect!
export const themeColors = {
primary: '#0078d7',
secondary: '#f3f2f1',
danger: '#d13438',
};
现在,想象一个只希望使用 Button 组件的消费者应用程序:
import { Button } from 'awesome-ui';
// Use the Button component
sideEffects: false
当 webpack 在启用摇树优化的情况下处理此导入时:
sideEffects: false
。结果:Button 组件将渲染,但由于 Button.css 在摇树优化期间被删除,因此没有任何样式。
为了解决这个问题,我们需要更新 package.json,将 CSS 文件正确标记为具有副作用:
{
"name": "awesome-ui",
"version": "1.0.0",
"main": "dist/index.js",
"sideEffects": ["**/*.css"]
}
有了这个配置:
以下是 webpack 在摇树优化期间评估模块的方式:
此模块的导出是否被直接或间接使用?
模块是否标记有副作用?
sideEffects
包含此文件或为 true
):包含该模块。sideEffects
为 false
或不包含此文件):排除该模块及其依赖项。对于我们库中具有正确 sideEffects 配置的文件:
dist/index.js
:没有直接导出使用,没有副作用 -> 跳过dist/components/index.js
:没有直接导出使用,没有副作用 -> 跳过dist/components/Button/index.js
:直接导出使用 -> 包含dist/components/Button/Button.css
:没有导出,有副作用 -> 包含dist/components/Card/*
:没有导出使用,没有副作用 -> 排除dist/components/Modal/*
:没有导出使用,没有副作用 -> 排除dist/theme/*
:没有导出使用,没有副作用 -> 排除不正确的副作用配置可能会产生重大影响:
这些问题可能特别难以调试,因为它们通常只在启用摇树优化的生产构建中出现。
测试副作用配置是否正确的好方法是:
可以通过使用 /*#__PURE__*/
注解告诉 webpack 一个函数调用是无副作用的(纯的)。它可以放在函数调用前面,以将其标记为无副作用。传递给函数的参数不会被注解标记,可能需要单独标记。当未使用的变量的变量声明中的初始值被认为是无副作用的(纯的)时,它会被标记为死代码,不执行并被压缩器删除。当 optimization.innerGraph
设置为 true
时,此行为启用。
file.js
/*#__PURE__*/ double(55);
我们已经通过使用 import
和 export
语法来提示“死代码”将被删除,但我们仍然需要将其从打包文件中删除。为此,将 mode
配置选项设置为 production
。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- mode: 'development',
- optimization: {
- usedExports: true,
- }
+ mode: 'production',
};
搞定之后,我们可以再次运行 npm run build
,看看是否有什么变化。
注意到 dist/bundle.js
有什么不同吗?整个 bundle 现在都被压缩和混淆了,但是,如果您仔细查看,您不会看到 square
函数被包含,但会看到 cube
函数的一个混淆版本(function r(e){return e*e*e}n.a=r
)。通过压缩和摇树优化,我们的 bundle 现在小了几字节!虽然在这个人造示例中看起来不多,但在处理具有复杂依赖树的大型应用程序时,摇树优化可以显著减小 bundle 大小。
在使用摇树优化和 sideEffects
标志时,有几个常见的陷阱需要避免:
sideEffects: false
在 package.json 中设置 sideEffects: false
对于实现最佳摇树优化很有吸引力,但这可能会在您的代码实际具有副作用时导致问题。隐藏副作用的例子包括:
考虑这种模式:
// This file has side effects that might be skipped
import './polyfill';
// Re-export components
export * from './components';
如果消费者只导入特定组件,那么如果未正确标记副作用,polyfill 导入可能会完全跳过。
您的包可能正确标记了副作用,但如果它依赖于错误标记其副作用的第三方包,您仍然可能会遇到问题。
摇树优化通常只在生产模式下完全激活。只在开发中测试可能会隐藏摇树优化问题,直到部署。
我们学到的是,为了利用摇树优化,您必须...
import
和 export
)。package.json
文件中添加 "sideEffects"
属性。production
mode
配置选项来启用各种优化,包括压缩和摇树优化(副作用优化在开发模式下使用标志值启用)。devtool
设置了正确的值,因为其中一些不能在 production
模式下使用。您可以将您的应用程序想象成一棵树。您实际使用的源代码和库代表了树的绿色、活着的叶子。死代码代表了秋天枯萎、死去的树叶。为了摆脱枯死的叶子,您必须摇晃这棵树,使它们掉落。
如果您对优化输出的更多方法感兴趣,请跳到下一指南以获取有关构建生产环境的详细信息。
在本指南中,我们将深入探讨构建生产站点或应用程序的一些最佳实践和实用工具。
开发和生产构建的目标大相径庭。在开发中,我们希望有强大的源映射和一个带有实时重新加载或模块热替换的 localhost 服务器。在生产中,我们的目标转向专注于压缩打包、更轻量级的源映射以及优化的资产以提高加载时间。鉴于这种逻辑分离,我们通常建议为每个环境编写单独的 webpack 配置。
虽然我们将分离出生产和开发特定的部分,但请注意,我们仍将保持一个“通用”配置以保持代码简洁。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge
的实用工具。有了“通用”配置,我们就无需在特定于环境的配置中重复代码。
让我们从安装 webpack-merge
并拆分我们在前几指南中已经完成的部分开始:
npm install --save-dev webpack-merge
项目
webpack-demo
|- package.json
|- package-lock.json
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
|- /dist
|- /src
|- index.js
|- math.js
|- /node_modules
webpack.common.js
+ const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+ module.exports = {
+ entry: {
+ app: './src/index.js',
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Production',
+ }),
+ ],
+ output: {
+ filename: '[name].bundle.js',
+ path: path.resolve(__dirname, 'dist'),
+ clean: true,
+ },
+ };
webpack.dev.js
+ const { merge } = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+ mode: 'development',
+ devtool: 'inline-source-map',
+ devServer: {
+ static: './dist',
+ },
+ });
webpack.prod.js
+ const { merge } = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+ mode: 'production',
+ });
在 webpack.common.js
中,我们现在设置了 entry
和 output
配置,并且包含了两个环境都需要的任何插件。在 webpack.dev.js
中,我们将 mode
设置为 development
。此外,我们还添加了该环境推荐的 devtool
(强源映射)以及我们的 devServer
配置。最后,在 webpack.prod.js
中,mode
设置为 production
,它加载了 TerserPlugin
,该插件首次在摇树优化指南中介绍。
注意环境特定配置中 merge()
调用的使用,以在 webpack.dev.js
和 webpack.prod.js
中包含我们的通用配置。webpack-merge
工具提供了各种高级合并功能,但对于我们的用例,我们不需要这些。
现在,让我们修改 npm 脚本以使用新的配置文件。对于运行 webpack-dev-server
的 start
脚本,我们将使用 webpack.dev.js
;对于运行 webpack
创建生产构建的 build
脚本,我们将使用 webpack.prod.js
。
package.json
{
"name": "development",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
- "start": "webpack serve --open",
+ "start": "webpack serve --open --config webpack.dev.js",
- "build": "webpack"
+ "build": "webpack --config webpack.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^0.28.4",
"csv-loader": "^2.1.1",
"express": "^4.15.3",
"file-loader": "^0.11.2",
"html-webpack-plugin": "^2.29.0",
"style-loader": "^0.18.2",
"webpack": "^4.30.0",
"webpack-dev-middleware": "^1.12.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0",
"xml-loader": "^1.2.1"
}
}
随意运行这些脚本,看看随着我们不断向生产配置中添加内容,输出会发生什么变化。
许多库会根据 process.env.NODE_ENV
变量来确定库中应包含的内容。例如,当 process.env.NODE_ENV
未设置为 'production'
时,某些库可能会添加额外的日志记录和测试以使调试更容易。但是,当 process.env.NODE_ENV
设置为 'production'
时,它们可能会删除或添加大量代码以优化您的实际用户的运行方式。自 webpack v4 以来,指定 mode
会通过 DefinePlugin
自动为您配置 process.env.NODE_ENV
。
webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});
如果您正在使用像 react
这样的库,在添加 DefinePlugin
后,您应该会看到打包文件大小显著减小。此外,请注意,我们本地的任何 /src
代码也可以利用这一点,因此以下检查是有效的:
src/index.js
import { cube } from './math.js';
+
+ if (process.env.NODE_ENV !== 'production') {
+ console.log('Looks like we are in development mode!');
+ }
function component() {
const element = document.createElement('pre');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component());
Webpack v4+ 默认会在生产模式
下压缩您的代码。
请注意,虽然 TerserPlugin
是压缩的绝佳起点并被默认使用,但还有其他选项:
如果您决定尝试其他压缩插件,请确保您的新选择也像摇树优化指南中描述的那样删除死代码,并将其作为 optimization.minimizer
提供。
我们鼓励您在生产环境中启用源映射,因为它们对于调试和运行基准测试都很有用。话虽如此,您应该选择一个构建速度相当快且推荐用于生产环境的源映射(参见 devtool
)。对于本指南,我们将使用生产环境中的 source-map
选项,而不是我们在开发环境中使用的 inline-source-map
。
webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
+ devtool: 'source-map',
});
在生产环境中最小化 CSS 至关重要。请参阅生产最小化部分。
上述许多选项都可以设置为命令行参数。例如,optimization.minimize
可以通过 --optimization-minimize
设置,而 mode
可以通过 --mode
设置。运行 npx webpack --help=verbose
可获取完整的 CLI 参数列表。
虽然这些简写方法很有用,但我们建议在 webpack 配置文件中设置这些选项,以获得更多的可配置性。
懒加载,或“按需加载”,是优化您的网站或应用程序的好方法。这种做法本质上是将您的代码在逻辑断点处进行分割,然后一旦用户执行了需要或将需要新代码块的操作,就加载它。这加快了应用程序的初始加载速度,并减轻了其整体重量,因为某些代码块可能永远不会被加载。
让我们以代码分割中的示例为例,并对其稍作调整,以更清晰地演示这个概念。那里的代码确实导致生成了一个单独的块 lodash.bundle.js
,并且在脚本运行后立即“懒加载”了它。问题在于加载这个 bundle 不需要用户交互——这意味着每次页面加载时,请求都会触发。这并没有给我们带来太大帮助,反而会负面影响性能。
让我们尝试一些不同的东西。当用户点击按钮时,我们将添加一个交互来将一些文本记录到控制台。但是,我们将等到第一次交互发生时才加载该代码(print.js
)。为此,我们将返回并重构代码分割中的最终动态导入示例,并将 lodash
保留在主块中。
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
src/print.js
console.log(
'The print.js module has loaded! See the network tab in dev tools...'
);
export default () => {
console.log('Button Clicked: Here\'s "some text"!');
};
src/index.js
+ import _ from 'lodash';
+
- async function getComponent() {
+ function component() {
const element = document.createElement('div');
- const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
+ const button = document.createElement('button');
+ const br = document.createElement('br');
+ button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.appendChild(br);
+ element.appendChild(button);
+
+ // Note that because a network request is involved, some indication
+ // of loading would need to be shown in a production-level site/app.
+ button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+ const print = module.default;
+
+ print();
+ });
return element;
}
- getComponent().then(component => {
- document.body.appendChild(component);
- });
+ document.body.appendChild(component());
现在让我们运行 webpack 并查看我们的新懒加载功能。
...
Asset Size Chunks Chunk Names
print.bundle.js 417 bytes 0 [emitted] print
index.bundle.js 548 kB 1 [emitted] [big] index
index.html 189 bytes [emitted]
...
在某些情况下,将模块的所有用途转换为异步可能很烦人或很困难,因为它强制对所有函数进行不必要的异步化,而无法提供仅延迟同步评估工作的能力。
TC39 提案 延迟模块评估 旨在解决这个问题。
该提案旨在引入一种新的语法导入形式,它将始终只返回一个命名空间特殊对象。使用时,模块及其依赖项将不会执行,但会在模块图被视为已加载之前完全加载到准备执行的状态。
只有在访问此模块的属性时,才会执行操作(如果需要)。
通过启用 experiments.deferImport 可以使用此功能。
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
src/print.js
console.log(
'The print.js module has loaded! See the network tab in dev tools...'
);
export default () => {
console.log('Button Clicked: Here\'s "some text"!');
};
src/index.js
import _ from 'lodash';
+ import defer * as print from './print';
function component() {
const element = document.createElement('div');
const button = document.createElement('button');
const br = document.createElement('br');
button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.appendChild(br);
element.appendChild(button);
- // Note that because a network request is involved, some indication
- // of loading would need to be shown in a production-level site/app.
+ // In this example, the print module is downloaded but not evaluated,
+ // so there is no network request involved after the button is clicked.
- button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+ button.onclick = e => {
const print = module.default;
+ // ^ The module is evaluated here.
print();
- });
+ };
return element;
}
getComponent().then(component => {
document.body.appendChild(component);
});
document.body.appendChild(component());
这类似于 CommonJS 风格的懒加载
src/index.js
import _ from 'lodash';
- import defer * as print from './print';
function component() {
const element = document.createElement('div');
const button = document.createElement('button');
const br = document.createElement('br');
button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.appendChild(br);
element.appendChild(button);
// In this example, the print module is downloaded but not evaluated,
// so there is no network request involved after the button is clicked.
button.onclick = e => {
+ const print = require('./print');
+ // ^ The module is evaluated here.
const print = module.default;
- // ^ The module is evaluated here.
print();
};
return element;
}
getComponent().then(component => {
document.body.appendChild(component);
});
document.body.appendChild(component());
许多框架和库都有自己的建议,说明如何在它们的方法中实现这一点。以下是一些示例:
ECMAScript 模块(ESM)是 Web 中使用模块的规范。所有现代浏览器都支持它,也是 Web 模块化代码编写的推荐方式。
Webpack 支持处理 ECMAScript 模块以对其进行优化。
export
关键字允许将 ESM 中的内容暴露给其他模块。
export const CONSTANT = 42;
export let variable = 42;
// only reading is exposed
// it's not possible to modify the variable from outside
export function fun() {
console.log('fun');
}
export class C extends Super {
method() {
console.log('method');
}
}
let a, b, other;
export { a, b, other as c };
export default 1 + 2 + 3 + more();
import
关键字允许将其他模块中的引用引入 ESM 中。
import { CONSTANT, variable } from './module.js';
// import "bindings" to exports from another module
// these bindings are live. The values are not copied,
// instead accessing "variable" will get the current value
// in the imported module
import * as module from './module.js';
module.fun();
// import the "namespace object" which contains all exports
import theDefaultValue from './module.js';
// shortcut to import the "default" export
默认情况下,webpack 会自动检测文件是 ESM 还是不同的模块系统。
Node.js 建立了一种通过在 package.json
中使用属性来显式设置文件模块类型的方式。在 package.json 中设置 "type": "module"
会强制此 package.json 下的所有文件都成为 ECMAScript 模块。而设置 "type": "commonjs"
则会强制它们成为 CommonJS 模块。
{
"type": "module"
}
此外,文件可以通过使用 .mjs
或 .cjs
扩展名来设置模块类型。.mjs
将强制它们为 ESM,.cjs
将强制它们为 CommonJs。
在 DataURI 中,使用 text/javascript
或 application/javascript
MIME 类型也将强制模块类型为 ESM。
除了模块格式外,将模块标记为 ESM 还会影响解析逻辑、互操作逻辑以及模块中可用的符号。
ESM 中的导入解析更严格。除非您通过 fullySpecified=false
禁用此行为,否则相对请求必须包含文件名和文件扩展名(例如 *.js
或 *.mjs
)。
只有“default”导出可以从非 ESM 导入。命名导出不可用。
CommonJs 语法不可用:require
, module
, exports
, __filename
, __dirname
。
webpack
编译器可以理解用 ES2015 模块、CommonJS 或 AMD 编写的模块。然而,一些第三方库可能期望全局依赖(例如 jQuery
的 $
)。这些库也可能创建需要导出的全局变量。这些“损坏的模块”是垫片发挥作用的一个实例。
垫片在另一种情况下也很有用,即当您想 polyfill 浏览器功能以支持更多用户时。在这种情况下,您可能只想将这些 polyfill 提供给需要修补的浏览器(即按需加载它们)。
以下文章将详细介绍这两种用例。
让我们从第一个垫片全局变量的用例开始。在做任何事情之前,让我们再看看我们的项目:
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
|- /node_modules
还记得我们正在使用的 lodash
包吗?为了演示目的,假设我们想在整个应用程序中将其作为全局变量提供。为此,我们可以使用 ProvidePlugin
。
ProvidePlugin
将一个包作为变量提供给通过 webpack 编译的每个模块。如果 webpack 看到该变量被使用,它将把给定的包包含在最终的打包文件中。让我们继续,删除 lodash
的 import
语句,而是通过插件提供它:
src/index.js
-import _ from 'lodash';
-
function component() {
const element = document.createElement('div');
- // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
+const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash',
+ }),
+ ],
};
我们在这里所做的本质上是告诉 webpack……
如果您遇到至少一个
_
变量的实例,则包含lodash
包并将其提供给需要它的模块。
如果我们运行构建,我们应该仍然看到相同的输出。
$ npm run build
..
[webpack-cli] Compilation finished
asset main.js 69.1 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 344 bytes 2 modules
cacheable modules 530 KiB
./src/index.js 191 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 2910 ms
我们还可以使用 ProvidePlugin
通过“数组路径”(例如 [module, child, ...children?]
)配置,来暴露模块的单个导出。所以,假设我们只想在任何调用 join
方法的地方提供 lodash
的 join
方法:
src/index.js
function component() {
const element = document.createElement('div');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.ProvidePlugin({
- _: 'lodash',
+ join: ['lodash', 'join'],
}),
],
};
这与摇树优化完美配合,因为 lodash
库的其余部分应该会被删除。
一些遗留模块依赖于 this
为 window
对象。让我们更新 index.js
以使其符合这种情况:
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
+ // Assume we are in the context of `window`
+ this.alert("Hmmm, this probably isn't a great idea...");
+
return element;
}
document.body.appendChild(component());
当模块在 this
等于 module.exports
的 CommonJS 上下文中执行时,这会成为一个问题。在这种情况下,您可以使用 imports-loader
覆盖 this
。
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: require.resolve('./src/index.js'),
+ use: 'imports-loader?wrapper=window',
+ },
+ ],
+ },
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
假设一个库创建了一个全局变量,并期望其消费者使用它。我们可以在设置中添加一个小型模块来演示这一点:
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- globals.js
|- /node_modules
src/globals.js
const file = 'blah.txt';
const helpers = {
test: function () {
console.log('test something');
},
parse: function () {
console.log('parse something');
},
};
现在,虽然您在自己的源代码中可能永远不会这样做,但您可能会遇到一个过时的库,其中包含与上面所示相似的代码,并且您希望使用它。在这种情况下,我们可以使用 exports-loader
,将该全局变量作为正常的模块导出。例如,为了将 file
导出为 file
,将 helpers.parse
导出为 parse
:
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
+ {
+ test: require.resolve('./src/globals.js'),
+ use:
+ 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+ },
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
现在,在我们的入口脚本(即 src/index.js
)中,我们可以使用 const { file, parse } = require('./globals.js');
,一切都应该顺利运行。
到目前为止,我们讨论的一切都与处理旧版软件包有关。让我们继续讨论第二个主题:polyfill。
有很多方法可以加载 polyfill。例如,要包含 babel-polyfill
,我们可能需要:
npm install --save babel-polyfill
并将其 import
进来以包含在我们的主打包文件中:
src/index.js
+import 'babel-polyfill';
+
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
请注意,这种方法优先考虑正确性而非打包文件大小。为了安全和健壮,polyfill/shims 必须在所有其他代码之前运行,因此需要同步加载,或者所有应用程序代码需要在所有 polyfill/shims 加载之后才能加载。社区中也有许多误解,认为现代浏览器“不需要” polyfill,或者 polyfill/shims 仅仅是为了添加缺失的功能——事实上,它们通常会修复损坏的实现,即使是在最现代的浏览器中也是如此。因此,最佳实践仍然是无条件地同步加载所有 polyfill/shims,尽管这会增加打包文件大小。
如果您觉得您已经解决了这些问题并希望承担破坏的风险,这里有一种可能的方法:让我们将 import
移动到一个新文件,并添加 whatwg-fetch
polyfill:
npm install --save whatwg-fetch
src/index.js
-import 'babel-polyfill';
-
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- globals.js
+ |- polyfills.js
|- /node_modules
src/polyfills.js
import 'babel-polyfill';
import 'whatwg-fetch';
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ polyfills: './src/polyfills',
+ index: './src/index.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
{
test: require.resolve('./src/globals.js'),
use:
'exports-loader?type=commonjs&exports[]=file&exports[]=multiple|helpers.parse|parse',
},
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
有了这些,我们就可以添加逻辑来有条件地加载我们的新 polyfills.bundle.js
文件。您如何做出这个决定取决于您需要支持的技术和浏览器。我们将进行一些测试来确定是否需要我们的 polyfill:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
+ <script>
+ const modernBrowser = 'fetch' in window && 'assign' in Object;
+
+ if (!modernBrowser) {
+ const scriptElement = document.createElement('script');
+
+ scriptElement.async = false;
+ scriptElement.src = '/polyfills.bundle.js';
+ document.head.appendChild(scriptElement);
+ }
+ </script>
</head>
<body>
- <script src="main.js"></script>
+ <script src="index.bundle.js"></script>
</body>
</html>
现在我们可以在入口脚本中 fetch
一些数据:
src/index.js
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert("Hmmm, this probably isn't a great idea...");
return element;
}
document.body.appendChild(component());
+
+fetch('https://jsonplaceholder.typicode.com/users')
+ .then((response) => response.json())
+ .then((json) => {
+ console.log(
+ "We retrieved some data! AND we're confident it will work on a variety of browser distributions."
+ );
+ console.log(json);
+ })
+ .catch((error) =>
+ console.error('Something went wrong when fetching this data: ', error)
+ );
如果我们运行构建,将发出另一个 polyfills.bundle.js
文件,并且一切在浏览器中应该仍然运行顺利。请注意,此设置可能可以改进,但它应该能让您很好地了解如何仅向实际需要 polyfill 的用户提供它们。
babel-preset-env
包使用 browserslist 来仅转译您的浏览器矩阵中不支持的内容。此预设带有 useBuiltIns
选项(默认为 false
),它将您的全局 babel-polyfill
导入转换为更细粒度的按功能 import
模式:
import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';
import 'core-js/modules/web.timers';
import 'core-js/modules/web.immediate';
import 'core-js/modules/web.dom.iterable';
更多信息请参阅 babel-preset-env 文档。
Node 内置模块,例如 process
,可以直接从您的配置文件中进行 polyfill,而无需使用任何特殊的加载器或插件。有关更多信息和示例,请参阅 node 配置页面。
还有一些其他工具可以在处理旧版模块时提供帮助。
当模块没有 AMD/CommonJS 版本并且您想包含 dist
时,您可以在 noParse
中标记此模块。这将导致 webpack 包含该模块而不对其进行解析或解析 require()
和 import
语句。这种做法也用于提高构建性能。
最后,有些模块支持多种模块样式;例如 AMD、CommonJS 和遗留模块的组合。在大多数这些情况下,它们首先检查 define
,然后使用一些古怪的代码来导出属性。在这些情况下,通过 imports-loader
设置 additionalCode=var%20define%20=%20false;
可能有助于强制执行 CommonJS 路径。
TypeScript 是 JavaScript 的一个带类型超集,它编译为纯 JavaScript。在本指南中,我们将学习如何将 TypeScript 与 webpack 集成。
首先通过运行以下命令安装 TypeScript 编译器和加载器:
npm install --save-dev typescript ts-loader
现在我们将修改目录结构和配置文件
项目
webpack-demo
|- package.json
|- package-lock.json
+ |- tsconfig.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
+ |- index.ts
|- /node_modules
tsconfig.json
让我们设置一个配置来支持 JSX 并将 TypeScript 编译为 ES5...
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
请参阅TypeScript 的文档,了解有关 tsconfig.json
配置选项的更多信息。
要了解有关 webpack 配置的更多信息,请参阅配置概念。
现在让我们配置 webpack 来处理 TypeScript。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
这将指示 webpack 从 ./index.ts
进入,通过 ts-loader
加载所有 .ts
和 .tsx
文件,并将 bundle.js
文件输出到当前目录。
现在让我们更改 ./index.ts
中 lodash
的导入,因为 lodash
定义中没有默认导出。
./index.ts
- import _ from 'lodash';
+ import * as _ from 'lodash';
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.ts
中使用 TypeScript 的方法在 webpack.config.ts
中使用 TypeScript 有 5 种方式:
使用带 TypeScript 配置的 webpack
webpack -c ./webpack.config.ts
(由于 rechoir
和 interpret
的限制,并非所有功能都受支持。)
为 Node.js 使用自定义 --import
NODE_OPTIONS='--import tsx' webpack --disable-interpret -c ./webpack.config.ts
为 Node.js v22.7.0 ≥ 您的 Node.js 版本 < v23.6.0 使用内置 TypeScript 模块
NODE_OPTIONS='--experimental-strip-types' webpack --disable-interpret -c ./webpack.config.ts
为 Node.js ≥ v22.6.0 使用内置 TypeScript 模块
webpack --disable-interpret -c ./webpack.config.ts
为 Node.js ≥ v22.6.0 使用 tsx
NODE_OPTIONS='--no-experimental-strip-types --import tsx' webpack --disable-interpret -c ./webpack.config.ts
我们在本指南中使用 ts-loader
,因为它使启用其他 webpack 功能(例如导入其他 Web 资产)变得更容易。
请注意,如果您已经使用 babel-loader
来转译代码,您可以使用 @babel/preset-typescript
,让 Babel 同时处理您的 JavaScript 和 TypeScript 文件,而无需使用额外的加载器。请记住,与 ts-loader
相反,底层的 @babel/plugin-transform-typescript
插件不执行任何类型检查。
要了解有关源映射的更多信息,请参阅开发指南。
要启用源映射,我们必须配置 TypeScript 将内联源映射输出到我们编译后的 JavaScript 文件。以下行必须添加到我们的 TypeScript 配置中:
tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/",
+ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
}
}
现在我们需要告诉 webpack 提取这些源映射并包含在我们的最终打包文件中:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.ts',
+ devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
有关更多信息,请参阅devtool 文档。
您可以在 TypeScript 代码中使用 webpack 特定功能,例如 import.meta.webpack
。Webpack 也为它们提供了类型,添加一个 TypeScript reference
指令来声明它:
/// <reference types="webpack/module" />
console.log(import.meta.webpack); // without reference declared above, TypeScript will throw an error
从 npm 安装第三方库时,务必记住安装该库的类型定义。
例如,如果我们想安装 lodash,我们可以运行以下命令来获取其类型定义:
npm install --save-dev @types/lodash
如果 npm 包中已包含其声明类型文件,则无需下载相应的 @types
包。更多信息请参阅 TypeScript 变更日志博客。
要将非代码资产与 TypeScript 一起使用,我们需要延迟这些导入的类型。这需要一个 custom.d.ts
文件,它表示我们项目中 TypeScript 的自定义定义。让我们为 .svg
文件设置一个声明:
custom.d.ts
declare module '*.svg' {
const content: any;
export default content;
}
在这里,我们通过指定任何以 .svg
结尾的导入并将模块的 content
定义为 any
来声明 SVG 的新模块。我们可以通过将类型定义为字符串来更明确地说明它是一个 URL。相同的概念适用于包括 CSS、SCSS、JSON 等在内的其他资产。
请参阅构建性能指南中的构建工具部分。
从 webpack 5 开始,您可以无需 worker-loader
即可使用 Web Workers。
new Worker(new URL('./worker.js', import.meta.url));
// or customize the chunk name with magic comments
// see https://webpack.js.cn/api/module-methods/#magic-comments
new Worker(
/* webpackChunkName: "foo-worker" */ new URL('./worker.js', import.meta.url)
);
选择此语法是为了允许在没有打包器的情况下运行代码,它也可在浏览器中的原生 ECMAScript 模块中使用。
请注意,虽然 Worker
API 建议 Worker
构造函数会接受表示脚本 URL 的字符串,但在 webpack 5 中,您只能使用 URL
。
src/index.js
const worker = new Worker(new URL('./deep-thought.js', import.meta.url));
worker.postMessage({
question:
'The Answer to the Ultimate Question of Life, The Universe, and Everything.',
});
worker.onmessage = ({ data: { answer } }) => {
console.log(answer);
};
src/deep-thought.js
self.onmessage = ({ data: { question } }) => {
self.postMessage({
answer: 42,
});
};
Node.js(>= 12.17.0)支持类似的语法:
import { Worker } from 'worker_threads';
new Worker(new URL('./worker.js', import.meta.url));
请注意,这仅在 ESM 中可用。Webpack 或 Node.js 都不支持 CommonJS 语法中的 Worker
。
渐进式 Web 应用(或称 PWA)是提供类似于原生应用程序体验的 Web 应用。有许多因素促成了这一点。其中,最重要的是应用能够在**离线**时运行的能力。这通过使用名为 Service Workers 的 Web 技术实现。
本节将重点介绍如何为我们的应用添加离线体验。我们将使用一个名为 Workbox 的 Google 项目来实现,它提供了有助于更轻松地为 Web 应用设置离线支持的工具。
到目前为止,我们一直通过直接访问本地文件系统来查看输出。然而,通常真实用户通过网络访问 Web 应用;他们的浏览器与一个**服务器**通信,该服务器会提供所需的资源(例如 .html
、.js
和 .css
文件)。
那么,让我们使用一个具有更基本功能的服务器来测试当前体验。让我们使用 http-server 包:npm install http-server --save-dev
。我们还将修改 package.json
的 scripts
部分,添加一个 start
脚本。
package.json
{
...
"scripts": {
- "build": "webpack"
+ "build": "webpack",
+ "start": "http-server dist"
},
...
}
注意:webpack DevServer 默认写入内存。我们需要启用 devserverdevmiddleware.writeToDisk 选项,以便 http-server 能够从 ./dist
目录提供文件。
如果您之前没有这样做,请运行命令 npm run build
来构建您的项目。然后运行命令 npm start
。这应该会产生以下输出:
> http-server dist
Starting up http-server, serving dist
Available on:
http://xx.x.x.x:8080
http://127.0.0.1:8080
http://xxx.xxx.x.x:8080
Hit CTRL-C to stop the server
如果您在浏览器中打开 https://:8080
(即 http://127.0.0.1
),您应该会看到您的 webpack 应用从 dist
目录提供。如果您停止服务器并刷新,webpack 应用将不再可用。
这就是我们希望改变的地方。一旦我们完成本模块,我们应该能够停止服务器,刷新页面,并且仍然看到我们的应用。
让我们添加 Workbox webpack 插件并调整 webpack.config.js
文件。
npm install workbox-webpack-plugin --save-dev
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Progressive Web Application',
}),
+ new WorkboxPlugin.GenerateSW({
+ // these options encourage the ServiceWorkers to get in there fast
+ // and not allow any straggling "old" SWs to hang around
+ clientsClaim: true,
+ skipWaiting: true,
+ }),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
设置完成后,让我们看看运行 npm run build
会发生什么。
...
Asset Size Chunks Chunk Names
app.bundle.js 545 kB 0, 1 [emitted] [big] app
print.bundle.js 2.74 kB 1 [emitted] print
index.html 254 bytes [emitted]
precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js 268 bytes [emitted]
service-worker.js 1 kB [emitted]
...
如您所见,现在生成了两个额外文件;service-worker.js
和更详细的 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
。service-worker.js
是 Service Worker 文件,而 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
是 service-worker.js
运行所需的文件。您自己生成的文件可能会有所不同;但您应该有一个 service-worker.js
文件。
现在我们已经成功生成了一个 Service Worker,真是令人高兴。接下来是什么?
让我们通过注册 Service Worker 来使其发挥作用。我们将通过添加以下注册代码来实现:
index.js
import _ from 'lodash';
import printMe from './print.js';
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/service-worker.js').then(registration => {
+ console.log('SW registered: ', registration);
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError);
+ });
+ });
+ }
再次运行 npm run build
来构建包含注册代码的应用版本。然后用 npm start
启动服务。导航到 https://:8080
并查看控制台。您应该会在其中看到:
SW registered
现在来测试一下。停止服务器并刷新页面。如果您的浏览器支持 Service Workers,那么您应该仍然能看到您的应用。然而,它是通过您的 Service Worker 而**不是**服务器提供的。
您已使用 Workbox 项目构建了一个离线应用。您已开始将您的 Web 应用转变为 PWA 的旅程。您现在可能想进一步探索。一个能帮助您的好资源可以在这里找到。
publicPath
配置选项在各种场景中都非常有用。它允许您为应用中的所有资源指定基本路径。
在实际应用中,有几个用例使得此功能变得特别巧妙。本质上,每个输出到 output.path
目录的文件都将从 output.publicPath
位置引用。这包括(通过代码分割创建的)子块以及依赖图中的任何其他资源(例如图像、字体等)。
例如,在开发环境中,我们可能有一个 assets/
文件夹与我们的索引页处于同一级别。这很好,但如果我们在生产环境中希望将所有这些静态资源托管在 CDN 上怎么办?
要解决这个问题,您可以轻松使用一个旧的环境变量。假设我们有一个变量 ASSET_PATH
。
import webpack from 'webpack';
// Try the environment variable, otherwise use root
const ASSET_PATH = process.env.ASSET_PATH || '/';
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// This makes it possible for us to safely use env vars on our code
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
}),
],
};
另一个可能的用例是动态设置 publicPath
。Webpack 暴露了一个名为 __webpack_public_path__
的全局变量,允许您这样做。在您的应用入口点,您可以这样做:
__webpack_public_path__ = process.env.ASSET_PATH;
这就是您所需要的一切。由于我们已经在配置中使用了 DefinePlugin
,process.env.ASSET_PATH
将始终被定义,因此我们可以安全地这样做。
// entry.js
import './public-path';
import './app';
有时您可能无法提前知道 publicPath
是什么,webpack 可以通过从 import.meta.url
、document.currentScript
、script.src
或 self.location
等变量确定公共路径来自动处理。您需要将 output.publicPath
设置为 'auto'
。
webpack.config.js
module.exports = {
output: {
publicPath: 'auto',
},
};
请注意,在不支持 document.currentScript
的情况下,例如 IE 浏览器,您将不得不包含一个像 currentScript Polyfill
这样的 polyfill。
让我们首先澄清一个常见的误解。Webpack 是一个模块打包器,类似于 Browserify 或 Brunch。它*不是*任务运行器,不像 Make、Grunt 或 Gulp。任务运行器处理常见开发任务的自动化,例如代码检查、构建或测试项目。与打包器相比,任务运行器更关注高层次任务。您仍然可以从它们的高级工具中受益,同时将打包问题留给 webpack。
打包器帮助您准备好 JavaScript 和样式表以供部署,将它们转换为适合浏览器的格式。例如,JavaScript 可以被压缩或拆分为块并延迟加载以提高性能。打包是 Web 开发中最重要的挑战之一,很好地解决它可以在此过程中消除很多痛苦。
好消息是,尽管存在一些重叠,但如果方法得当,任务运行器和打包器可以很好地协同工作。本指南提供了 webpack 如何集成到一些更流行的任务运行器中的高级概述。
通常,webpack 用户使用 npm scripts
作为他们的任务运行器。这是一个很好的起点。跨平台支持可能会成为一个问题,但有几种解决方法。许多(如果不是大多数)用户通过 npm scripts
以及不同级别的 webpack 配置和工具来完成工作。
因此,虽然 webpack 的核心重点是打包,但也有各种扩展可以使您将其用于任务运行器的典型工作。集成一个单独的工具会增加复杂性,因此在继续之前务必权衡利弊。
对于使用 Grunt 的用户,我们推荐 grunt-webpack
包。使用 grunt-webpack
,您可以将 webpack 或 webpack-dev-server 作为任务运行,在模板标签中获取统计信息,拆分开发和生产配置等等。如果您尚未安装,请首先安装 grunt-webpack
以及 webpack
本身。
npm install --save-dev grunt-webpack webpack
然后注册配置并加载任务:
Gruntfile.js
const webpackConfig = require('./webpack.config.js');
module.exports = function (grunt) {
grunt.initConfig({
webpack: {
options: {
stats: !process.env.NODE_ENV || process.env.NODE_ENV === 'development',
},
prod: webpackConfig,
dev: Object.assign({ watch: true }, webpackConfig),
},
});
grunt.loadNpmTasks('grunt-webpack');
};
更多信息请访问仓库。
借助 webpack-stream
包(又名 gulp-webpack
),Gulp 的集成也相当直接。在这种情况下,不需要单独安装 webpack
,因为它是 webpack-stream
的直接依赖项。
npm install --save-dev webpack-stream
您可以 require('webpack-stream')
而不是 webpack
,并且可以选择性地向其传递配置。
gulpfile.js
const gulp = require('gulp');
const webpack = require('webpack-stream');
gulp.task('default', function () {
return gulp
.src('src/entry.js')
.pipe(
webpack({
// Any configuration options...
})
)
.pipe(gulp.dest('dist/'));
});
更多信息请访问仓库。
mocha-webpack
工具可用于与 Mocha 的简洁集成。该仓库提供了有关优缺点的更多详细信息,但本质上 mocha-webpack
是一个简单的包装器,提供与 Mocha 本身几乎相同的 CLI,并提供各种 webpack 功能,例如改进的观察模式和改进的路径解析。以下是您如何安装它并使用它来运行测试套件(位于 ./test
中)的一个小示例:
npm install --save-dev webpack mocha mocha-webpack
mocha-webpack 'test/**/*.js'
更多信息请访问仓库。
karma-webpack
包允许您在 Karma 中使用 webpack 预处理文件。
npm install --save-dev webpack karma karma-webpack
karma.conf.js
module.exports = function (config) {
config.set({
frameworks: ['webpack'],
files: [
{ pattern: 'test/*_test.js', watched: false },
{ pattern: 'test/**/*_test.js', watched: false },
],
preprocessors: {
'test/*_test.js': ['webpack'],
'test/**/*_test.js': ['webpack'],
},
webpack: {
// Any custom webpack configuration...
},
plugins: ['karma-webpack'],
});
};
更多信息请访问仓库。
当对 entry 使用值数组时,可以提供不同类型的文件,以在未在 JavaScript 中使用 import
样式(单页应用之前或出于其他原因)的应用中,为 CSS 和 JavaScript(以及其他)文件实现单独的打包。
让我们举个例子。我们有一个 PHP 应用,包含两种页面类型:主页和账户页。主页具有不同的布局和与应用其余部分(账户页)不共享的 JavaScript。我们希望从应用文件中为主页输出 home.js
和 home.css
,为账户页输出 account.js
和 account.css
。
home.js
console.log('home page type');
home.scss
// home page individual styles
account.js
console.log('account page type');
account.scss
// account page individual styles
我们将使用 MiniCssExtractPlugin
在 production
模式下处理 CSS,作为一种最佳实践。
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: process.env.NODE_ENV,
entry: {
home: ['./home.js', './home.scss'],
account: ['./account.js', './account.scss'],
},
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.scss$/,
use: [
// fallback to style-loader in development
process.env.NODE_ENV !== 'production'
? 'style-loader'
: MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
};
使用上述配置运行 webpack 会输出到 ./dist
,因为我们没有指定不同的输出路径。./dist
目录现在将包含四个文件:
资源模块允许在不配置额外加载器的情况下使用资源文件(字体、图标等)。
在 webpack 5 之前,通常使用:
raw-loader
将文件作为字符串导入url-loader
将文件作为数据 URI 内联到打包中file-loader
将文件输出到输出目录资源模块类型通过添加 4 种新的模块类型来替换所有这些加载器:
asset/resource
输出一个单独的文件并导出 URL。以前通过使用 file-loader
实现。asset/inline
导出资源的 data URI。以前通过使用 url-loader
实现。asset/source
导出资源的源代码。以前通过使用 raw-loader
实现。asset
自动选择导出数据 URI 还是输出单独的文件。以前通过设置资源大小限制的 url-loader
实现。在 webpack 5 中,当旧资源加载器(即 file-loader
/url-loader
/raw-loader
)与资源模块一起使用时,您可能希望阻止资源模块再次处理您的资源,因为这会导致资源重复。可以通过将资源的模块类型设置为 'javascript/auto'
来实现。
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
},
],
+ type: 'javascript/auto'
},
]
},
}
要从资源加载器中排除来自新 URL 调用的资源,请将 dependency: { not: ['url'] }
添加到加载器配置中。
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
+ dependency: { not: ['url'] },
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
],
}
}
默认情况下,在底层,asset
类型执行 __webpack_public_path__ + import.meta
。这意味着在您的配置中设置 output.publicPath
将允许您覆盖 asset
加载的 URL。
如果您在代码中设置 __webpack_public_path__
,为了不破坏 asset
加载逻辑,您需要确保它在应用中作为第一个代码运行,并且不使用函数来实现。一个例子是创建一个名为 publicPath.js
的文件,其内容如下:
__webpack_public_path__ = 'https://cdn.url.com';
然后更新您的 webpack.config.js
中的 entry
字段,使其看起来像:
module.exports = {
entry: ['./publicPath.js', './App.js'],
};
或者,您可以在 App.js
中执行以下操作,而无需修改 webpack 配置。唯一的缺点是您必须在此处强制执行顺序,这可能会与某些代码检查工具发生冲突。
import './publicPath.js';
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.png/,
+ type: 'asset/resource'
+ }
+ ]
+ },
};
src/index.js
import mainImage from './images/main.png';
img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
所有 .png
文件都将输出到输出目录,其路径将注入到打包中。此外,您可以自定义它们的 outputPath
和 publicPath
。
默认情况下,asset/resource
模块以 [hash][ext][query]
文件名输出到输出目录。
您可以通过在 webpack 配置中设置 output.assetModuleFilename
来修改此模板。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
+ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
};
另一个自定义输出文件名的场景是将某些类型的资源输出到指定目录。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
+ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
- }
+ },
+ {
+ test: /\.html/,
+ type: 'asset/resource',
+ generator: {
+ filename: 'static/[hash][ext][query]'
+ }
+ }
]
},
};
通过此配置,所有 html
文件都将输出到输出目录内的 static
目录中。
Rule.generator.filename
与 output.assetModuleFilename
相同,并且仅适用于 asset
和 asset/resource
模块类型。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
- assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
- test: /\.png/,
- type: 'asset/resource'
+ test: /\.svg/,
+ type: 'asset/inline'
- },
+ }
- {
- test: /\.html/,
- type: 'asset/resource',
- generator: {
- filename: 'static/[hash][ext][query]'
- }
- }
]
}
};
src/index.js
- import mainImage from './images/main.png';
+ import metroMap from './images/metro.svg';
- img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
+ block.style.background = `url(${metroMap})`; // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)
所有 .svg
文件都将作为数据 URI 注入到打包中。
默认情况下,webpack 发出的数据 URI 表示使用 Base64 算法编码的文件内容。
如果您想使用自定义编码算法,可以指定一个自定义函数来编码文件内容:
webpack.config.js
const path = require('path');
+ const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
+ generator: {
+ dataUrl: content => {
+ content = content.toString();
+ return svgToMiniDataURI(content);
+ }
+ }
}
]
},
};
现在所有 .svg
文件都将通过 mini-svg-data-uri
包进行编码。
webpack.config.js
const path = require('path');
- const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
- test: /\.svg/,
- type: 'asset/inline',
- generator: {
- dataUrl: content => {
- content = content.toString();
- return svgToMiniDataURI(content);
- }
- }
+ test: /\.txt/,
+ type: 'asset/source',
}
]
},
};
src/example.txt
Hello world
src/index.js
- import metroMap from './images/metro.svg';
+ import exampleText from './example.txt';
- block.style.background = `url(${metroMap}); // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)
+ block.textContent = exampleText; // 'Hello world'
所有 .txt
文件将按原样注入到打包中。
当使用 new URL('./path/to/asset', import.meta.url)
时,webpack 也会创建一个资源模块。
src/index.js
const logo = new URL('./logo.svg', import.meta.url);
根据您配置中的 target
,webpack 会将上述代码编译成不同的结果:
// target: web
new URL(
__webpack_public_path__ + 'logo.svg',
document.baseURI || self.location.href
);
// target: webworker
new URL(__webpack_public_path__ + 'logo.svg', self.location);
// target: node, node-webkit, nwjs, electron-main, electron-renderer, electron-preload, async-node
new URL(
__webpack_public_path__ + 'logo.svg',
require('url').pathToFileUrl(__filename)
);
自 webpack 5.38.0 起,new URL()
中也支持 数据 URL。
src/index.js
const url = new URL('data:,', import.meta.url);
console.log(url.href === 'data:,');
console.log(url.protocol === 'data:');
console.log(url.pathname === ',');
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
+ test: /\.txt/,
+ type: 'asset',
}
]
},
};
现在 webpack 将根据默认条件自动选择 resource
或 inline
:大小小于 8kb 的文件将被视为 inline
模块类型,否则视为 resource
模块类型。
您可以通过在 webpack 配置的模块规则级别设置 Rule.parser.dataUrlCondition.maxSize
选项来更改此条件。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset',
+ parser: {
+ dataUrlCondition: {
+ maxSize: 4 * 1024 // 4kb
+ }
+ }
}
]
},
};
您还可以指定一个函数来决定是否内联模块。
在资源模块和 Webpack 5 之前,可以使用上述传统加载器的内联语法。
现在建议删除所有内联加载器语法,并使用 resourceQuery
条件来模拟内联语法的功能。
例如,在用 asset/source
类型替换 raw-loader
的情况下:
- import myModule from 'raw-loader!my-module';
+ import myModule from 'my-module?raw';
以及在 webpack 配置中:
module: {
rules: [
// ...
+ {
+ resourceQuery: /raw/,
+ type: 'asset/source',
+ }
]
},
如果您想排除原始资源不被其他加载器处理,请使用负条件:
module: {
rules: [
// ...
+ {
+ test: /\.m?js$/,
+ resourceQuery: { not: [/raw/] },
+ use: [ ... ]
+ },
{
resourceQuery: /raw/,
type: 'asset/source',
}
]
},
或者使用 oneOf
规则列表。这里只应用第一个匹配的规则:
module: {
rules: [
// ...
+ { oneOf: [
{
resourceQuery: /raw/,
type: 'asset/source',
},
+ {
+ test: /\.m?js$/,
+ use: [ ... ]
+ },
+ ] }
]
},
对于像服务器端渲染这样的用例,您可能希望禁用资源输出,这可以通过 Rule.generator
下的 emit
选项实现。
module.exports = {
// …
module: {
rules: [
{
test: /\.png$/i,
type: 'asset/resource',
generator: {
emit: false,
},
},
],
},
};
包的 package.json
中的 exports
字段允许在使用 import "package"
或 import "package/sub/path"
等模块请求时声明应使用哪个模块。它取代了默认实现,即为 "package"
返回 main
字段或 index.js
文件,以及为 "package/sub/path"
进行文件系统查找的默认实现。
当指定 exports
字段时,只有这些模块请求可用。任何其他请求都将导致 ModuleNotFound 错误。
通常,exports
字段应包含一个对象,其中每个属性指定模块请求的子路径。对于上述示例,可以使用以下属性:"."
用于 import "package"
,"./sub/path"
用于 import "package/sub/path"
。以 /
结尾的属性会将带有此前缀的请求转发到旧的文件系统查找算法。对于以 *
结尾的属性,*
可以取任何值,并且属性值中的任何 *
都将替换为所取的值。
示例
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
"./other-prefix/*": "./yet-another/*/*.js"
}
}
模块请求 | 结果 |
---|---|
package | .../package/main.js |
package/sub/path | .../package/secondary.js |
package/prefix/some/file.js | .../package/directory/some/file.js |
package/prefix/deep/file.js | .../package/other-directory/file.js |
package/other-prefix/deep/file.js | .../package/yet-another/deep/file/deep/file.js |
package/main.js | 错误 |
除了提供单个结果外,包作者还可以提供一个结果列表。在这种情况下,此列表将按顺序尝试,并使用第一个有效结果。
注意:仅使用第一个有效结果,而不是所有有效结果。
示例
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
这里 package/things/apple
可能会在 .../package/good-things/apple
或 .../package/bad-things/apple
中找到。
例如,给定以下配置:
{
"exports": {
".": ["-bad-specifier-", "./non-existent.js", "./existent.js"]
}
}
Webpack 5.94.0+ 现在将抛出错误,因为找不到 non-existent.js
,而之前的行为会解析到 existent.js
。
包作者可以不直接在 exports
字段中提供结果,而是让模块系统根据环境条件选择一个。
在这种情况下,应使用一个将条件映射到结果的对象。条件按对象顺序尝试。包含无效结果的条件将被跳过。条件可以嵌套以创建逻辑 AND。对象中的最后一个条件可以是特殊的 "default"
条件,它总是匹配的。
示例
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
这可以转化为类似以下内容:
if (red && valid('./stop.js')) return './stop.js';
if (yellow && valid('./stop.js')) return './stop.js';
if (green) {
if (free && valid('./drive.js')) return './drive.js';
if (valid('./wait.js')) return './wait.js';
}
if (valid('./drive-carefully.js')) return './drive-carefully.js';
throw new ModuleNotFoundError();
可用条件因所使用的模块系统和工具而异。
当包中只支持一个入口("."
)时,可以省略 { ".": ... }
对象嵌套。
{
"exports": "./index.mjs"
}
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
在每个键都是条件的对象中,属性的顺序是重要的。条件按其指定的顺序处理。
示例:{ "red": "./stop.js", "green": "./drive.js" }
!= { "green": "./drive.js", "red": "./stop.js" }
(当 red
和 green
条件都设置时,将使用第一个属性)
在每个键都是子路径的对象中,属性(子路径)的顺序不重要。更具体的路径优先于不那么具体的路径。
示例:{ "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" }
== { "./a/b/c": "./z", "./a/b/": "./y/", "./a/": "./x/" }
(顺序将始终是:./a/b/c
> ./a/b/
> ./a/
)
exports
字段优先于其他包入口字段,如 main
、module
、browser
或自定义字段。
功能 | 支持者 |
---|---|
"." 属性 | Node.js、webpack、rollup、esinstall、wmr |
普通属性 | Node.js、webpack、rollup、esinstall、wmr |
以 / 结尾的属性 | |
以 * 结尾的属性 | Node.js、webpack、rollup、esinstall |
替代方案 | Node.js、webpack、rollup、 |
仅缩写路径 | Node.js、webpack、rollup、esinstall、wmr |
仅缩写条件 | Node.js、webpack、rollup、esinstall、wmr |
条件语法 | Node.js、webpack、rollup、esinstall、wmr |
嵌套条件语法 | Node.js、webpack、rollup、wmr(5) |
条件顺序 | Node.js、webpack、rollup、wmr(6) |
"default" 条件 | Node.js、webpack、rollup、esinstall、wmr |
路径顺序 | Node.js、webpack、rollup |
未映射时报错 | Node.js、webpack、rollup、esinstall、wmr(7) |
混合条件和路径时报错 | Node.js、webpack、rollup |
(1) 在 Node.js 17 中移除。请改用 *
。
(2) "./"
被有意地忽略为键。
(3) 属性值被忽略,属性键被用作目标。实际上只允许键和值相同的映射。
(4) 支持该语法,但始终使用第一个入口,这使其无法用于任何实际用例。
(5) 回退到替代的同级父条件处理不正确。
(6) 对于 require
条件,对象顺序处理不正确。这是有意为之,因为 wmr 不区分引用语法。
(7) 当使用 "exports": "./file.js"
缩写时,任何请求(例如 package/not-existing
)都将解析到该文件。不使用缩写时,直接的文件访问(例如 package/file.js
)不会导致错误。
根据用于引用模块的语法,将设置以下条件之一:
条件 | 描述 | 支持者 |
---|---|---|
import | 请求来自 ESM 语法或类似语法。 | Node.js、webpack、rollup、esinstall(1)、wmr(1) |
require | 请求来自 CommonJs/AMD 语法或类似语法。 | Node.js、webpack、rollup、esinstall(1)、wmr(1) |
style | 请求来自样式表引用。 | - |
sass | 请求来自 sass 样式表引用。 | - |
asset | 请求来自资源引用。 | - |
script | 请求来自没有模块系统的普通脚本标签。 | - |
这些条件也可能额外设置:
条件 | 描述 | 支持者 |
---|---|---|
module | 所有允许引用 JavaScript 的模块语法都支持 ESM。 (仅与 import 或 require 结合使用) | webpack、rollup、wmr |
esmodules | 受支持的工具总是设置此项。 | wmr |
types | 请求来自对类型声明感兴趣的 TypeScript。 | - |
(1) import
和 require
都被设置,与引用语法无关。require
总是具有较低的优先级。
以下语法将设置 import
条件:
import
声明import()
表达式<script type="module">
<link rel="preload/prefetch">
new Worker(..., { type: "module" })
import
部分import.hot.accept/decline([...])
Worklet.addModule
以下语法将设置 require
条件:
require(...)
define()
require([...])
require.resolve()
require.ensure([...])
require.context
module.hot.accept/decline([...])
<script src="...">
以下语法将设置 style
条件:
@import
<link rel="stylesheet">
以下语法将设置 asset
条件:
url()
new URL(..., import.meta.url)
<img src="...">
以下语法将设置 script
条件:
<script src="...">
script
只应在不支持模块系统时设置。当脚本被支持 CommonJs 的系统预处理时,它应该设置 require
。
此条件应用于查找可在 HTML 页面中作为脚本标签注入而无需额外预处理的 JavaScript 文件。
以下条件用于各种优化:
条件 | 描述 | 支持者 |
---|---|---|
生产环境 | 在生产环境中。 不应包含开发工具。 | webpack |
development | 在开发环境中。 应包含开发工具。 | webpack |
注意:由于并非所有工具都支持 production
和 development
,因此在未设置这些条件时,不应做任何假设。
以下条件根据目标环境设置:
条件 | 描述 | 支持者 |
---|---|---|
browser | 代码将在浏览器中运行。 | webpack、esinstall、wmr |
electron | 代码将在 Electron 中运行。(1) | webpack |
worker | 代码将在 (Web)Worker 中运行。(1) | webpack |
worklet | 代码将在 Worklet 中运行。(1) | - |
node | 代码将在 Node.js 中运行。 | Node.js、webpack、wmr(2) |
deno | 代码将在 Deno 中运行。 | - |
react-native | 代码将在 react-native 中运行。 | - |
(1) electron
、worker
和 worklet
会与 node
或 browser
结合使用,具体取决于上下文。
(2) 这为浏览器目标环境设置。
由于每个环境都有多个版本,因此适用以下准则:
node
:兼容性请参见 engines
字段。browser
:与发布包时的当前规范和第 4 阶段提案兼容。polyfill 或转译必须在消费者端处理。
deno
:待定react-native
:待定以下条件根据预处理源代码的工具而设置。
条件 | 描述 | 支持者 |
---|---|---|
webpack | 由 webpack 处理。 | webpack |
遗憾的是,Node.js 作为运行时没有 node-js
条件。这将简化为 Node.js 创建例外的情况。
以下工具支持自定义条件:
工具 | 支持 | 备注 |
---|---|---|
Node.js | 是 | 使用 --conditions CLI 参数。 |
webpack | 是 | 使用 resolve.conditionNames 配置选项。 |
rollup | 是 | 为 @rollup/plugin-node-resolve 使用 exportConditions 选项。 |
esinstall | 否 | - |
wmr | 否 | - |
对于自定义条件,建议使用以下命名方案:
<公司名称>:<条件名称>
示例:example-corp:beta
、google:internal
。
所有模式都通过包中的单个 "."
入口进行解释,但它们也可以从多个入口扩展,只需为每个入口重复该模式即可。
这些模式应作为指导而非严格的规则集。它们可以根据各个包进行调整。
这些模式基于以下目标/假设列表:
exports
应该编写为对未知的未来情况使用回退。default
条件可以用于此。根据包的意图,可能有其他更合适的方案,在这种情况下,应采用相应的模式。例如:对于命令行工具,浏览器式的未来和回退没有太大意义,在这种情况下,应改用 Node.js 类似的环境和回退。
对于复杂的用例,需要通过嵌套这些条件来组合多个模式。
这些模式适用于不使用特定于环境的 API 的包。
{
"type": "module",
"exports": "./index.js"
}
注意:仅提供 ESM 对 Node.js 有限制。这样的包只会在 Node.js >= 14 版本中且仅在使用 import
时工作。它不适用于 require()
。
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
大多数工具获取 ESM 版本。Node.js 在这里是一个例外。当使用 require()
时,它会获取 CommonJs 版本。当使用 require()
和 import
引用它时,这会导致该包的两个实例,但这不会造成损害,因为该包是无状态的。
当使用支持 ESM for require()
的工具(例如为 Node.js 打包时使用的打包器)预处理面向 Node 的代码时,module
条件被用作优化。对于这样的工具,将跳过此例外。这在技术上是可选的,但否则打包器会两次包含包的源代码。
如果您能够将包状态隔离在 JSON 文件中,也可以使用无状态模式。JSON 可以从 CommonJs 和 ESM 中消费,而不会用其他模块系统污染图。
请注意,这里的无状态也意味着类实例不通过 instanceof
进行测试,因为双重模块实例化可能导致存在两个不同的类。
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"import": "./wrapper.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
// wrapper.js
import cjs from './index.cjs';
export const A = cjs.A;
export const B = cjs.B;
在一个有状态的包中,我们必须确保该包永远不会被实例化两次。
这对大多数工具来说不是问题,但 Node.js 在这里再次是个例外。对于 Node.js,我们总是使用 CommonJs 版本,并通过 ESM 包装器在 ESM 中暴露命名导出。
我们再次将 module
条件用作优化。
{
"type": "commonjs",
"exports": "./index.js"
}
提供 "type": "commonjs"
有助于静态检测 CommonJs 文件。
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
请注意,尽管 dist-bundle.js
使用了 "type": "module"
和 .js
,但此文件并非 ESM 格式。它应该使用全局变量以允许作为脚本标签直接消费。
当一个包包含两个版本(一个用于开发,一个用于生产)时,这些模式是合理的。例如,开发版本可以包含额外的代码以提供更好的错误消息或额外的警告。
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
当支持 development
条件时,我们使用为开发而增强的版本。否则,在生产环境或模式未知时,我们使用优化版本。
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"node": "./wrapper-process-env.cjs",
"default": "./index-optimized.js"
}
}
// wrapper-process-env.cjs
if (process.env.NODE_ENV !== 'development') {
module.exports = require('./index-optimized.cjs');
} else {
module.exports = require('./index-with-devtools.cjs');
}
我们倾向于通过 production
或 development
条件进行生产/开发模式的静态检测。
Node.js 允许通过 process.env.NODE_ENV
在运行时检测生产/开发模式,因此我们在 Node.js 中将其用作回退。同步条件导入 ESM 是不可能的,并且我们不希望加载两次包,因此我们必须使用 CommonJs 进行运行时检测。
当无法检测模式时,我们回退到生产版本。
应选择一个对包支持未来环境有意义的回退环境。通常应假定为类似浏览器的环境。
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
这是一个包的示例,它针对生产和开发使用进行了优化,带有 process.env
的运行时检测,并同时发布 CommonJs 和 ESM 版本。
{
"type": "module",
"exports": {
"node": {
"development": {
"module": "./index-with-devtools.js",
"import": "./wrapper-with-devtools.js",
"require": "./index-with-devtools.cjs"
},
"production": {
"module": "./index-optimized.js",
"import": "./wrapper-optimized.js",
"require": "./index-optimized.cjs"
},
"default": "./wrapper-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
这是一个包的示例,它支持 Node.js、浏览器和 Electron,具有针对生产和开发使用进行优化的功能,带有 process.env
的运行时检测,并同时发布 CommonJs 和 ESM 版本。
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
看起来很复杂,是的。我们已经能够通过一个假设来减少一些复杂性:只有 node
需要 CommonJs 版本,并且可以通过 process.env
检测生产/开发模式。
default
导出。它在不同工具之间处理方式不同。只使用命名导出。package.json
中使用 .cjs
或 type: "commonjs"
来明确将源代码标记为 CommonJs。这使得工具可以静态检测是使用了 CommonJs 还是 ESM。这对于只支持 ESM 而不支持 CommonJs 的工具来说很重要。package.json
的其他包。data:
URL 请求。