Webpack使用总结
2018-05-04
Webpack简介
- 当我们使用Webpack来打包一个项目的时候,Webpack首先从配置中的
entries
属性定义的入口开始,历遍整个项目,根据import
的递归地历遍整个项目的文件,构建模块之间的依赖图(dependency graph)。然后再根据webpack的配置信息对项目进行打包。
Webpack执行过程
- 解析过程:
- Webpack自
entry
起递归地历遍整个项目。可以设置entry
的resolve
属性来设置查找路径 - 每次Webpack成功地解析一个模块, 都会通过对应的Loader对模块进行处理。
- 对于每个Loader也会发生类似策解析过程。
- Webpack自
- 执行过程:
- Webpack按照从右往左从上到下的顺序执行模块匹配的Loader。每个Loader依次处理该模块。
- 执行的结果最终会被注入到打包后的文件中。
- 完成:
- 在每个模块都被处理之后,Webpack根据
output
的设定生成最终打包文件。
- 在每个模块都被处理之后,Webpack根据
Developing
简单的 Webpack 配置
创建项目并安装Webpack
1 | mkdir webpack-car |
运行webpack
1 | $ node_modules/.bin/webpack |
- 输出提示我们未设置
mode
属性, 并且由于没有找到相关文件来解析。通过如下步骤解决- 创建
src/index.js
文件。 写入console.log('Doom!');
。 - 执行
node_modules/.bin/webpack --mode development
。 这次webpack会发现文件来解析打包。 - 刚才的命令会在项目根目录下生成
dist/main.js
文件。 其中包含webpack的启动代码,还有项目打包后的输出。
- 创建
更复杂一点
src/component.js
1
2
3
4
5
6
7export default (text = "Hello world") => {
const element = document.createElement("div");
element.innerHTML = text;
return element;
};src/index.js
1
2
3import component from "./component";
document.body.appendChild(component());
配置html-webpack-plugin
测试本项目
1 | npm install html-webpack-plugin --save-dev |
webpack.config.js
1
2
3
4
5
6
7
8
9const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "Webpack demo",
}),
],
};输入如下命令,打开浏览器到提示网址查看结果。
1
2
3node_modules/.bin/webpack --mode production
cd dist
serve
查看输出结果
1 | Hash: d172aaf21a4ccd6a3124 |
Hash: d172aaf21a4ccd6a3124
: 这次build的哈希值,可以将这个hash值添加到打包后输出的文件名中。main.js 679 bytes 0 [emitted] main
: 生成文件的名字,大小, 分块的ID,状态信息,分块的名字。Child html-webpack-plugin for "index.html":
与Plugin有关的输出。npm scripts快捷方式
package.json
1
2
3"scripts": {
"build": "webpack --mode production"
}
配置webpack-dev-server
Webpack watch
模式和 webpack-dev-server
传入
--watch
选项开启watch mode, webpack在watch模式下会检测任何文件变更,并重新编译文件。1
2
3
4
5npm run build -- --watch
```
- `webpack-dev-server`(WDS)也实现了watch模式,WDS运行在内存当中。当WDS运行时,所有对项目文件的更改生成的结果都是直接写入到打包文件中,而是存储在内存中。默认状态下,WDS会自动刷新浏览器中内容。
```bash
npm install webpack-dev-server --save-devpackage.json
1
2
3
4"srcipt": {
"start": "webpack-dev-server --mode development",
"build": "webpack --mode production"
},现在运行
npm start
后,http://localhost:8080
中会显示内容, 代码更改后会强制刷新
配置 webpack-dev-server
1 | module.exports = { |
- 可以通过命令行设置
HOST
和PORT
的值(PORT=3000 npm start
)。
轮询来替代 watch mode
- 当watch mode在某些系统上是小事。可以启动WDS的轮询选项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const path = require("path");
module.exports = {
devServer: {
watchOptions: {
aggregateTimeout: 300,
poll: 1000,
},
},
plugins: [
new webpack.WatchIgnorePlugin([
path.join(__dirname, "node_module")
]),
],
};
更改配置后自动重启WDS
1 | npm install nodemon --save-dev |
package.json
1
2
3"scripts": {
"start": "nodemon --watch webpack.config.js --exec \"webpack-dev-server --mode development\""
}webpack-dev-server
的一些其他选项devServer.contentBase
:假如我们没有自动生成index.html
,可以提供过设置contentBase
来指定内容路径。devServer.proxy
:默认禁用, 接受一个代理映射对象, 将对应的查询映射到对应的服务器地址。devServer.headers
:在请求中加入自定义头部。
配置管理
配置管理的几种常见方法
常见方法
webpack.parts.js
1
2
3
4
5
6
7
8
9exports.devServer = ({ host, port } = {}) => ({
devServer: {
stats: "errors-only",
host, // Defaults to `localhost`
port, // Defaults to 8080
open: true,
overlay: true,
},
});webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32const merge = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const parts = require("./webpack.parts");
const commonConfig = merge([
{
plugins: [
new HtmlWebpackPlugin({
title: "Webpack demo",
}),
],
},
]);
const productionConfig = merge([]);
const developmentConfig = merge([
parts.devServer({
// Customize host/port here if needed
host: process.env.HOST,
port: process.env.PORT,
}),
]);
module.exports = mode => {
if (mode === "production") {
return merge(commonConfig, productionConfig, { mode });
}
return merge(commonConfig, developmentConfig, { mode });
};package.json
1
2
3
4"scripts": {
"build": "webpack --env production",
"start": "nodemon --watch webpack.config.js --exec \" webpack-dev-server --env development\""
},
Styling
- Webpack通过loader和plugin来处理加载样式文件。
加载CSS样式
css-loader
:历遍每个匹配文件的@import
和url()
, 并将其作为ES6的import
处理来构建依赖图, 如果@import
指向外部资源的话,css-loader
会跳过该语句。style-loader
: 创建style
元素, 并将所有样式内容写入。
1 | npm install css-loader style-loader --save-dev |
webpack.parts.js
1
2
3
4
5
6
7
8
9
10
11
12exports.loadCSS = ({ include, exclude} = {}) => ({
module: {
rules: [
{
test: /\.css$/,
include,
exclude,
use: ["style-loader", "css-loader"], // evaluated from right to left
}
]
}
})webpack.config.js
1
2
3
4const commonConfig = merge([
// ...
parts.loadCSS(),
]);src/main.css
1
2
3body {
background: lime;
}src/index.js
1
2import './main.css';
//...
加载LESS样式
- 通过
less-loader
处理LESS样式1
2
3
4{
test:/\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
}
加载SASS样式
- 通过
sass-loader
处理SASS样式1
2
3
4{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
}
加载Stylus样式
- 通过
stylus-loader
处理Stylus样式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
// ...
module: {
rules: [
{
test: /\.styl$/,
use: [
"style-loader",
"css-loader",
"stylus-loader",
],
},
],
},
},
PostCSS
- PostCSS允许你通过JavaScript插件对CSS执行转化。 PostCSS之于CSS如Babel之于JavaScript。
- 通过
postcss-loader
配置PostCSS1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
plugins: () => ([
require("autoprerfixer"),
require("precss"),
]),
},
},
],
}
其他
css-loader
的文件查找,只能处理相对路径引用,不能处理绝对路径引用。 如果想使用绝对路径引入文件,需要把文件复制到项目下。- 从node_modules中引入文件:
@import "~bootstrap/less/bootstrap";
, 通过在路径前添加~
符号,告知webpack在node_modules
中查询文件。
分离CSS样式
- 尽管通过刚才的设置我们已经基本完成了对CSS样式的处理, 但还有一些问题。 首先我们的CSS样式是直接行内置在JavaScript脚本中的。这样的话浏览器端无法缓存CSS样式。并且由于浏览器会先下载并加载JavaScript,CSS样式在JavaScript执行后才会被渲染,这就会导致无风格内容闪烁(Flash of Un styled Content)。
配置MiniCssExtractPlugin
1 | npm install mini-css-extract-plugin --save-dev |
MiniCssExtractPlugin中包含loader。loader提取CSS内容, MiniCssExtractPlugin把loader提取的内容打包到一个单独的CSS文件中。
webpack.part.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const MiniCssExtractPlugin = require("mini-css-extract-plugin");
exports.extractCSS = ({ include, exclude, use = [] }) => {
// Output extracted CSS to a file
const plugin = new MiniCssExtractPlugin({
filename: "[name].css",
});
return {
module: {
rules: [
{
test: /\.css$/,
include,
exclude,
use: [
MiniCssExtractPlugin.loader,
].concat(use),
},
],
},
plugins: [plugin],
};
}[name]
占位符的值最终会被引用CSS样式的文件的名字取代。webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13const commonConfig = merge([
//parts.loadCSS(),
]);
// const productionConfig = merge([]);
const productionConfig = merge([
parts.extractCSS({
use: "css-loader",
}),
]);
const developmentConfig = merge([
// ...
parts.loadCSS(),
]);
删除无用的CSS
配置PurifyCSS
1 | npm install glob purifycss-webpack purify-css --save-dev |
webpack.parts.js
1
2
3
4
5const PurifyCSSPlugin = require("purifycss-webpack");
exports.purifyCSS = ({ paths }) => ({
plugins: [ new PurifyCSSPlugin({ paths })],
});webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// ...
const path = require("path");
const glob = require("glob");
const parts = require("./webpack.parts");
const PATHS = {
app: path.join(__dirname, "src"),
};
//...
const productionConfig = merge([
//...
parts.purifyCSS({
paths: glob.sync(`${PATHS.app}/**/*.js`, { nodir: true }),
}),
]);PurifyCSS 必须放在 MiniCssExtractPlugin后执行。
Autoprefixing
有些CSS规则在某些浏览器上表现异常,需要使用浏览器供应商特定的前缀来处理CSS规则异常。记住这些浏览器供应商前缀和异常CSS规则之间的对应关系很难。可以通过Autoprefix插件来完成
配置Autoprefixing
1
npm install postcss-loader autoprefixer --save-dev
webpack.parts.js
1
2
3
4
5
6exports.autoprefix = () => ({
loader: "postcss-loader",
options: {
plugins: () => [require("autoprefixer")()],
},
});webpack.config.js
1
2
3
4
5
6
7
8
9
10
11const productionConfig = merge([
parts.extractCSS({
use: "css-loader",
use: ["css-loader", parts.autoprefix()],
}),
// ...
]);app/main.css
1
2
3
4.pure-button {
-webkit-border-radius: 1em;
border-radius: 1em;
}创建
.browserslistrc
文件,来规定需要支持的浏览器版本1
2
3> 1% # Browser usage over 1%
Last 2 versions # Or last two versions
IE 8 # Or IE 8
加载 Assets
Loader的定义
- webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19module.exports = {
// ...
module: {
rules: [
{
// 条件判断: 通过正则表达式判断匹配文件。
test: /\.js$/
// 路径限制条件
include: path.join(__dirname, "app"),
exclude(path) {
return path.match(/node_modules/);
}
// 需要执行的loader
user: "babel-loader",
}
]
}
}
Loader的执行顺序
- 从下到上, 从右到左。例如:
use:["style-loader", "css-loader"]
可以看做style(css(input))
。 - 通过指定
enfore
的值(pre
或post
)来强制改变loader执行顺序。pre
在其他loader之前执行,post
在其他loader执行之后执行。向Loader传递参数
通过查询格式传递参数给loader:
1
2
3
4
5{
test: /\.js$/,
include: PATHS.app,
use: "babel-loader?presets[]=env",
},通过
options
属性传递参数1
2
3
4
5
6
7
8
9
10{
test: /\.js$/,
include: PATHS.app,
use: {
loader: "babel-loader",
options: {
presets: ["env"],
},
},
}向多个loader传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
test: /\.js$/,
include: PATHS.app,
use: [
{
loader: "babel-loader",
options: {
presets: ["env"],
},
},
{
loader: "react-loader",
options: {
presets: ["env"],
},
},
// Add more loaders
]
}
通过 use 分支来管理配置
1 | { |
行内置定义loader
1 | // process foo.png through url-loader |
匹配资源的其他方法
- 其他常见方法:
resource: /inline/
: 匹配资源路径中包含inline
的资源,包括查询语句。例如:/path/foo.inline.js
,/path/bar.png?inline
.issuer: /bar.js/
: 匹配被bar.js
文件引入的文件。resourcePath: /inline/
: 匹配路径中包含inline
的资源。不包含查询语句。resourceQuery: /inline/
: 匹配资源查询语句中包含inline
的资源。
通过
rsourceQuery
匹配文件1
2
3
4
5
6
7
8
9
10
11
12
13{
test: /\.png$/,
oneOf: [
{
resourceQuery: /inline/,
use: "url-loader",
},
{
resourceQuery: /external/,
use: "file-loader",
},
],
}通过
issuer
匹配文件。- 通过
issuer
属性根据匹配文件被引入位置的不同作出不同的处理。1
2
3
4
5
6
7
8
9
10
11
12{
test:/\.css$/,
rules: [
{
issuer: /\.js$/,
use: "style-loader",
},
{
use: "css-loader",
},
],
}
- 通过
加载图像
配置 url-loader
- url-loader 将图片以base64编码并以行内置的方式插入到打包的JavaScript文件中。这可以减少HTTP请求数量, 但是会增加打包后文件的大小。所有url-loader常用于development阶段。
- url-loader的
limit
属性规定了url-loader可以处理文件的大小上限,当文件大小超过limit
时,会被url-loader跳过。 - 通过url-loader和file-loader组合使用, 可以在打包文件中内置小与
limit
的文件, 而大的文件则通过file-loader单独生成文件。1
2
3
4
5
6
7
8
9{
test: /\.(jpg|png)$/,
use: {
loader: "url-loader",
options: {
limit: 25000,
},
},
}
配置 file-loader
1 | { |
[hash]
为文件内容的MD5哈希值。项目中加入图片
webpack.parts.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15exports.loadImages = ({ include, exclude, options } = {}) => ({
module: {
rules: [
{
test: /\.(png|jpg)$/,
include,
exclude,
use: {
loader: "url-loader",
options,
},
},
],
},
});webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const productionConfig = merge([
// ...
parts.loadImages({
options: {
limit: 15000,
name: "[name].[ext]",
},
}),
]);
const developmentConfig = merge([
// ...
parts.loadImages(),
]);
加载SVGs
- 通过file-loader处理SVGs
1
2
3
4{
test: /\.svg$/,
use: "file-loader",
}
图片加载优化
- 可以使用image-webpack-loader, svgo-loader或者imagemin-webpack-plugin,来压缩图片或SVG. 这些插件应该首先作用在图片数据上,所以他们作为
use
属性的数组的最后一项。 - webpack.parts.js 配置image-webpack-loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31module: {
rules: [
{
test: /\.(png|jpg)$/,
use: [
// ...
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75,
}
}
]
}
]
}
其他
- 通过
image-trace-loader
或lqip-loader
在图片加载时使用占位符。 webpack-spritesmith
创建图片精灵 sprites.
加载字体
- 加载字体的配置和image的配置差不多,也是通过url-loader和file-loader完成, 但是由于不同浏览器支持不同的字体格式,字体格式的匹配查找模式
test
变得更为复杂。 除了Opera Mini外,所有浏览器都支持.woff格式.
1
2
3
4
5
6
7
8
9{
test: /\.woff$/,
use: {
loader: "url-loader",
options: {
limit: 5000,
},
},
}所有现代浏览器都支持.woff2格式。
1
2
3
4
5
6
7
8
9
10
11{
test: /\.(woff|woff20)(\?v=\d+\.\d+)?$/,
use: {
loader: "url-loader",
options: {
limit: 50000,
mimetype: "application/font-wolf",
name: "./fonts/[name].[ext]",
},
},
}CSS: 把.woff2格式的字体放在首位,新的格式版本相比旧版有很多优势。
1
2
3
4
5
6
7
8@font-face {
font-family: "myfontfamily";
src: url("./fonts/myfontfile.woff2") format("woff2"),
url("./fonts/myfontfile.woff") format("woff"),
url("./fonts/myfontfile.eot") format("embedded-opentype"),
url("./fonts/myfontfile.ttf") format("truetype");
/* Add other formats as you see fit */
}
加载javascript
- Webpack支持ES6的模块机制,但是不会转译ES6的新语法,这需要Babel来完成。
配置babel-loader
1 | npm install babel-loader babel-core --save-dev |
webpack.parts.js
1
2
3
4
5
6
7
8
9
10
11
12exports.loadJavaScript = ({ include, exclude } = {}) => ({
module: {
rules: [
{
test: /\.js$/,
include,
exclude,
use: "babel-loader",
},
],
},
});webpack.config.js
1
2
3
4const commonConfig = merge([
// ...
parts.loadJavaScript({ include: PATHS.app }),
]);
通过.babelrc配置Babel
babel-preset-env 是Babel的预设配置,根据运行环境启用响应的插件。
1
npm install babel-preset-env --save-dev
.babelrc 配置
1
2
3
4
5
6
7
8
9
10{
"presets": [
[
"env",
{
"modules": false,
}
]
]
}Webpack支持ES6模块机制,如果再经过Babel处理,Webpack的HMR机制就会失效,所以设置
"modules": false
。
Building 构建
Source Maps
Source Map 的作用:当我们的代码被打包转译之后,代码debug成了一个问题。 当我们在浏览器中之中发现问题所在之后如何在源代码中确定出现问题的代码的位置呢? 我们可以通过Source Map来解决。
行内置的Source Map和分离的 Source Map
Inline Source Map: 适合在Development阶段使用, 拥有更好的性能, 但是打包后文件更大。
- Seperate Source Map: 适合在Production阶段使用, 分离文件不会增加打包后文件大小。
- Hidden Source Map: 只提供调用栈信息。
开启Source Map
- Webpack配置
Webpack.parts.js
1
2
3exports.generateSourceMaps = ({ type }) => ({
devtool: type,
})Webpack.config.js
1
2
3
4
5// enable source-map for production and let webpack use the default for development.
const productionConfig = merge([
parts.generateSourceMaps({ type: "source-map"}),
// ...
]);Output
1
2
3
4
5
6
7
8
9
10
11
12
13
Hash: b59445cb2b9ae4cea11b
Version: webpack 4.1.1
Time: 1347ms
Built at: 3/16/2018 4:58:14 PM
Asset Size Chunks Chunk Names
main.js 838 bytes 0 [emitted] main
main.css 3.49 KiB 0 [emitted] main
main.js.map 3.75 KiB 0 [emitted] main
main.css.map 85 bytes 0 [emitted] main
index.html 220 bytes [emitted]
Entrypoint main = main.js main.css main.js.map main.css.map
...
行内置Source Map
- devtool: “eval”
- 模块内容被包裹在
eval()
中. - Source Map的相关信息则以注释的方式表示.
- 模块内容被包裹在
- devtool: “cheap-eval-source-map”
- 模块内容被包裹在
eval()
中。 - Source Map的相关信息则以Base64编码注释的方式保存。
- 模块内容被包裹在
- devtool: “cheap-module-eval-source-map”
- 与”cheap-eval-source-map”类似, 只不过生成source map内容更详细, 性能有所下降。
- devtool: “eval-source-map”
- 生成最详细的source map, 但也是最慢的。
分离的Source Map
生成的分离Source Map文件的扩展名为
.map
。只有被浏览器要求时才会加载, 既保证了性能,又方便debug。devtool: “cheap-source-map”
- 类似于上述的cheap类型的source map。
- 没有列的映射关系。
- 从loader中生成的 source map 也不会被显示。
- devtool: “cheap-module-source-map”
- 类似于 “cheap-source-map”。
- 从loader中产生的source map将被简化显示。
- devtool: “hidden-source-map”
- 不会将 Source Map 暴露给浏览器,但是会显示调用栈的信息。
- devtool: “nosource-source-map”
- 生成的source map中不包含
sourcesContent
, 但会得到调用栈的信息。
- 生成的source map中不包含
- devtool: “source-map”
- 生成质量最好的source-map, 但性能最差。
打包分割
- 如果没有打包分割的话, 那么production模式下打包后的文件就只生成一个文件, 每次发生文件修改, 客户端都需要重新下载整个文件。 打包分割后, 客户端只需要下载发生更改后的那部分文件就可以了。
- Webpack 4提供了一些开箱即用的打包分割。
- 通过打包分割, 可以将应用的依赖单独打包到一个文件当中,如:vender.js, 这样可以利用客户端缓存。 应用的大小不会改变,但在初次使用时发送的HTTP请求数会增多。
增加需要分割的内容
1 | npm install react react-dom --save |
- src/index.js
1
2
3import 'react';
import 'react-dom';
//...
打包分割
- Webpack4之前,打包分割需要通过CommonsChunkPlugin插件来完成, 而在Webpack4之后,可以通过配置来完成。
- webpack.config.js
1
2
3
4
5
6
7
8
9
10const productionConfig = merge([
//...
{
optimization: {
splitChunks: {
chunks: "initial",
},
},
},
]);
1 | npm run build |
分割和合并代码块
- AggressiveSplitingPlugin: 可以生成更多更小的代码块。可以结合HTTP/2标志使用。
- AggressiveMergingPlugin: 将小的模块合并。
1
2
3
4
5
6
7
8
9// Aggresive Splitting
{
plugins: {
new Webpack.optimize.AggressiveSplittingPlugin({
minSize: 10000,
maxSize: 30000,
}),
},
}
1 | // Aggresive Merging |
Webpack中代码块的种类
- Entry Chunks: 包含webpack runtime 和之后需要加载的模块.
- Normal Chunks: 不包含Webpack Runtime。 这些代码动态加载到应用中。
- Initial Chunks: Normal Chunk 中的一种,影响应用的初次加载时间。
Code分割(懒加载 lazy loading)
- 当Web应用慢慢发展,具有更多特性之后,其代码文件的大小也难免越来越大,应用首次加载时需要下载的代码文件过大,应用首次加载的时间就会很长。
- 懒加载允许客户端从服务端下载目前需要的代码块。当用户进入应用的另外一个视图层时,则再下载该视图层对应的代码。
代码分割格式
- 代码分割通常使用两种方法: 动态
import
和require.ensure
。 - 代码分割的目的: 通过在代码中设置分割点,使对应的代码只在需要的时候被加载。
Dynamic import
- Dynamic
import
语法并不是正式语言标准。需要设置Babel配置来支持这一特性。 Dynamic
import
使用Promise:1
2
3
4
5
6
7
8
9import(/* webpackChunkName: "optional-name"*/ "./modules").then(
module => {
// ...
}
).catch(
error => {
// ...
}
)Optional Name 允许你将多个分割代码合并到一个打包后文件中,只要它们具有相同的optional-name。 默认的是每个分割点打包到单独的文件中。
- 也可以多个 dynamic
import
组合在一起。1
2
3
4
5
6Promise.all([
import("moduleA"),
import("moduleB")
]).then(((moduleA, moduleB]) => {
// do something
});
配置Babel
1 | npm install babel-plugin-syntax-dynamic-import --save-dev |
.babelrc
1
2
3{
"plugins": ["syntax-dynamic-import"],
}如果在使用ESlint, 需要在ESlint配置文件中设置
"parser":"babel-eslint"
和parserOptions.allowImportExportEveryWhere: true
。通过 Dynamic
import
来设置分割点src/lazy.js
1
export default 'Hello, Lazy loading`;
src/component.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17export default (text = 'Hello world') => {
const element = document.createElement('div');
element.className = "pure-button";
element.innerHTML = text;
element.onClick = () => {
import('./lazy')
.then(lazy => {
element.textContent = lazy.default;
})
.catch(err => {
console.error(err);
});
}
return element;
}
1 | npm run build |
React 中的代码分割方法
- React 中通过将分割点包裹在React中完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import React from 'react';
<AsyncComponent loader = {() => import('./SomeComponent'))}/>
class AsyncComponent extends React.Component{
constructor(props) {
super(props);
this.state = { Component: null };
}
componentDidMount(){
this.props.loader().then(Component => this.setState({ Component }));
}
render() {
const { Component } = this.state;
const { Placeholder, ...props } = this.props;
return Component ? <Component {...props} />:<Placeholder />;
}
}
AsyncComponent.propTypes = {
loader: PropTypes.func.isRequired,
Placeholder: PropTypes.node.isRequired,
}
禁用代码分割
1 | const webpack = require('webpack'); |
- 代码分割需要我们决定哪些代码需要分割, 我们可以参考router设置。
清除Build路径: 在每次build之前,清除之前build生成的文件。
设置 CleanWebpackPlugin
1 | npm install clean-webpack-plugin --save-dev |
webpack.parts.js
1
2
3
4const CleanWebpackPlugin = require('clean-webpack-plugin');
exports.clean = path => ({
plugins: [new CleanWebpackPlugin([path])],
});webpack.config.js
1
2
3
4
5
6
7
8
9const PATHS = {
app: path.join(__dirname, "src"),
build: path.join(__dirname, "dist"),
};
// ...
const productionConfig = merge([
parts.clean(PATHS.build);
// ...
]);
添加Build的相关信息到Build之后的文件
配置 BannerPlugin 和 GitRevisionPlugin
1 | npm install git-revision-webpack-plugin --save-dev |
优化
Minifying JavaScript
- Minification 将代码文件转化到更小的形式。安全的转化过程不会影响代码的正常工作。
- 在Webpack 4中, Minification 通过两个配置选项:
optimization.minimize
和optimization.minimizer
来完成。 通过 uglifyjs-webpack-plugin 插件可以修改默认设置。
设置 Minify JavaScript
1
npm install uglifyjs-webpack-plugin --save-dev
webpack.parts.js
1
2
3
4
5
6
7const UglifyWebpackPlugin = require("uglifyjs-webpack-plugin");
exports.minifyJavaScript = () => ({
optimization: {
minimizer: [ new UglifyWebpackPlugin({ sourceMap: ture })],
},
});webpack.config.js
1
2
3
4const productionConfig = merge([
parts.clean(PATHS.build),
parts.minifyJavaScript(),
]);
设置 Minify CSS
1 | npm install optimize-css-assets-webpack-plugin cssnano --save-dev |
webpack.parts.js
1
2
3
4
5
6
7
8
9
10
11
12const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const cssnano = require('cssnano');
exports.minifyCSS = ({ options }) => ({
plugins: [
new OptimizeCSSAssetsPlugin({
cssProcessor: cssnano,
cssProcessorOptions: options,
canPrint: false,
})
]
})webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12const productionConfig = merge([
// ...
parts.minifyJavaScript(),
parts.minifyCSS({
options: {
discardComments: {
removeAll: true,
},
safe: true,
},
})
]);
Tree Shaking
- Tree Shaking是ES6的新特性, 它可以静态检测代码, 分析出代码的哪一部分被使用, 哪一部分没有被使用。
src/shake.js
1
2
3
4const shake = () => console.log("shake");
const bake = () => console.log("bake");
export { shake, bake };src/index.js
1
2
3
4// ...
import { bake } from "./shake";
bake();
// ...运行
npm run build
后,会发现dist/main.js中只包含console.log("bake")
但不包括console.log("shake")
。
环境变量 Environment Variables
- 通过设置环境变量, 我们可以控制代码的执行, 例如
if (process.env.NODE_ENV === "development") { // do something }
,Webpack通过DefinePlugin会将环境变量转化为实际值,如if(false){ // do something }
,而JavaScript Minifiers 会最终移除if(false){}
这样的代码。 Webpack 4根据mode自动设置
process.env.NODE_ENV
。DefinePlugin的工作原理
1
2
3
4
5
6
7var foo
if (foo === 'bar') {
console.log('bar');
}
if ( EnvirVar === 'bar') {
console.log('bar');
}DefinePlugin 会替换环境变量的值, 如果
EnvirVar
的值为foobar
则代码如下:1
2
3
4
5
6
7var foo;
if (foo === 'bar') {
console.log('bar');
}
if ('foobar' === 'bar') {
console.log('bar');
}DefinePlugin 进一步分析发现
'foobar' === 'bar'
结果为false
。则Minifier 处理后代码如下:1
2
3
4var foo;
if (foo === 'bar') {
console.log('bar');
}
设置 process.env.NODE_ENV
webpack.parts.js
1
2
3
4
5
6
7
8
9const webpack = require('webpack');
exports.setFreeVariable = (key, value) => {
const env = {};
env[key] = JSON.stringfy(value);
return {
plugins: [new webpack.DefinePlugin(env)],
};
};webpack.config.js
1
2
3
4const commonConfig = merge([
// ...
parts.setFreeVariable("Hello", "Hello from config"),
]);
环境变量控制模块使用
1 | // src/store/index.js |
- 通过环境变量控制模块使用时,只能使用CommonJS模块机制,ES6的模块机制不支持动态加载。
在文件名中加入哈希值
- 尽管每次build之后生成的文件都被自动命名, 但这并不能有效利用客户端缓存, 因为客户端并不知道哪些文件有效,而哪些文件已经经过修改,其对应的缓存文件已经失效。可以在文件名中加入哈希值解决这个问题。
常用占位符
[id]
: 返回 chunk id。[path]
: 返回文件路径。[name]
: 返回文件名。[ext]
: 返回文件扩展名。[hash]
: 返回本次build的哈希值。[chunkhash]
: 返回特定entry的哈希值, 如果配置文件中的entry改变, 该值也改变。[contenthash]
: 返回根据内容生成的哈希值。设置哈希值
图片和字体使用
[hash]
占位符, 代码块则使用[chunkhash]
占位符。webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const productionConfig = merge([
{
output: {
chunkFilename: '[name].[chunkhash:4].js',
filename: '[name].[chunkhash:4].js',
},
}
// ...
parts.loadImages({
options: {
limit: 15000,
name: '[name].[hash:4].[ext]',
},
}),
// ...
]);如果生成的CSS也是用
[chunkhash]
的话,则会产生一些问题,由于具有相同的entry, 所以当JavaScript(或CSS)改变时,CSS(或JavaScript)也被认定为失效。由此我们使用[contentHash]
对应CSS文件。
分离Minifest文件
- Minifest文件包含在生成的vendor bundle中, Minifest描述Webpack需要加载的文件。如果生成文件的哈希值改变了, Minifest文件将会失效。 这将导致vendor bundle文件也失效了。
从Vendor bundle提取Minifest文件
- webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14const productionConfig = merge([
// ...
{
optimization: {
splitChunks: {
// ...
},
runtimeChunk: {
name: 'manifest','
},
},
},
// ...
]);
Build Analysis
配置Webpack
- package.json
1
2
3
4
5
6"scripts": {
"build:stats": "webpack --env production --json > stats.json",
// ...
},
Output
Build Targets
- 我们通过
target
的值来控制webpack的输出目标(output target)。Web Targets
- Webpack默认使用Web作为output target。
- 初始化需要加载的模块在manifest中标明。
Web Workers Targets
- 设置为Web Workers Targets可以将你的应用包装为一个Web worker。
- Web Workers可以在主线程之外执行运算。但不可以操控DOM。
Node Targets
- Webpack提供了两种Node特定的Target:
node
和async-node
。 node
模式下,使用require
语句加载模块。通常在使用服务端渲染的情况下使用。async-node
模式下, 可以通过Node的fs
和vm
加载模块。Desktop Targets
node-webkit
: 生成NW.js应用。atom
,electron
,electron-main
生成Electron主进程。electron-render
生成Electron 渲染进程。
Multiple Pages
Generating Multiple Pages through multi-complier mode.
- 为每个页面都设置配置文件,然后在 multi-complier模式下处理文件。
webpack.parts.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const HtmlWebpackPlugin = require('html-webpack-plugin');
exports.page = ({
path = '',
template = require.resolve(
'html-webpack-plugin/default_index.ejs'
),
title,
} = {}) => ({
plugins: [
new HtmlWebpackPlugin({
filename: `${path && `${path}/`}index.html`,
template,
title,
}),
],
});webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// const HtmlWebpackPlugin = require('html-webpack-plugin');
const commonConfig = merge([
// {
// plugins: [
// new HtmlWebpackPlugin({
// title: 'Webpack demo',
// }),
// ],
// }
// ...
]);
module.exports = mode => {
// if (mode === 'production') {
// return merge(commonConfig, productionConfig, { mode });
// }
// return merge(commonConfig, developmentConfig, { mode });
const pages = [
parts.page({ title: 'Webpack demo'}),
parts.page({ title: 'Another demo', path: 'another'}),
];
const config = mode === 'production' ? productionConfig : developmentConfig;
return pages.map(page => merge(commonConfig, config, page, { mode }));
}现在构建后的页面加载相同的代码, 为了使每个页面能单独加载代码。 我们需要在每个页面的配置文件中单独的设置
entry
的值。src/another.js
1
2
3
4
5
6import './main.css';
import component from './component';
const demoComponent = component('Another');
document.body.appendChild(demoComponent);webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29const commonConfig = merge([
{
output: {
publicPath: '/',
},
},
// ...
]);
module.exports = mode => {
const pages = [
parts.page({
title: 'Webpack demo',
entry: {
app: PATHS.app,
},
}),
parts.page({
title: 'Another demo',
path: 'another',
entry: {
another: path.join(PATHS.app, 'another.js'),
},
}),
];
const config = mode === 'production' ? productionConfig : developmentConfig;
return pages.map(page => merge(commonConfig, config, page, { mode }));
}webpack.parts.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22exports.page = (
{
path = "",
template = require.resolve(
"html-webpack-plugin/default_index.ejs"
),
title,
entry,
} = {}
) => ({
entry,
plugins: [
new HtmlWebpackPlugin({
filename: `${path && path + "/"}index.html`,
title,
}),
],
});