首先我们创建一个目录,初始化 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
起步
我们还需要调整 package.json
文件,以便确保我们安装包是 private
(私有的),并移除 main
入口。这可以防止意外发布你的代码。
如果你想要了解 package.json
内在机制的更多信息,我们推荐阅读 npm 文档。
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"
}
}
在此示例中, 标签之间存在隐式依赖关系。在
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.html
目前放在 dist
目录下,但它是手动创建的。在指南的 后续章节 中,我们会教你如何生成 index.html
而非手动编辑它。如此做,便可安全地清空 dist
目录并重新生成目录中的所有文件。
我们需要在本地安装 lodash
依赖,以在 index.js
中打包它:
npm install --save lodash
安装一个将被打包到生产环境 bundle 的 package 时,应该使用 npm install --save
;而在安装一个用于开发环境的 package 时(例如,linter、测试库等),应该使用 npm install --save-dev
。更多信息请查看 npm 文档。
现在,在我们的脚本中导入 lodash
:
src/index.js
+import _ from 'lodash';
+
function component() {
const element = document.createElement('div');
- // lodash(目前通过一个 script 脚本引入)对于执行这一行是必需的
+ // lodash 现在使用 import 引入
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
现在,我们将会打包所有脚本,因此必须更新 index.html
文件。由于现在是通过 import
引入 lodash,所以应将 lodash 删除;然后修改另一个
标签来加载 bundle,而不是原始的
./src
文件:
dist/index.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
如果 webpack.config.js
存在,则 webpack
命令将默认选择使用它。我们在这里使用 --config
选项只是向你表明可以传递任何名称的配置文件。这对于需要拆分成多个文件的复杂配置是非常有用的。
比起 CLI 这种简单直接的使用方式,配置文件更加灵活。我们可以在配置文件中指定 loader 规则、插件、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
可以通过在 npm run build
命令与参数之间添加两个连接符的方式向 webpack 传递自定义参数,例如:npm run build -- --color
。
现在,你已经有了一个基础构建配置,你可以移至下一指南——资源管理,以了解如何通过 webpack 管理诸如图像、图标等资源。此刻你的项目目录看起来应该如下:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- main.js
|- index.html
|- /src
|- index.js
|- /node_modules
不要使用 webpack 编译不可信的代码。它可能会在你的计算机,远程服务器或者在你 web 应用程序使用者的浏览器中执行恶意代码。
如果想要了解 webpack 设计思想,可以查看 基本概念 和 配置 页面。此外,你能够在 API 章节深入了解 webpack 提供的各种接口。
如果你是从一开始就沿用指南的示例,现在应该有一个显示 “Hello webpack” 的小项目。接下来我们尝试混合一些图像之类的其他资源,看看 webpack 如何处理。
在 webpack 出现之前,前端开发人员会使用 grunt 和 gulp 等工具来处理资源,并将它们从 /src
文件夹移动到 /dist
或 /build
目录中。JavaScript 模块也遵循同样的方式,但是,像 webpack 这样的工具将 动态打包 所有依赖(创建所谓的 依赖图)。这是极好的创举,因为现在每个模块都可以 明确表述它自身的依赖,以避免打包未使用的模块。
webpack 最出色的功能之一就是除了引入 JavaScript,还可以通过 loader 或内置的 Asset Modules 引入任何其他类型的文件。换言之,以上列出的那些 JavaScript 的优点(例如显式依赖),同样可以用来构建 web 站点或 web 应用程序中的所有非 JavaScript 内容。让我们从 CSS 开始起步,或许你可能已经熟悉了下面这些设置。
在开始之前,让我们对项目做一个小的修改:
dist/index.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'],
+ },
+ ],
+ },
};
module loader 可以链式调用。链中的每个 loader 都将对资源进行转换,不过链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。
请确保 loader 的先后顺序:'style-loader' 在前,而 'css-loader' 在后。如果不遵守此约定,webpack 可能会抛出错误。
webpack 根据正则表达式确定应该查找哪些文件,并将其提供给指定的 loader。在此示例中,所有以 .css
结尾的文件,都将被提供给 style-loader
和 css-loader
。
这使你可以在依赖于此样式的 JavaScript 文件中 import './style.css'
。现在,在此模块执行过程中,含有 CSS 字符串的 标签,将被插入到 html 文件的
中。
让我们来试试!现在在项目中添加一个新的 style.css
文件,并将其导入到 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 现在使用 import 引入。
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.classList.add('hello');
return element;
}
document.body.appendChild(component());
然后运行构建命令:
$ npm run build
...
[webpack-cli] Compilation finished
asset bundle.js 72.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 539 KiB
modules by path ./node_modules/ 538 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
modules by path ./src/ 965 bytes
./src/index.js + 1 modules 639 bytes [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 326 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 2231 ms
再次在浏览器中打开 dist/index.html
,应该看到 Hello webpack
现在的样式是红色。要查看 webpack 做了什么,请检查页面(不要查看页面源代码,它不会显示结果,因为 标签是由 JavaScript 动态创建的),并查看页面的 head 标签。它应该包含 style 块元素,也就是在
index.js
中导入的 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 以相同方式处理 。
试试向项目中添加一个图像,并观察它是如何工作的,你可以使用任何你喜欢的图像:
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 现在使用 import 引入。
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
如果一切顺利,现在应该看到图标成为了重复的背景图,并且 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
Mary
John
Reminder
Call Cindy on Tuesday
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 现在使用 import 引入
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
// 将图像添加到已经存在的 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
。查看开发者工具中的控制台,应该能够看到导入的数据会被打印出来!
在使用 d3 等工具实现某些数据可视化时,这个功能极其有用。这将帮助你不用在运行时发送请求获取和解析数据,而是在构建过程中将其提前加载到模块中,以便浏览器加载模块后,可以直接访问解析过的数据。
只有在使用 JSON 模块默认导出时会没有警告。
// 没有警告
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
对应的 package:
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 现在使用 import 引入。
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
// 将图像添加到已经存在的 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 ,那么项目应该能够良好运行。
但是,假如你只能被局限在旧有开发方式,或者你有一些在多个组件(视图、模板、模块等)之间共享的资源。你仍然可以将这些资源存储在一个基本目录中,甚至配合 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 现在使用 import 引入。
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.classList.add('hello');
-
- // 将图像添加到已经存在的 div 中。
- const myIcon = new Image();
- myIcon.src = Icon;
-
- element.appendChild(myIcon);
-
- console.log(Data);
- console.log(Notes);
return element;
}
document.body.appendChild(component());
并移除之前添加的依赖:
npm uninstall css-loader csv-loader json5 style-loader toml xml-loader yamljs
开发环境
在开始前,我们先将 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 map 功能,可以将编译后的代码映射回原始源代码。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)的引用。这将帮助我们确切知道所要解决问题的位置。
某些文本编辑器具有“safe write(安全写入)”功能,可能会干扰下面一些工具。阅读 调整文本编辑器 以解决这些问题。
在每次编译代码时,手动运行 npm run build
会显得很麻烦。
webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:
在多数场景中可能会使用 webpack-dev-server
,但是不妨探讨一下以上的所有选项。
你可以指示 webpack “观察”依赖图中所有文件的更改。如果其中一个文件被更新,代码将被重新编译,所以不必再去手动运行整个构建。
像下面这样添加一个用于启动 webpack 观察模式的 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 是如何编译代码的。 然而,你会发现它并没有退出命令行,这是因为该脚本当前还在观察你的文件。
现在,在 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,并具有实时重新加载的功能。设置如下:
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 的可访问文件)。
由于在这个示例中单个 HTML 页面有多个入口,所以添加了 optimization.runtimeChunk: 'single'
配置,否则可能会遇到 这个问题。查看 代码分割 章节了解更多细节。
webpack-dev-server
会从 output.path 中定义的目录中的 bundle 文件提供服务,即文件可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename]
进行访问。
webpack-dev-server
在编译之后不会写入到任何输出文件,而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。如果你的页面希望在其他不同路径中找到 bundle 文件,可以通过 dev server 配置中的 devMiddleware.publicPath 选项进行修改。
添加一个可以直接运行 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
具有许多可配置的选项。关于其他更多配置,请查看 配置文档。
现在,server 正在运行,你可能需要尝试 模块热替换(hot module replacement)!
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
下,稍后我们会指定端口号。接下来是设置自定义 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"
}
}
现在,在终端执行 npm run server
,将会有类似如下信息输出:
Example app listening on port 3000!
...
[webpack-dev-middleware] asset index.bundle.js 1.38 MiB [emitted] (name: index)
asset print.bundle.js 6.25 KiB [emitted] (name: print)
asset index.html 274 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 709 ms
[webpack-dev-middleware] Compiled successfully.
[webpack-dev-middleware] Compiling...
[webpack-dev-middleware] assets by status 1.38 MiB [cached] 2 assets
cached modules 530 KiB (javascript) 1.9 KiB (runtime) [cached] 12 modules
webpack 5.4.0 compiled successfully in 19 ms
[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
。现在,你已经学会了如何自动编译代码,并运行一个简单的开发环境 server。查看下一个指南,学习 代码分割(Code Splitting)。
本指南继续沿用 起步 中的示例代码。请确保你已熟悉这些指南中提供的示例以及 管理输出 章节。
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间。
常用的代码分离方法有三种:
这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患。不过,我们将会介绍如何解决这些隐患。先来看看如何从 main bundle 中分离另一个模块:
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/another-module.js
,我们也曾在 ./src/index.js
中引入过 lodash
,这就导致了重复引用。下一章节会介绍如何移除重复的模块。
在配置文件中配置 dependOn 选项,以在多个 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'] }
。这样可以获得更好的优化效果,并在使用异步脚本标签时保证执行顺序一致。
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 中移除,减轻了 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:
webpack 提供了两个类似的技术实现动态拆分代码。第一种,也是推荐选择的方式,是使用符合 ECMAScript 提案 的 import() 语法 实现动态导入。第二种则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。让我们先尝试使用第一种。
调用 import()
会在内部使用 promise。因此如果在旧版本浏览器中(例如,IE 11)使用 import()
,需要使用一个 polyfill 库(例如 es6-promise 或 promise-polyfill)来 shim Promise
。
在我们开始之前,先从上述示例的配置中移除多余的 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
现在,我们不再静态导入 lodash
,而是通过动态导入来分离出一个 chunk:
src/index.js
-import _ from 'lodash';
-
-function component() {
+function getComponent() {
- const element = document.createElement('div');
- // lodash 现在使用 import 引入
- 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 模块。更多有关背后原因的信息,请阅读 webpack 4: import() and CommonJs。
试试构建最新的代码,看看 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);
});
在稍后示例中,当需要根据计算后的变量导入特定模块时,可以通过向 import()
传入一个 动态表达式 实现。
Webpack v4.6.0+ 增加了对预获取(prefetch)和预加载(preload)的支持。
在声明 import
时,使用下面这些内置指令,可以让 webpack 输出“resource hint”,来告知浏览器:
下面这个预获取的简单示例中,有一个 HomePage
组件,其内部渲染一个 LoginButton
组件,然后在点击后按需加载 LoginModal
组件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
这会生成 并追加到页面头部,指示浏览器在闲置时间预取
login-modal-chunk.js
文件。
只要父 chunk 完成加载,webpack 就会添加预获取提示。
与预获取指令相比,预加载指令有许多不同之处:
下面这个简单的预加载示例中,有一个 Component
,依赖于一个较大的库,所以应该将其分离到一个独立的 chunk 中。
假想这里的图表组件 ChartComponent
组件需要依赖一个体积巨大的 ChartingLibrary
库。它会在渲染时显示一个 LoadingIndicator
组件,然后立即按需导入 ChartingLibrary
:
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
在页面中使用 ChartComponent
时,在请求 ChartComponent.js
的同时,还会通过 请求
charting-library-chunk
。假定 page-chunk 体积比 charting-library-chunk
更小,也更快地被加载完成,页面此时就会显示 LoadingIndicator
,等到 charting-library-chunk
请求完成,LoadingIndicator
组件才消失。这将会使得加载时间能够更短一点,因为只进行单次往返,而不是两次往返,尤其是在高延迟环境下。
不正确地使用 webpackPreload
会有损性能,请谨慎使用。
有时你需要自己控制预加载。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下很有用。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// 在发生错误时做一些处理
// 例如,我们可以在网络错误的情况下重试请求
});
如果在 webpack 开始加载该脚本之前脚本加载失败(如果该脚本不在页面上,webpack 只是创建一个 script 标签来加载其代码),则该 catch 处理程序将不会启动,直到 chunkLoadTimeout 未通过。此行为可能是意料之外的。但这是可以解释的 —— webpack 不能抛出任何错误,因为 webpack 不知道那个脚本失败了。webpack 将在错误发生后立即将 onerror 处理脚本添加到 script 中。
为了避免上述问题,你可以添加自己的 onerror 处理脚本,将会在错误发生时移除该 script。
在这种情况下,错误的 script 将被删除。webpack 将创建自己的 script,并且任何错误都将被处理而没有任何超时。
一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。官方分析工具 是一个不错的开始。还有一些其他社区支持的可选项:
接下来,查看 懒加载 来学习如何在真实的应用程序中使用 import()
的具体示例,以及查看 缓存 来学习如何有效地分离代码。