指南章节,用于理解和掌握 webpack 提供的各种工具和特性。首先是一个 快速开始 指引。
指南会逐步带你由浅入深。本章节更多是作为一个切入点,一旦阅读完成后,你就会更加容易深入到实际的 配置 文档中。
webpack 用于编译 JavaScript 模块。一旦完成 安装,你就可以通过 webpack 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.
现在,我们将创建以下目录结构、文件和内容:
project
webpack-demo
|- package.json
|- package-lock.json
+ |- index.html
+ |- /src
+ |- index.js
src/index.js
function component() {
const element = document.createElement('div');
// lodash(目前通过一个 script 引入)对于执行这一行是必需的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>起步</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 来管理这些脚本。
首先,我们稍微调整下目录结构,创建分发代码(./dist
)文件夹用于存放分发代码,源代码(./src
)文件夹仍存放源代码。源代码是指用于书写和编辑的代码。分发代码是指在构建过程中,经过最小化和优化后产生的输出结果,最终将在浏览器中加载。调整后目录结构如下:
project
webpack-demo
|- package.json
|- package-lock.json
+ |- /dist
+ |- index.html
- |- index.html
|- /src
|- index.js
要在 index.js
中打包 lodash
依赖,我们需要在本地安装 library:
npm install --save lodash
现在,在我们的 script 中 import lodash
:
src/index.js
+import _ from 'lodash';
+
function component() {
const element = document.createElement('div');
- // lodash(目前通过一个 script 引入)对于执行这一行是必需的
+ // lodash 在当前 script 中使用 import 引入
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
现在,我们将会打包所有脚本,我们必须更新 index.html
文件。由于现在是通过 import
引入 lodash,所以要将 lodash <script>
删除,然后修改另一个 <script>
标签来加载 bundle,而不是原始的 ./src
文件:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>起步</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 能够利用这些信息去构建依赖图,然后使用图生成一个优化过的 bundle,并且会以正确顺序执行。
可以这样说,执行 npx webpack
,会将我们的脚本 src/index.js
作为 入口起点,也会生成 dist/main.js
作为 输出。Node 8.2/npm 5.2.0 以上版本提供的 npx
命令,可以运行在初次安装的 webpack package 中的 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'
。
ES2015 中的 import
和 export
语句已经被标准化。虽然大多数浏览器还无法支持它们,但是 webpack 却能够提供开箱即用般的支持。
事实上,webpack 在幕后会将代码 “转译”,以便旧版本浏览器可以执行。如果你检查 dist/main.js
,你可以看到 webpack 具体如何实现,这是独创精巧的设计!除了 import
和 export
,webpack 还能够很好地支持多种其他模块语法,更多信息请查看 模块 API。
注意,webpack 不会更改代码中除 import
和 export
语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack loader 系统 中使用了一个像是 Babel 的 transpiler(转译器)。
在 webpack v4 中,可以无须任何配置,然而大多数项目会需要很复杂的设置,这就是为什么 webpack 仍然要支持 配置文件。这比在 terminal(终端) 中手动输入大量命令要高效的多,所以让我们创建一个配置文件:
project
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
比起 CLI 这种简单直接的使用方式,配置文件具有更多的灵活性。我们可以通过配置方式指定 loader 规则(loader rule)、plugin(插件)、resolve 选项,以及许多其他增强功能。更多详细信息请查看 配置文档。
考虑到用 CLI 这种方式来运行本地的 webpack 副本并不是特别方便,我们可以设置一个快捷方式。调整 package.json 文件,添加一个 npm script:
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
命令。注意,使用 npm scripts
,我们可以像使用 npx
那样通过模块名引用本地安装的 npm packages。这是大多数基于 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 来管理资源,例如 images、fonts。此刻你的项目看起来应该如下:
project
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"。现在我们尝试混合一些其他资源,比如 images,看看 webpack 如何处理。
在 webpack 出现之前,前端开发人员会使用 grunt 和 gulp 等工具来处理资源,并将它们从 /src
文件夹移动到 /dist
或 /build
目录中。JavaScript 模块也遵循同样方式,但是,像 webpack 这样的工具,将动态打包所有依赖(创建所谓的 依赖图(dependency graph))。这是极好的创举,因为现在每个模块都可以明确表述它自身的依赖,可以避免打包未使用的模块。
webpack 最出色的功能之一就是,除了引入 JavaScript,还可以通过 loader 或内置的 Asset Modules 引入任何其他类型的文件。也就是说,以上列出的那些 JavaScript 的优点(例如显式依赖),同样可以用来构建 web 站点或 web 应用程序中的所有非 JavaScript 内容。让我们从 CSS 开始起步,或许你可能已经熟悉了下面这些设置。
在开始之前,让我们对项目做一个小的修改:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
- <title>起步</title>
+ <title>管理资源</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
配置 中添加这些 loader:
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'],
+ },
+ ],
+ },
};
模块 loader 可以链式调用。链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。
应保证 loader 的先后顺序:'style-loader'
在前,而 'css-loader'
在后。如果不遵守此约定,webpack 可能会抛出错误。
这使你可以在依赖于此样式的 js 文件中 import './style.css'
。现在,在此模块执行过程中,含有 CSS 字符串的 <style>
标签,将被插入到 html 文件的 <head>
中。
我们尝试一下,通过在项目中添加一个新的 style.css
文件,并将其 import 到我们的 index.js
中:
project
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());
现在运行 build 命令:
$ 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 标签。它应该包含 style 块元素,也就是我们在 index.js
中 import 的 css 文件中的样式。
注意,在多数情况下,你也可以进行 压缩 CSS,以便在生产环境中节省加载时间。最重要的是,现有的 loader 可以支持任何你可以想到的 CSS 风格 - postcss, sass 和 less 等。
假如,现在我们正在下载 CSS,但是像 background 和 icon 这样的图像,要如何处理呢?在 webpack 5 中,可以使用内置的 Asset Modules,我们可以轻松地将这些内容混入我们的系统中:
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')
。loader 会识别这是一个本地文件,并将 './my-image.png'
路径,替换为 output
目录中图像的最终路径。而 html-loader 以相同的方式处理 <img src="./my-image.png" />
。
我们向项目添加一个图像,然后看它是如何工作的,你可以使用任何你喜欢的图像:
project
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');
+ // 将图像添加到我们已经存在的 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
如果一切顺利,你现在应该看到你的 icon 图标成为了重复的背景图,以及 Hello webpack
文本旁边的 img
元素。如果检查此元素,你将看到实际的文件名已更改为 29822eaa871e8eadeaa4.png
。这意味着 webpack 在 src
文件夹中找到我们的文件,并对其进行了处理!
那么,像字体这样的其他资源如何处理呢?使用 Asset Modules 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,也包括字体。让我们更新 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',
+ },
],
},
};
在项目中添加一些字体文件:
project
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
配置好 loader 并将字体文件放在合适的位置后,你可以通过一个 @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。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 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'],
+ },
],
},
};
在项目中添加一些数据文件:
project
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
。查看开发者工具中的控制台,你应该能够看到导入的数据会被打印出来!
// 没有警告
import data from './data.json';
// 显示警告,规范不允许这样做。
import { foo } from './data.json';
通过使用 自定义 parser 替代特定的 webpack loader,可以将任何 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
的 packages:
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
目录下。只要你已经安装过全部 外部依赖 ,并且 已经在配置中定义过相同的 loader ,那么项目应该能够良好运行。
但是,假如你只能被局限在旧有开发方式,或者你有一些在多个组件(视图、模板、模块等)之间共享的资源。你仍然可以将这些资源存储在一个基本目录(base directory)中,甚至配合使用 alias 来使它们更方便 import 导入
。
对于下篇指南,我们无需使用本指南中所有用到的资源,因此我们会进行一些清理工作,以便为下篇指南 管理输出 做好准备:
project
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); // `JSON5 Example`
-console.log(json.owner.name); // output `Tom Preston-Werner`
function component() {
const element = document.createElement('div');
- // lodash,现在通过 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());
And remove those dependencies we added before:
npm uninstall css-loader csv-loader json5 style-loader toml xml-loader yamljs
我们继续移步到 管理输出
到目前为止,我们都是在 index.html
文件中手动引入所有资源,然而随着应用程序增长,并且一旦开始 在文件名中使用 hash 并输出 多个 bundle,如果继续手动管理 index.html
文件,就会变得困难起来。然而,通过一些插件可以使这个过程更容易管控。
首先,调整一下我们的项目:
project
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>管理资源</title>
+ <title>管理输出</title>
+ <script src="./print.bundle.js"></script>
</head>
<body>
- <script src="bundle.js"></script>
+ <script src="./index.bundle.js"></script>
</body>
</html>
现在调整配置。我们将在 entry 添加 src/print.js
作为新的入口起点(print
),然后修改 output,以便根据入口起点定义的名称,动态地产生 bundle 名称:
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
,就可以看到在点击按钮时会发生什么。
但是,如果我们更改了我们的一个入口起点的名称,甚至添加了一个新的入口,会发生什么?会在构建时重新命名生成的 bundle,但是我们的 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: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
在我们构建之前,你应该了解,虽然在 dist/
文件夹我们已经有了 index.html
这个文件,然而 HtmlWebpackPlugin
还是会默认生成它自己的 index.html
文件。也就是说,它会用新生成的 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
创建了一个全新的文件,所有的 bundle 会自动添加到 html 中。
如果你想要了解 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 通过 manifest,可以追踪所有模块到输出 bundle 之间的映射。如果你想要知道如何以其他方式来控制 webpack 输出
,了解 manifest 是个好的开始。
通过 WebpackManifestPlugin
插件,可以将 manifest 数据提取为一个 json 文件以供使用。
我们不会在此展示一个如何在项目中使用此插件的完整示例,你可以在 manifest 概念页面深入阅读,以及在 缓存 指南中,了解它与长效缓存有何关系。
现在,你已经了解如何向 HTML 动态添加 bundle,让我们深入 开发环境 指南。或者如果你想要深入更多相关高级话题,我们推荐你前往 代码分离 指南。
如果你一直跟随之前的指南,应该对一些 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 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。例如,如果将三个源文件(a.js
, b.js
和 c.js
)打包到一个 bundle(bundle.js
)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js
。你可能需要准确地知道错误来自于哪个源文件,所以这种提示这通常不会提供太多帮助。
为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。如果一个错误来自于 b.js
,source map 就会明确的告诉你。
source map 有许多 可用选项,请务必仔细阅读它们,以便可以根据需要进行配置。
对于本指南,我们将使用 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 "watch" 依赖图中所有文件的更改。如果其中一个文件被更新,代码将被重新编译,所以你不必再去手动运行整个构建。
我们添加一个用于启动 webpack watch mode 的 npm scripts:
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 是如何编译代码。
然而,你会发现并没有退出命令行。这是因为此 script 当前还在 watch 你的文件。
现在,webpack 观察文件的同时,先移除我们之前加入的错误:
src/print.js
export default function printMe() {
- cosnole.log('I get called from print.js!');
+ console.log('I get called from print.js!');
}
现在,保存文件并检查 terminal(终端) 窗口。应该可以看到 webpack 自动地重新编译修改后的模块!
唯一的缺点是,为了看到修改后的实际效果,你需要刷新浏览器。如果能够自动刷新浏览器就更好了,因此接下来我们会尝试通过 webpack-dev-server
实现此功能。
webpack-dev-server
为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能。设置如下:
npm install --save-dev webpack-dev-server
修改配置文件,告知 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
目录下的文件 serve 到 localhost:8080
下。(译注:serve,将资源作为 server 的可访问文件)
我们添加一个可以直接运行 dev server 的 script:
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 server 将在编译代码后自动重新加载。试试看!
webpack-dev-server
具有许多可配置的选项。关于其他更多配置,请查看 配置文档。
webpack-dev-middleware
是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。webpack-dev-server
在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。下面是一个 webpack-dev-middleware 配合 express server 的示例。
首先,安装 express
和 webpack-dev-middleware
:
npm install --save-dev express webpack-dev-middleware
现在,我们需要调整 webpack 配置文件,以确保 middleware(中间件) 功能能够正确启用:
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: '/',
},
};
我们将会在 server 脚本使用 publicPath
,以确保文件资源能够正确地 serve 在 http://localhost:3000
下,稍后我们会指定 port number(端口号)。接下来是设置自定义 express
server:
project
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);
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
现在,添加一个 npm script,以使我们更方便地运行 server:
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"
}
}
现在,在 terminal(终端) 中执行 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.
现在,打开浏览器,访问 http://localhost:3000
。应该看到 webpack 应用程序已经运行!
使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有 "safe write(安全写入)" 功能,会影响重新编译。
在一些常见的编辑器中禁用此功能,查看以下列表:
atomic_save: 'false'
。Preferences > Appearance & Behavior > System Settings
中取消选中 "Use safe write"。:set backupcopy=yes
。现在,你已经学会了如何自动编译代码,并运行一个简单的 development server,查看下一个指南,其中将介绍代码分割(Code Splitting)。
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常用的代码分离方法有三种:
entry
配置手动地分离代码。SplitChunksPlugin
去重和分离 chunk。这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):
project
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
正如前面提到的,这种方式存在一些隐患:
以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js
中也引入过 lodash
,这样就在两个 bundle 中造成重复引用。在下一章节会移除重复的模块。
配置 dependOn
option 选项,这样可以在多个 chunk 之间共享模块:
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
插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 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
分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。执行 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
以下是由社区提供,一些对于代码分离很有帮助的 plugin 和 loader:
mini-css-extract-plugin
: 用于将 CSS 从主应用程序中分离。当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import()
语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 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',
- },
- },
};
我们将更新我们的项目,移除现在未使用的文件:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
现在,我们不再使用 statically import(静态导入) lodash
,而是通过 dynamic import(动态导入) 来分离出一个 chunk:
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 模块时,将不再解析为 module.exports
的值,而是为 CommonJS 模块创建一个 artificial namespace 对象,更多有关背后原因的信息,请阅读 webpack 4: import() and CommonJs。
让我们执行 webpack,查看 lodash
是否会分离到一个单独的 bundle:
...
[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,因此它可以和 async
函数一起使用。下面是如何通过 async 函数简化代码:
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 v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 "resource hint(资源提示)",来告知浏览器:
下面这个 prefetch 的简单示例中,有一个 HomePage
组件,其内部渲染一个 LoginButton
组件,然后在点击后按需加载 LoginModal
组件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
这会生成 <link rel="prefetch" href="login-modal-chunk.js">
并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js
文件。
与 prefetch 指令相比,preload 指令有许多不同之处:
下面这个简单的 preload 示例中,有一个 Component
,依赖于一个较大的 library,所以应该将其分离到一个独立的 chunk 中。
我们假想这里的图表组件 ChartComponent
组件需要依赖一个体积巨大的 ChartingLibrary
库。它会在渲染时显示一个 LoadingIndicator(加载进度条)
组件,然后立即按需导入 ChartingLibrary
:
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
在页面中使用 ChartComponent
时,在请求 ChartComponent.js 的同时,还会通过 <link rel="preload">
请求 charting-library-chunk。假定 page-chunk 体积比 charting-library-chunk 更小,也更快地被加载完成,页面此时就会显示 LoadingIndicator(加载进度条)
,等到 charting-library-chunk
请求完成,LoadingIndicator 组件才消失。这将会使得加载时间能够更短一点,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。
有时你需要自己控制预加载。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下很有用。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// 在发生错误时做一些处理
// 例如,我们可以在网络错误的情况下重试请求
});
如果在 webpack 开始加载该脚本之前脚本加载失败(如果该脚本不在页面上,webpack 只是创建一个 script 标签来加载其代码),则该 catch 处理程序将不会启动,直到 chunkLoadTimeout 未通过。此行为可能是意料之外的。但这是可以解释的 - webpack 不能抛出任何错误,因为 webpack 不知道那个脚本失败了。Webpack 将在错误发生后立即将 onerror 处理脚本添加到 script 中。
为了避免上述问题,你可以添加自己的 onerror 处理脚本,将会在错误发生时移除该 script。
<script
src="https://example.com/dist/dynamicComponent.js"
async
onerror="this.remove()"
></script>
在这种情况下,错误的 script 将被删除。Webpack 将创建自己的 script,并且任何错误都将被处理而没有任何超时。
一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。 官方分析工具 是一个不错的开始。还有一些其他社区支持的可选项:
接下来,查看 延迟加载 来学习如何在实际一个真实应用程序中使用 import()
的具体示例,以及查看 缓存 来学习如何有效地分离代码。
以上,我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist
目录,然后把打包后的内容放置在此目录中。只要 /dist
目录中的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。
此指南的重点在于通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。
我们可以通过替换 output.filename
中的 substitutions 设置,来定义输出文件的名称。webpack 提供了一种使用称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。其中,[contenthash]
substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash]
也会发生变化。
这里使用 起步 中的示例和 管理输出 中的 plugins
插件来作为项目基础,所以我们依然不必手动地维护 index.html
文件:
project
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,
},
};
使用此配置,然后运行我们的 build script npm run build
,产生以下输出:
...
Asset Size Chunks Chunk Names
main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
可以看到,bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后再次运行构建,我们以为文件名会保持不变。然而,如果我们真的运行,可能会发现情况并非如此:
...
Asset Size Chunks Chunk Names
main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
这也是因为 webpack 在入口 chunk 中,包含了某些 boilerplate(引导模板),特别是 runtime 和 manifest。(译注:boilerplate 指 webpack 运行时的引导代码)
正如我们在 代码分离 中所学到的,SplitChunksPlugin
可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可使用 optimization.runtimeChunk
选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single
来为所有 chunk 创建一个 runtime bundle:
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',
+ },
};
再次构建,然后查看提取出来的 runtime
bundle:
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
将第三方库(library)(例如 lodash
或 react
)提取到单独的 vendor
chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致。
这可以通过使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin
插件的 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
bundle:
...
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 bytes
!
在项目中再添加一个模块 print.js
:
project
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 是由当前 script 脚本 import 进来的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
再次运行构建,然后我们期望的是,只有 main
bundle 的 hash 发生变化,然而……
...
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]
...
……我们可以看到这三个文件的 hash 都变化了。这是因为每个 module.id
会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。简要概括:
main
bundle 会随着自身的新增内容的修改,而发生变化。vendor
bundle 会随着自身的 module.id
的变化,而发生变化。manifest
runtime 会因为现在包含一个新模块的引用,而发生变化。第一个和最后一个都是符合预期的行为,vendor
hash 发生变化是我们要修复的。我们将 optimization.moduleIds
设置为 'deterministic'
:
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
hash 都应该保持一致:
...
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 是由当前 script 脚本 import 进来的
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
bundle 文件名称,都是 55e79e5927a639d21a1b
。
缓存可能很复杂,但是从应用程序或站点用户可以获得的收益来看,这值得付出努力。想要了解更多信息,请查看下面 进一步阅读 部分。
除了打包应用程序,webpack 还可以用于打包 JavaScript library。以下指南适用于希望简化打包策略的 library 作者。
假设我们正在编写一个名为 webpack-numbers
的小的 library,可以将数字 1 到 5 转换为文本表示,反之亦然,例如将 2 转换为 'two'。
基本的项目结构可能如下所示:
project
+ |- 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 标签使用它:
<script src="https://example.org/webpack-numbers.js"></script>
<script>
window.webpackNumbers.wordToNum('Five');
</script>
然而它只能通过被 script 标签引用而发挥作用,它不能运行在 CommonJS、AMD、Node.js 等环境中。
作为一个库作者,我们希望它能够兼容不同的环境,也就是说,用户应该能够通过以下方式使用打包后的库:
CommonJS module require:
const webpackNumbers = require('webpack-numbers');
// ...
webpackNumbers.wordToNum('Two');
AMD module require:
require(['webpackNumbers'], function (webpackNumbers) {
// ...
webpackNumbers.wordToNum('Two');
});
script tag:
<!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',
+ library: {
+ name: 'webpackNumbers',
+ type: 'umd',
+ },
},
};
现在 webpack 将打包一个库,其可以与 CommonJS、AMD 以及 script 标签使用。
现在,如果执行 webpack
,你会发现创建了一个体积相当大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash
当作 peerDependency
。也就是说,consumer(使用者) 应该已经安装过 lodash
。因此,你就可以放弃控制此外部 library ,而是将控制权让给使用 library 的 consumer。
这可以使用 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: '_',
+ },
+ },
};
这意味着你的 library 需要一个名为 lodash
的依赖,这个依赖在 consumer 环境中必须存在且可用。
对于想要实现从一个依赖中调用多个文件的那些 library:
import A from 'library/one';
import B from 'library/two';
// ...
无法通过在 externals 中指定整个 library
的方式,将它们从 bundle 中排除。而是需要逐个或者使用一个正则表达式,来排除它们。
module.exports = {
//...
externals: [
'library/one',
'library/two',
// 匹配以 "library/" 开始的所有依赖
/^library\/.+$/,
],
};
遵循 生产环境 指南中提到的步骤,来优化生产环境下的输出结果。那么,我们还需要将生成 bundle 的文件路径,添加到 package.json
中的 main
字段中。
package.json
{
...
"main": "dist/webpack-numbers.js",
...
}
或者,按照这个 指南,将其添加为标准模块:
{
...
"module": "src/index.js",
...
}
这里的 key(键) main
是参照 package.json
标准,而 module
是参照 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。
现在,你可以 将其发布为一个 npm package,并且在 unpkg.com 找到它,并分发给你的用户。
想要消除 webpack.config.js
在 开发环境 和 生产环境 之间的差异,你可能需要环境变量(environment variable)。
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 更新到最新版本,也有助于提高性能。除此之外,将你的 package 管理工具(例如 npm
或者 yarn
)更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。
将 loader 应用于最少数量的必要模块。而非如下:
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
},
],
},
};
通过使用 include
字段,仅将 loader 应用在实际需要将其转换的模块:
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
每个额外的 loader/plugin 都有其启动时间。尽量少地使用工具。
以下步骤可以提高解析速度:
resolve.modules
, resolve.extensions
, resolve.mainFiles
, resolve.descriptionFiles
中条目数量,因为他们会增加文件系统调用的次数。npm link
或者 yarn link
),可以设置 resolve.symlinks: false
。resolve.cacheWithContext: false
。使用 DllPlugin
为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。
减少编译结果的整体大小,以提高构建性能。尽量保持 chunk 体积小。
SplitChunksPlugin
。SplitChunksPlugin
,并开启 async
模式。thread-loader
可以将非常消耗资源的 loader 分流给一个 worker pool。
在 webpack 配置中使用 cache
选项。使用 package.json
中的 "postinstall"
清除缓存目录。
对它们进行概要分析,以免在此处引入性能问题。
将 ProgressPlugin
从 webpack 中删除,可以缩短构建时间。请注意,ProgressPlugin
可能不会为快速构建提供太多价值,因此,请权衡利弊再使用。
以下步骤对于 开发环境 特别有帮助。
使用 webpack 的 watch mode(监听模式)。而不使用其他工具来 watch 文件和调用 webpack 。内置的 watch mode 会记录时间戳并将此信息传递给 compilation 以使缓存失效。
在某些配置环境中,watch mode 会回退到 poll mode(轮询模式)。监听许多文件会导致 CPU 大量负载。在这些情况下,可以使用 watchOptions.poll
来增加轮询的间隔时间。
下面几个工具通过在内存中(而不是写入磁盘)编译和 serve 资源来提高性能:
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
变体配置进行增量编译。某些 utility, plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin
来 minify(压缩) 和 mangle(混淆破坏) 代码是没有意义的。通常在开发环境下,应该排除以下这些工具:
TerserPlugin
[fullhash]
/[chunkhash]
/[contenthash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
Webpack 只会在文件系统中输出已经更新的 chunk。某些配置选项(HMR, output.chunkFilename
的 [name]
/[chunkhash]/[contenthash]
,[fullhash]
)来说,除了对已经更新的 chunk 无效之外,对于 entry chunk 也不会生效。
确保在生成 entry chunk 时,尽量减少其体积以提高性能。下面的配置为运行时代码创建了一个额外的 chunk,所以它的生成代价较低:
module.exports = {
// ...
optimization: {
runtimeChunk: true,
},
};
Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:
module.exports = {
// ...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
};
Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo
设置中关闭:
module.exports = {
// ...
output: {
pathinfo: false,
},
};
Node.js v8.9.10 - v9.11.1 中的 ES2015 Map
和 Set
实现,存在 性能回退。Webpack 大量地使用这些数据结构,因此这次回退也会影响编译时间。
之前和之后的 Node.js 版本不受影响。
你可以为 loader 传入 transpileOnly
选项,以缩短使用 ts-loader
时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin
。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。
module.exports = {
// ...
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
};
以下步骤对于 生产环境 特别有帮助。
source map 相当消耗资源。你真的需要它们?
下列工具存在某些可能会降低构建性能的问题:
fork-ts-checker-webpack-plugin
进行类型检查。ts-loader
时,设置 happyPackMode: true
/ transpileOnly: true
。node-sass
中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 thread-loader
时,需要设置 workerParallelJobs: 2
。Webpack 能够为其加载的所有脚本添加 nonce
。要启用此功能,需要在引入的入口脚本中设置一个 __webpack_nonce__
变量。应该为每个唯一的页面视图生成和提供一个唯一的基于 hash 的 nonce
,这就是为什么 __webpack_nonce__
要在入口文件中指定,而不是在配置中指定的原因。注意,__webpack_nonce__
应该是一个 base64 编码的字符串。
在 entry 文件中:
// ...
__webpack_nonce__ = 'c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=';
// ...
注意,默认情况下不启用 CSP。需要与文档(document)一起发送相应的 CSP
header 或 meta 标签 <meta http-equiv="Content-Security-Policy" ...>
,以告知浏览器启用 CSP。以下是一个包含 CDN 白名单 URL 的 CSP header 的示例:
Content-Security-Policy: default-src 'self'; script-src 'self'
https://trusted.cdn.com;
有关 CSP 和 nonce
属性的更多信息,请查看页面底部的 延伸阅读 部分。
webpack 还能够使用 Trusted Types 来加载动态构建的脚本,遵守 CSP require-trusted-types-for
指令的限制。可查看 output.trustedTypes
配置项。
如果你在开发一个更加高级的项目,并且使用 Vagrant 来实现在虚拟机(Virtual Machine)上运行你的开发环境,那你可能会需要在虚拟机中运行 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
文件。其中的 script
标签应当指向你的 bundle。如果没有在配置中指定 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 访问服务器。所以我们需要修改 --host
参数,
以允许在我们的宿主 PC 上访问。
webpack-dev-server
会在包中引入一个脚本,此脚本连接到 WebSocket,这样可以在任何文件修改时,触发重新加载应用程序。
--client-web-socket-url
标志可以确保脚本知道从哪里查找 WebSocket。默认情况下,服务器会使用 8080
端口,因此也需要在这里指定。
--watch-options-poll
可以确保 webpack 能够检测到文件更改。webpack 默认会监听文件系统触发的相关事件,但是 VirtualBox 使用默认配置会有许多问题。
现在服务器应该能够通过 http://10.10.10.61:8080
访问了。修改 app.js
,应用程序就会实时重新加载。
为了更好的模拟类生产环境(production-like environment),还可以用 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
访问服务,这点关系不大,因为 ngnix 能够使得你的 PC 电脑能访问到服务器。
我们能够从静态 IP 访问 Vagrant box,然后使 webpack-dev-server
可以公开访问,以便浏览器可以访问到它。然后,我们解决了 VirtualBox 不发送到文件系统事件的常见问题,此问题会导致服务器无法重新加载文件更改。
es6 modules
commonjs
amd
如果你的 request 含有表达式(expressions),就会创建一个上下文(context),因为在编译时(compile time)并不清楚 具体 导入哪个模块。
示例,考虑到我们有包含 .ejs
文件的如下目录结构:
example_directory
│
└───template
│ │ table.ejs
│ │ table-row.ejs
│ │
│ └───directory
│ │ another.ejs
当台下的 require()
调用被评估解析:
require('./template/' + name + '.ejs');
webpack 解析 require()
调用,然后提取出如下一些信息:
Directory: ./template
Regular expression: /^.*\.ejs$/
context module
会生成一个 context module(上下文模块)。它包含 目录下的所有模块 的引用,如果一个 request 符合正则表达式,就能 require 进来。该 context module 包含一个 map(映射)对象,会把 requests 翻译成对应的模块 id。(译者注:request 参考概念术语 )
示例 map(映射):
{
"./table.ejs": 42,
"./table-row.ejs": 43,
"./directory/another.ejs": 44
}
此 context module 还包含一些访问这个 map 对象的 runtime 逻辑。
这意味着 webpack 能够支持动态地 require,但会导致所有可能用到的模块都包含在 bundle 中。
你还可以通过 require.context()
函数来创建自己的 context。
可以给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
Webpack 会在构建中解析代码中的 require.context()
。
语法如下:
require.context(
directory,
(useSubdirectories = true),
(regExp = /^\.\/.*$/),
(mode = 'sync')
);
示例:
require.context('./test', false, /\.test\.js$/);
//(创建出)一个 context,其中文件来自 test 目录,request 以 `.test.js` 结尾。
require.context('../', true, /\.stories\.js$/);
// (创建出)一个 context,其中所有文件都来自父文件夹及其所有子级文件夹,request 以 `.stories.js` 结尾。
一个 context module 会导出一个(require)函数,此函数可以接收一个参数:request。
此导出函数有三个属性:resolve
, keys
, id
。
resolve
是一个函数,它返回 request 被解析后得到的模块 id。keys
也是一个函数,它返回一个数组,由所有可能被此 context module 处理的请求(译者注:参考下面第二段代码中的 key)组成。如果想引入一个文件夹下面的所有文件,或者引入能匹配一个正则表达式的所有文件,这个功能就会很有帮助,例如:
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$/));
// 在构建时(build-time),所有被 require 的模块都会被填充到 cache 对象中。
id
是 context module 的模块 id. 它可能在你使用 module.hot.accept
时会用到。本指南介绍了安装 webpack 的各种方法。
在开始之前,请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本(LTS - Long Term Support),是理想的起步。 使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能, 或者缺少相关 package。
最新的 webpack 正式版本是:
要安装最新版本或特定版本,请运行以下命令之一:
npm install --save-dev webpack
# 或指定版本
npm install --save-dev webpack@<version>
如果你使用 webpack v4+ 版本,并且想要在命令行中调用 webpack
,你还需要安装 CLI。
npm install --save-dev webpack-cli
对于大多数项目,我们建议本地安装。这可以在引入重大更新(breaking change)版本时,更容易分别升级项目。
通常会通过运行一个或多个 npm scripts 以在本地 node_modules
目录中查找安装的 webpack,
来运行 webpack:
"scripts": {
"build": "webpack --config webpack.config.js"
}
通过以下 NPM 安装方式,可以使 webpack
在全局环境下可用:
npm install --global webpack
如果你热衷于使用最新版本的 webpack,你可以使用以下命令安装 beta 版本, 或者直接从 webpack 的仓库中安装:
npm install --save-dev webpack@next
# 或特定的 tag/分支
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...');
}
console
[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
在 Node.js API 中使用 webpack dev server 时,不要将 dev server 选项放在 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(); // 存储 element,以在 print.js 修改时重新渲染
+ 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(); // 重新渲染 "component",以便更新 click 事件处理函数
+ document.body.appendChild(element);
})
}
这仅仅是一个示例,还有很多让人易于犯错的情况。 幸运的是,有很多 loader(下面会提到一些)可以使得模块热替换变得更加容易。
借助于 style-loader
,使用模块热替换来加载 CSS 实际上极其简单。此 loader 在幕后使用了 module.hot.accept
,在 CSS 依赖模块更新之后,会将其 patch(修补) 到 <style>
标签中。
首先使用以下命令安装两个 loader :
npm install --save-dev style-loader css-loader
然后更新配置文件,使用这两个 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,
},
};
热加载样式表也可以通过将它们引入一个模块来实现:
project
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
的 style 改为 background: red;
,你应该可以立即看到页面的背景颜色随之更改,而无需完全刷新。
styles.css
body {
- background: blue;
+ background: red;
}
社区还提供许多其他 loader 和示例,可以使 HMR 与各种框架和库平滑地进行交互……
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import
和 export
。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。
webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json
的 "sideEffects"
属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
在我们的项目中添加一个新的通用模块文件 src/math.js
,并导出两个函数:
project
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,以确定 bundle 不会被压缩:
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());
注意,我们没有从 src/math.js
模块中 import
另外一个 square
方法。这个函数就是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 export
。现在运行 npm script npm run build
,并查看输出的 bundle:
dist/bundle.js (around lines 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
,但它仍然被包含在 bundle 中。我们将在下一节解决这个问题。
在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。
通过 package.json 的 "sideEffects"
属性,来实现这种方式。
{
"name": "your-project",
"sideEffects": false
}
如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false
,来告知 webpack 它可以安全地删除未用到的 export。
如果你的代码确实有一些副作用,可以改为提供一个数组:
{
"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"]
}
最后,还可以在 module.rules
配置选项 中设置 "sideEffects"
。
sideEffects
sideEffects
和 usedExports
(更多被认为是 tree shaking)是两种不同的优化方式。
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。它给一个语句标记为没有副作用。就这样一个简单的改变就能够使下面的代码被 tree-shake:
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 个模块被导入到 bundle 中:
index.js
: 基本为空的configure.js
components/Button.js
components/Button.css
在这次的优化后,其它的优化项目都可以应用。例如:从 Button.js
导出 的buttonFrom
和 buttonsFrom
也没有被使用。usedExports
优化会捡起这些代码而且 terser 会能够从 bundle 中把这些语句摘除出来。
模块合并也会应用。所以这 4 个模块,加上入口的模块(也可能有更多的依赖)会被合并。index.js
最终没有生成代码.
是可以告诉 webpack 一个函数调用是无副作用的,只要通过 /*#__PURE__*/
注释。它可以被放到函数调用之前,用来标记它们是无副作用的(pure)。传到函数中的入参是无法被刚才的注释所标记,需要单独每一个标记才可以。如果一个没被使用的变量定义的初始值被认为是无副作用的(pure),它会被标记为死代码,不会被执行且会被压缩工具清除掉。当 optimization.innerGraph
被设置成 true
时这个行为会被启用。
file.js
/*#__PURE__*/ double(55);
通过 import
和 export
语法,我们已经找出需要删除的“未引用代码(dead code)”,然而,不仅仅是要找出,还要在 bundle 中删除它们。为此,我们需要将 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 都已经被 minify(压缩) 和 mangle(混淆破坏),但是如果仔细观察,则不会看到引入 square
函数,但能看到 cube
函数的混淆破坏版本(function r(e){return e*e*e}n.a=r
)。现在,随着 minification(代码压缩) 和 tree shaking,我们的 bundle 减小几个字节!虽然,在这个特定示例中,可能看起来没有减少很多,但是,在有着复杂依赖树的大型应用程序上运行 tree shaking 时,会对 bundle 产生显著的体积优化。
我们学到为了利用 tree shaking 的优势, 你必须...
import
和 export
)。package.json
文件中,添加 "sideEffects"
属性。mode
为 "production"
的配置项以启用更多优化项,包括压缩代码与 tree shaking。你可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
如果你对优化输出很感兴趣,请进入到下个指南,来了解 生产环境 构建的详细细节。
在本指南中,我们将深入一些最佳实践和工具,将站点或应用程序构建到生产环境中。
development(开发环境) 和 production(生产环境) 这两个环境下的构建目标存在着巨大差异。在开发环境中,我们需要:强大的 source map 和一个有着 live reloading(实时重新加载) 或 hot module replacement(热模块替换) 能力的 localhost server。而生产环境目标则转移至其他方面,关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
虽然,以上我们将 生产环境 和 开发环境 做了细微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个 "common(通用)" 配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge
的工具。此工具会引用 "common" 配置,因此我们不必再在环境特定(environment-specific)的配置中编写重复代码。
我们先从安装 webpack-merge
开始,并将之前指南中已经成型的那些代码进行分离:
npm install --save-dev webpack-merge
project
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
(强大的 source map)和 devServer
配置。最后,在 webpack.prod.js
中,我们将 mode
设置为 production
,其中会引入之前在 tree shaking 指南中介绍过的 TerserPlugin
。
注意,在环境特定的配置中使用 merge()
功能,可以很方便地引用 webpack.dev.js
和 webpack.prod.js
中公用的 common 配置。webpack-merge
工具提供了各种 merge(合并) 高级功能,但是在我们的用例中,无需用到这些功能。
现在,我们把 scripts
重新指向到新配置。让 npm start
script 中 webpack-dev-server
, 使用 webpack.dev.js
, 而让 npm run build
script 使用 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"
}
}
随便运行下这些脚本,然后查看输出结果的变化,然后我们会继续添加一些 生产环境 配置。
许多 library 通过与 process.env.NODE_ENV
环境变量关联,以决定 library 中应该引用哪些内容。例如,当process.env.NODE_ENV
没有被设置为 'production'
时,某些 library 为了使调试变得容易,可能会添加额外的 log(日志记录) 和 test(测试) 功能。并且,在使用 process.env.NODE_ENV === 'production'
时,一些 library 可能针对具体用户的环境,删除或添加一些重要代码,以进行代码执行方面的优化。从 webpack v4 开始, 指定 mode
会自动地配置 DefinePlugin
:
webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});
如果你正在使用像 react
这样的 library,那么在添加此 DefinePlugin 插件后,你应该看到 bundle 大小显著下降。还要注意,任何位于 /src
的本地代码都可以关联到 process.env.NODE_ENV 环境变量,所以以下检查也是有效的:
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+ will minify your code by default in production mode
.
注意,虽然生产环境下默认使用 TerserPlugin
,并且也是代码压缩方面比较好的选择,但是还有一些其他可选择项。以下有几个同样很受欢迎的插件:
如果决定尝试一些其他压缩插件,确保新插件也会按照 tree shake 指南中所陈述的具有删除未引用代码(dead code)的能力,并将它作为 optimization.minimizer
。
我们鼓励你在生产环境中启用 source map,因为它们对 debug(调试源码) 和运行 benchmark tests(基准测试) 很有帮助。虽然有着如此强大的功能,然而还是应该针对生产环境用途,选择一个可以快速构建的推荐配置(更多选项请查看 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 进行压缩会非常重要,请查看 在生产环境下压缩 章节。
上述许多选项都可以通过命令行参数进行设置。例如,optimize-minimize
可以使用 --optimization-minimize
进行设置,mode
可以使用 --mode
进行设置。运行 npx webpack --help=verbose
可以查看所有关于 CLI 的可用参数。
虽然这种简写方式很有用处,但我们还是建议通过 webpack 配置文件的方式进行使用,这样可以提高可配置能力。
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
我们在代码分离中的例子基础上,进一步做些调整来说明这个概念。那里的代码确实会在脚本运行的时候产生一个分离的代码块 lodash.bundle.js
,在技术概念上“懒加载”它。问题是加载这个包并不需要用户的交互 - 意思是每次加载页面的时候都会请求它。这样做并没有对我们有很多帮助,还会对性能产生负面影响。
我们试试不同的做法。我们增加一个交互,当用户点击按钮的时候用 console 打印一些文字。但是会等到第一次交互的时候再加载那个代码块(print.js
)。为此,我们返回到代码分离的例子中,把 lodash
放到主代码块中,重新运行 代码分离 中的代码 final Dynamic Imports example。
project
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]
...
许多框架和类库对于如何用它们自己的方式来实现(懒加载)都有自己的建议。这里有一些例子:
ECMAScript 模块(ESM)是在 Web 中使用模块的规范。 所有现代浏览器均支持此功能,同时也是在 Web 中编写模块化代码的推荐方式。
webpack 支持处理 ECMAScript 模块以优化它们。
关键字 export
允许将 ESM 中的内容暴露给其他模块:
export const CONSTANT = 42;
export let variable = 42;
// 对外暴露的变量为只读
// 无法从外部修改
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';
// 导入由其他模块导出的“绑定”
// 这些绑定是动态的. 这里并非获取到了值的副本
// 而是当将要访问“variable”时
// 再从导入的模块中获取当前值
import * as module from './module.js';
module.fun();
// 导入包含所有导出内容的“命名空间对象”
import theDefaultValue from './module.js';
// 导入 `default` 导出的快捷方式
默认情况下,webpack 将自动检测文件是 ESM 还是其他模块系统。
Node.js 通过设置 package.json
中的属性来显式设置文件模块类型。
在 package.json 中设置 "type": "module"
会强制 package.json 下的所有文件使用 ECMAScript 模块。
设置 "type": "commonjs"
将会强制使用 CommonJS 模块。
{
"type": "module"
}
除此之外,文件还可以通过使用 .mjs
或 .cjs
扩展名来设置模块类型。 .mjs
将它们强制置为 ESM,.cjs
将它们强制置为 CommonJs。
在使用 text/javascript
或 application/javascript
mime type 的 DataURI 中,也将使用 ESM。
除了模块格式外,将模块标记为 ESM 还会影响解析逻辑,操作逻辑和模块中的可用符号。
导入模块在 ESM 中更为严格,导入相对路径的模块必须包含文件名和文件扩展名(例如 *.js
或者 *.mjs
),除非你设置了 fullySpecified=false
。
non-ESM 仅能导入 default
导出的模块,不支持命名导出的模块。
CommonJs 语法不可用: require
, module
, exports
, __filename
, __dirname
.
webpack
compiler 能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些 third party(第三方库) 可能会引用一些全局依赖(例如 jQuery
中的 $
)。因此这些 library 也可能会创建一些需要导出的全局变量。这些 "broken modules(不符合规范的模块)" 就是 shimming(预置依赖) 发挥作用的地方。
shim 另外一个极其有用的使用场景就是:当你希望 polyfill 扩展浏览器能力,来支持到更多用户时。在这种情况下,你可能只是想要将这些 polyfills 提供给需要修补(patch)的浏览器(也就是实现按需加载)。
下面的文章将向我们展示这两种用例。
让我们开始第一个 shimming 全局变量的用例。在此之前,先看下我们的项目:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
|- /node_modules
还记得我们之前用过的 lodash
吗?出于演示目的,例如把这个应用程序中的模块依赖,改为一个全局变量依赖。要实现这些,我们需要使用 ProvidePlugin
插件。
使用 ProvidePlugin
后,能够在 webpack 编译的每个模块中,通过访问一个变量来获取一个 package。如果 webpack 看到模块中用到这个变量,它将在最终 bundle 中引入给定的 package。让我们先移除 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
package 引入进来,并将其提供给需要用到它的模块。
运行我们的构建脚本,将会看到同样的输出:
$ 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'],
}),
],
};
这样就能很好的与 tree shaking 配合,将 lodash
library 中的其余没有用到的导出去除。
一些遗留模块依赖的 this
指向的是 window
对象。在接下来的用例中,调整我们的 index.js
:
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
+ // 假设我们处于 `window` 上下文
+ this.alert('Hmmm, this probably isn\'t a great idea...')
+
return element;
}
document.body.appendChild(component());
当模块运行在 CommonJS 上下文中,这将会变成一个问题,也就是说此时的 this
指向的是 module.exports
。在这种情况下,你可以通过使用 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'],
}),
],
};
让我们假设,某个 library 创建出一个全局变量,它期望 consumer(使用者) 使用这个变量。为此,我们可以在项目配置中,添加一个小模块来演示说明:
project
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');
},
};
你可能从来没有在自己的源码中做过这些事情,但是你也许遇到过一个老旧的 library,和上面所展示的代码类似。在这种情况下,我们可以使用 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'],
}),
],
};
此时,在我们的 entry 入口文件中(即 src/index.js
),可以使用 const { file, parse } = require('./globals.js');
,可以保证一切将顺利运行。
目前为止我们所讨论的所有内容都是处理那些遗留的 package,让我们进入到第二个话题:polyfill。
有很多方法来加载 polyfill。例如,想要引入 babel-polyfill
我们只需如下操作:
npm install --save babel-polyfill
然后,使用 import
将其引入到我们的主 bundle 文件:
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());
注意,这种方式优先考虑正确性,而不考虑 bundle 体积大小。为了安全和可靠,polyfill/shim 必须运行于所有其他代码之前,而且需要同步加载,或者说,需要在所有 polyfill/shim 加载之后,再去加载所有应用程序代码。 社区中存在许多误解,即现代浏览器“不需要”polyfill,或者 polyfill/shim 仅用于添加缺失功能 - 实际上,它们通常用于修复损坏实现(repair broken implementation),即使是在最现代的浏览器中,也会出现这种情况。 因此,最佳实践仍然是,不加选择地和同步地加载所有 polyfill/shim,尽管这会导致额外的 bundle 体积成本。
如果你认为自己已经打消这些顾虑,并且希望承受损坏的风险。那么接下来的这件事情,可能是你应该要做的:
我们将会把 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());
project
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>
现在,在 entry 入口文件中,可以通过 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
package 通过 browserslist 来转译那些你浏览器中不支持的特性。这个 preset 使用 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';
See the babel-preset-env documentation for more information.
像 process
这种 Node 内置模块,能直接根据配置文件进行正确的 polyfill,而不需要任何特定的 loader 或者 plugin。查看 node 配置页面获取更多信息。
还有一些其他的工具,也能够帮助我们处理这些遗留模块。
如果这些遗留模块没有 AMD/CommonJS 版本,但你也想将他们加入 dist
文件,则可以使用 noParse
来标识出这个模块。这样就能使 webpack 将引入这些模块,但是不进行转化(parse),以及不解析(resolve) require()
和 import
语句。这种用法还会提高构建性能。
最后,一些模块支持多种 模块格式,例如一个混合有 AMD、CommonJS 和 legacy(遗留) 的模块。在大多数这样的模块中,会首先检查 define
,然后使用一些怪异代码导出一些属性。在这些情况下,可以通过 imports-loader
设置 additionalCode=var%20define%20=%20false;
来强制 CommonJS 路径。
TypeScript 是 JavaScript 的超集,为其增加了类型系统,可以编译为普通 JavaScript 代码。这篇指南里我们将会学习是如何将 webpack 和 TypeScript 进行集成。
首先,执行以下命令安装 TypeScript compiler 和 loader:
npm install --save-dev typescript ts-loader
现在,我们将修改目录结构和配置文件:
project
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
文件。
现在让我们改变 lodash
在 ./index.ts
文件中的引入,
因为在 lodash
的定义中没有默认(default)的导出。
./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());
在本指南中,我们使用 ts-loader
,因为它能够很方便地启用额外的 webpack 功能,例如将其他 web 资源导入到项目中。
Note that if you're already using babel-loader
to transpile your code, you can use @babel/preset-typescript
and let Babel handle both your JavaScript and TypeScript files instead of using an additional loader. Keep in mind that, contrary to ts-loader
, the underlying @babel/plugin-transform-typescript
plugin does not perform any type checking.
想要了解 source map 的更多信息,请查看 开发 指南。
想要启用 source map,我们必须配置 TypeScript,以将内联的 source map 输出到编译后的 JavaScript 文件中。必须在 TypeScript 配置中添加下面这行:
tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/",
+ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
}
}
现在,我们需要告诉 webpack 提取这些 source map,并内联到最终的 bundle 中。
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); // 没有上面的声明的话,TypeScript 会抛出一个错误
在从 npm 安装 third party library(第三方库) 时,一定要记得同时安装此 library 的类型声明文件(typing definition)。你可以从 TypeSearch 中找到并安装这些第三方库的类型声明文件。
举个例子,如果想安装 lodash 类型声明文件,我们可以运行下面的命令:
npm install --save-dev @types/lodash
想了解更多,可以查看 这篇文章。
想要在 TypeScript 中使用非代码资源(non-code asset),我们需要告诉 TypeScript 推断导入资源的类型。在项目里创建一个 custom.d.ts
文件,这个文件用来表示项目中 TypeScript 的自定义类型声明。我们为 .svg
文件设置一个声明:
custom.d.ts
declare module '*.svg' {
const content: any;
export default content;
}
H 这里,我们通过指定任何以 .svg
结尾的导入(import),将 SVG 声明(declare) 为一个新的模块(module),并将模块的 content
定义为 any
。我们可以通过将类型定义为字符串,来更加显式地将它声明为一个 url。同样的概念适用于其他资源,包括 CSS, SCSS, JSON 等。
关于构建工具,请查看构建性能指南。
从 webpack 5 开始,你可以使用 Web Workers 代替 worker-loader
。
new Worker(new URL('./worker.js', import.meta.url));
选择这种语法是为了实现不使用 bundler 就可以运行代码,它也可以在浏览器中的原生 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 中可用。但不可用于 ComonnJS,无论 webpack 还是 Node.js 均是如此。
渐进式网络应用程序(progressive web application - PWA),是一种可以提供类似于 native app(原生应用程序) 体验的 web app(网络应用程序)。PWA 可以用来做很多事。其中最重要的是,在**离线(offline)**时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。
本章将重点介绍,如何为我们的应用程序添加离线体验。我们将使用名为 Workbox 的 Google 项目来实现此目的,该项目提供的工具可帮助我们更简单地为 web app 提供离线支持。
到目前为止,我们一直是直接查看本地文件系统的输出结果。通常情况下,真正的用户是通过网络访问 web app;用户的浏览器会与一个提供所需资源(例如,.html
, .js
和 .css
文件)的 server 通讯。
我们通过搭建一个拥有更多基础特性的 server 来测试下这种离线体验。这里使用 http-server package:npm install http-server --save-dev
。还要修改 package.json
的 scripts
部分,来添加一个 start
script:
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
如果你打开浏览器访问 http://localhost:8080
(即 http://127.0.0.1
),你应该会看到 webpack 应用程序被 serve 到 dist
目录。如果停止 server 然后刷新,则 webpack 应用程序不再可访问。
这就是我们为实现离线体验所需要的改变。在本章结束时,我们应该要实现的是,停止 server 然后刷新,仍然可以看到应用程序正常运行。
添加 workbox-webpack-plugin 插件,然后调整 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({
+ // 这些选项帮助快速启用 ServiceWorkers
+ // 不允许遗留任何“旧的” ServiceWorkers
+ 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
启动服务。访问 http://localhost:8080
并查看 console 控制台。在那里你应该看到:
SW registered
现在来进行测试。停止 server 并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。然而,server 已经停止 serve 整个 dist 文件夹,此刻是 Service Worker 在进行 serve。
你已经使用 Workbox 项目构建了一个离线应用程序。开始进入将 web app 改造为 PWA 的旅程。你现在可能想要考虑下一步做什么。这里是可以帮助到你解决下一步问题的比较不错的资源。
publicPath
配置选项在各种场景中都非常有用。你可以通过它来指定应用程序中所有资源的基础路径。
下面提供一些用于实际应用程序的示例,通过这些示例,此功能显得极其简单。实质上,发送到 output.path
目录的每个文件,都将从 output.publicPath
位置引用。这也包括(通过 代码分离 创建的)子 chunk 和作为依赖图一部分的所有其他资源(例如 image, font 等)。
在开发环境中,我们通常有一个 assets/
文件夹,它与索引页面位于同一级别。这没太大问题,但是,如果我们将所有静态资源托管至 CDN,然后想在生产环境中使用呢?
想要解决这个问题,可以直接使用一个有着悠久历史的 environment variable(环境变量)。假设我们有一个变量 ASSET_PATH
:
import webpack from 'webpack';
// 尝试使用环境变量,否则使用根路径
const ASSET_PATH = process.env.ASSET_PATH || '/';
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// 这可以帮助我们在代码中安全地使用环境变量
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
}),
],
};
另一个可能出现的情况是,需要在运行时设置 publicPath
。webpack 暴露了一个名为 __webpack_public_path__
的全局变量。所以在应用程序的 entry point 中,可以直接如下设置:
__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
变量设置 publicPath。你需要做的是将 output.publicPath
设为 'auto'
:
webpack.config.js
module.exports = {
output: {
publicPath: 'auto',
},
};
请注意在某些情况下不支持 document.currentScript
,例如:IE 浏览器,你不得不引入一个 polyfill,例如 currentScript Polyfill
。
首先,我们要消除一个常见的误解。webpack 是一个模块打包工具(module bundler)(例如,Browserify 或 Brunch)。而 不是一个任务执行工具(task runner)(例如,Make, Grunt 或者 Gulp )。任务执行工具用来自动化处理常见的开发任务,例如,lint(代码检测)、build(构建)、test(测试)。相比模块打包工具,任务执行工具则聚焦在偏重上层的问题上面。你仍然可以得益于这种用法:使用上层的工具,而将打包部分的问题留给 webpack。
打包工具帮助你取得准备用于部署的 JavaScript 和 stylesheet,将它们转换为适合浏览器的可用格式。例如,可以通过 压缩、分离 chunk 和 惰性加载 我们的 JavaScript 来提高性能。打包是 web 开发中最重要的挑战之一,解决此问题可以消除开发过程中的大部分痛点。
好的消息是,虽然有一些功能重叠,但是如果使用方式正确,任务运行工具和模块打包工具还是能够一起协同工作。本指南提供了关于如何将 webpack 与一些流行的任务运行工具集成在一起的高度概述。
通常 webpack 用户使用 npm scripts
来作为任务执行工具。这是比较好的开始。然而跨平台支持可能是个问题,但是有几种解决方案。许多用户(但不是大多数用户)直接使用 npm scripts
和各种级别的 webpack 配置和工具。
因此,虽然 webpack 核心重点是打包,但是可以通过各种扩展,将它用于任务运行工具的常见工作。集成一个单独的工具会增加复杂度,因此在开始前一定要权衡利弊。
对于那些使用 Grunt 的人,我们推荐使用 grunt-webpack
package。使用 grunt-webpack
你可以将 webpack 或 webpack-dev-server 作为一项任务(task)执行,访问 grunt template tags 中的统计信息,拆分开发和生产配置等等。如果还没有安装 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
package(也称作 gulp-webpack
) 的帮助下,可以相当直接地将 Gulp 与 webpack 集成。在这种情况下,不需要单独安装 webpack
,因为它是 webpack-stream
直接依赖:
npm install --save-dev webpack-stream
只要将 webpack
替换为 require('webpack-stream')
,并传递一个配置:
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 与 webpack 完全集成。这个仓库提供了很多关于其优势和劣势的细节,基本上 mocha-webpack
只是一个简单封装,提供与 Mocha 几乎相同的 CLI,并提供各种 webpack 功能,例如改进了 watch mode 和改进了路径分析。这里是一个如何安装并使用它来运行测试套件的示例(在 ./test
中找到):
npm install --save-dev webpack mocha mocha-webpack
mocha-webpack 'test/**/*.js'
获取更多信息,请查看 仓库。
karma-webpack
package 允许你使用 webpack 预处理 Karma 中的文件。
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'],
});
};
获取更多信息,请查看 仓库。
The exports
field in the package.json
of a package allows to declare
which module should be used when using module requests like import "package"
or import "package/sub/path"
.
It replaces the default implementation that returns main
field resp. index.js
files for "package"
and
the file system lookup for "package/sub/path"
.
When the exports
field is specified, only these module requests are available.
Any other requests will lead to a ModuleNotFound Error.
In general the exports
field should contain an object
where each properties specifies a sub path of the module request.
For the examples above the following properties could be used:
"."
for import "package"
and "./sub/path"
for import "package/sub/path"
.
Properties ending with a /
will forward a request with this prefix to the old file system lookup algorithm.
For properties ending with *
, *
may take any value and any *
in the property value is replaced with the taken value.
An example:
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
"./other-prefix/*": "./yet-another/*/*.js"
}
}
Module request | Result |
---|---|
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 | Error |
Instead of providing a single result, the package author may provide a list of results. In such a scenario this list is tried in order and the first valid result will be used.
Note: Only the first valid result will be used, not all valid results.
Example:
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
Here package/things/apple
might be found in .../package/good-things/apple
or in .../package/bad-things/apple
.
Instead of providing results directly in the exports
field,
the package author may let the module system choose one based on conditions about the environment.
In this case an object mapping conditions to results should be used.
Conditions are tried in object order.
Conditions that contain invalid results are skipped.
Conditions might be nested to create a logical AND.
The last condition in the object might be the special "default"
condition,
which is always matched.
Example:
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
This translates to something like:
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();
The available conditions vary depending on the module system and tool used.
When only a single entry ("."
) into the package should be supported the { ".": ... }
object nesting can be omitted:
{
"exports": "./index.mjs"
}
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
In an object where each key is a condition, order of properties is significant. Conditions are handled in the order they are specified.
Example: { "red": "./stop.js", "green": "./drive.js" }
!= { "green": "./drive.js", "red": "./stop.js" }
(when both red
and green
conditions are set, first property will be used)
In an object where each key is a subpath, order of properties (subpaths) is not significant. More specific paths are preferred over less specific ones.
Example: { "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" }
== { "./a/b/c": "./z", "./a/b/": "./y/", "./a/": "./x/" }
(order will always be: ./a/b/c
> ./a/b/
> ./a/
)
exports
field is preferred over other package entry fields like main
, module
, browser
or custom ones.
Feature | Supported by |
---|---|
"." property | Node.js, webpack, rollup, esinstall, wmr |
normal property | Node.js, webpack, rollup, esinstall, wmr |
property ending with / | Node.js(1), webpack, rollup, esinstall(2), wmr(3) |
property ending with * | Node.js, webpack, rollup, esinstall |
Alternatives | Node.js, webpack, rollup, |
Abbreviation only path | Node.js, webpack, rollup, esinstall, wmr |
Abbreviation only conditions | Node.js, webpack, rollup, esinstall, wmr |
Conditional syntax | Node.js, webpack, rollup, esinstall, wmr |
Nested conditional syntax | Node.js, webpack, rollup, wmr(5) |
Conditions Order | Node.js, webpack, rollup, wmr(6) |
"default" condition | Node.js, webpack, rollup, esinstall, wmr |
Path Order | Node.js, webpack, rollup |
Error when not mapped | Node.js, webpack, rollup, esinstall, wmr(7) |
Error when mixing conditions and paths | Node.js, webpack, rollup |
(1) deprecated in Node.js, *
should be preferred.
(2) "./"
is intentionally ignored as key.
(3) The property value is ignored and property key is used as target. Effectively only allowing mappings with key and value are identical.
(4) The syntax is supported, but always the first entry is used, which makes it unusable for any practical use case.
(5) Fallback to alternative sibling parent conditions is handling incorrectly.
(6) For the require
condition object order is handled incorrectly. This is intentionally as wmr doesn't differ between referencing syntax.
(7) When using "exports": "./file.js"
abbreviation, any request e. g. package/not-existing
will resolve to that. When not using the abbreviation, direct file access e. g. package/file.js
will not lead to an error.
One of these conditions is set depending on the syntax used to reference the module:
Condition | Description | Supported by |
---|---|---|
import | Request is issued from ESM syntax or similar. | Node.js, webpack, rollup, esinstall(1), wmr(1) |
require | Request is issued from CommonJs/AMD syntax or similar. | Node.js, webpack, rollup, esinstall(1), wmr(1) |
style | Request is issued from a stylesheet reference. | |
sass | Request is issued from a sass stylesheet reference. | |
asset | Request is issued from a asset reference. | |
script | Request is issued from a normal script tag without module system. |
These conditions might also be set additionally:
Condition | Description | Supported by |
---|---|---|
module | All module syntax that allows to reference javascript supports ESM. (only combined with import or require ) | webpack, rollup, wmr |
esmodules | Always set by supported tools. | wmr |
types | Request is issued from typescript that is interested in type declarations. |
(1) import
and require
are both set independent of referencing syntax. require
has always lower priority.
The following syntax will set the import
condition:
import
declarations in ESMimport()
expression<script type="module">
in HTML<link rel="preload/prefetch">
in HTMLnew Worker(..., { type: "module" })
import
sectionimport.hot.accept/decline([...])
Worklet.addModule
The following syntax will set the require
condition:
require(...)
define()
require([...])
require.resolve()
require.ensure([...])
require.context
module.hot.accept/decline([...])
<script src="...">
The following syntax will set the style
condition:
@import
<link rel="stylesheet">
The following syntax will set the asset
condition:
url()
new URL(..., import.meta.url)
<img src="...">
The following syntax will set the script
condition:
<script src="...">
script
should only be set when no module system is supported.
When the script is preprocessed by a system supporting CommonJs
it should set require
instead.
This condition should be used when looking for a javascript file that can be injected as script tag in a HTML page without additional preprocessing.
The following conditions are set for various optimizations:
Condition | Description | Supported by |
---|---|---|
production | In a production environment. No devtooling should be included. | webpack |
development | In a development environment. Devtooling should be included. | webpack |
Note: Since production
and development
is not supported by everyone, no assumption should be made when none of these is set.
The following conditions are set depending on the target environment:
Condition | Description | Supported by |
---|---|---|
browser | Code will run in a browser. | webpack, esinstall, wmr |
electron | Code will run in electron.(1) | webpack |
worker | Code will run in a (Web)Worker.(1) | webpack |
worklet | Code will run in a Worklet.(1) | - |
node | Code will run in Node.js. | Node.js, webpack, wmr(2) |
deno | Code will run in Deno. | - |
react-native | Code will run in react-native. | - |
(1) electron
, worker
and worklet
comes combined with either node
or browser
, depending on the context.
(2) This is set for browser target environment.
Since there are multiple versions of each environment the following guidelines apply:
node
: See engines
field for compatibility.browser
: Compatible with current Spec and stage 4 proposals at time of publishing the package. Polyfilling resp. transpiling must be handled on consumer side.deno
: TBDreact-native
: TBDThe following conditions are set depending on which tool preprocesses the source code.
Condition | Description | Supported by |
---|---|---|
webpack | Processed by webpack. | webpack |
Sadly there is no node-js
condition for Node.js as runtime.
This would simplify creating exceptions for Node.js.
The following tools support custom conditions:
Tool | Supported | Notes |
---|---|---|
Node.js | yes | Use --conditions CLI argument. |
webpack | yes | Use resolve.conditionNames configuration option. |
rollup | yes | Use exportConditions option for @rollup/plugin-node-resolve |
esinstall | no | |
wmr | no |
For custom conditions the following naming schema is recommended:
<company-name>:<condition-name>
Examples: example-corp:beta
, google:internal
.
All patterns are explained with a single "."
entry into the package, but they can be extended from multiple entries too, by repeating the pattern for each entry.
These pattern should be used as guide not as strict ruleset. They can be adapted to the individual packages.
These pattern are based on the following list of goals/assumptions:
exports
should be written to use fallbacks for unknown future cases. default
condition can be used for that.Depending on the package intention maybe something else makes sense and in this case the patterns should be adopted to that. Example: For a command line tool a browser-like future and fallback doesn't make a lot of sense, and in this case node.js-like environments and fallbacks should be used instead.
For complex use cases multiple patterns need to be combined by nesting these conditions.
These patterns make sense for packages that do not use environment specific APIs.
{
"type": "module",
"exports": "./index.js"
}
Note: Providing only a ESM comes with restrictions for node.js.
Such a package would only work in Node.js >= 14 and only when using import
.
It won't work with require()
.
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
Most tools get the ESM version.
Node.js is an exception here.
It gets a CommonJs version when using require()
.
This will lead to two instances of these package when referencing it with require()
and import
, but that doesn't hurt as the package doesn't have state.
The module
condition is used as optimization when preprocessing node-targeted code with a tool that supports ESM for require()
(like a bundler, when bundling for Node.js).
For such a tool the exception is skipped.
This is technically optional, but bundlers would include the package source code twice otherwise.
You can also use the stateless pattern if you are able to isolate your package state in JSON files. JSON is consumable from CommonJs and ESM without polluting the graph with the other module system.
Note that here stateless also means class instances are not tested with instanceof
as there can be two different classes because of the double module instantiation.
{
"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;
In a stateful package we must ensure that the package is never instantiated twice.
This isn't a problem for most tools, but Node.js is again an exception here. For Node.js we always use the CommonJs version and expose named exports in the ESM with a ESM wrapper.
We use the module
condition as optimization again.
{
"type": "commonjs",
"exports": "./index.js"
}
Providing "type": "commonjs"
helps to statically detect CommonJs files.
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
Note that despite using "type": "module"
and .js
for dist-bundle.js
this file is not in ESM format.
It should use globals to allow direct consumption as script tag.
These patterns make sense when a package contains two versions, one for development and one for production. E. g. the development version could include additional code for better error message or additional warnings.
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
When the development
condition is supported we use the version enhanced for development.
Otherwise, in production or when mode is unknown, we use the optimized version.
{
"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');
}
We prefer static detection of production/development mode via the production
or development
condition.
Node.js allows to detection production/development mode at runtime via process.env.NODE_ENV
, so we use that as fallback in Node.js. Sync conditional importing ESM is not possible and we don't want to load the package twice, so we have to use CommonJs for the runtime detection.
When it's not possible to detect mode we fallback to the production version.
A fallback environment should be chosen that makes sense for the package to support future environments. In general a browser-like environment should be assumed.
{
"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"
}
}
This is an example for a package that has optimizations for production and development usage with runtime detection for process.env
and also ships a CommonJs and ESM version
{
"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"
}
}
This is an example for a package that supports Node.js, browser and electron, has optimizations for production and development usage with runtime detection for process.env
and also ships a CommonJs and ESM version.
{
"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"
}
}
Looks complex, yes. We were already able to reduce some complexity due to a assumption we can make: Only node
need a CommonJs version and can detect production/development with process.env
.
default
export. It's handled differently between tooling. Only use named exports..cjs
or type: "commonjs"
in package.json to clearly mark source code as CommonJs. This makes it statically detectable for tools if CommonJs or ESM is used. This is important for tools that only support ESM and no CommonJs.data:
url requests are supported.在不使用 import
样式文件的应用程序中(预单页应用程序或其他原因),使用一个值数组结构的 entry,并且在其中传入不同类型的文件,可以实现将 CSS 和 JavaScript(和其他)文件分离在不同的 bundle。
举个例子。我们有一个具有两种页面类型的 PHP 应用程序:home(首页) 和 account(帐户)。home 与应用程序其余部分(account 页面)具有不同的布局和不可共享的 JavaScript。我们想要从应用程序文件中输出 home 页面的 home.js
和 home.css
,为 account 页面输出 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
我们将在 production(生产)
模式中使用 MiniCssExtractPlugin
作为 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
目录下现在包含四个文件:
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack 5 之前,通常使用:
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用 file-loader
实现。asset/inline
导出一个资源的 data URI。之前通过使用 url-loader
实现。asset/source
导出资源的源代码。之前通过使用 raw-loader
实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader
,并且配置资源体积限制实现。当在 webpack 5 中使用旧的 assets loader(如 file-loader
/url-loader
/raw-loader
等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto'
来解决。
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
},
],
+ type: 'javascript/auto'
},
]
},
}
如需从 asset loader 中排除来自新 URL 处理的 asset,请添加 dependency: { not: ['url'] }
到 loader 配置中。
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
+ dependency: { not: ['url'] },
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
],
}
}
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
文件都将被发送到输出目录,并且其路径将被注入到 bundle 中,除此之外,你可以为它们自定义 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
文件都将作为 data URI 注入到 bundle 中。
webpack 输出的 data 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
文件将原样注入到 bundle 中。
当使用 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 起,Data URLs 也支持在 new 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 配置的 module rule 层级中,设置 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
+ }
+ }
}
]
},
};
还可以 指定一个函数 来决定是否 inline 模块。
在 asset 模块和 webpack 5 之前,可以使用内联语法与上述传统的 loader 结合使用。
现在建议去掉所有的内联 loader 的语法,使用资源查询条件来模仿内联语法的功能。
示例,将 raw-loader
替换为 asset/source
类型:
- import myModule from 'raw-loader!my-module';
+ import myModule from 'my-module?raw';
webpack 相关配置:
module: {
rules: [
// ...
+ {
+ resourceQuery: /raw/,
+ type: 'asset/source',
+ }
]
},
如果你想把原始资源排除在其他 loader 的处理范围以外,请使用使用取反的正则:
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: [ ... ]
+ },
+ ] }
]
},
For use cases like Server side rendering, you might want to disable emitting assets, which is feasible with emit
option under Rule.generator
:
module.exports = {
// …
module: {
rules: [
{
test: /\.png$/i,
type: 'asset/resource',
generator: {
emit: false,
},
},
],
},
};