webpack 4小记

webpack 5

devServer踩坑

1.配置项更为严格。

报错如:

1
2
3
4
Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration.devtool should match pattern "^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$".
BREAKING CHANGE since webpack 5: The devtool option is more strict.
Please strictly follow the order of the keywords in the pattern.

如上devtool的报错极有可能是命令中声明了参数,如webpack-dev-server -d --hot --env.dev,其中-d就是--devtool,此时去掉-d参数就好。

2.config配置

报错如:

1
2
3
Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- options has an unknown property 'contentBase'. These properties are valid:
object { allowedHosts?, bonjour?, client?, compress?, devMiddleware?, headers?, historyApiFallback?, host?, hot?, http2?, https?, ipc?, liveReload?, magicHtml?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, port?, proxy?, server?, setupExitSignals?, setupMiddlewares?, static?, watchFiles?, webSocketServer? }

devServer配置做了较大改动,配置可选字段一般可在报错的object字段中看出。需要注意的是contentBase在高版本(4.0.0起)也做了调整(虽然webpack文档上还有此字段),此时需要用static声明,如:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
devServer: {
port: 3000,
hot: true,
static: {
directory: path.join(__dirname, 'src'),
},
// contentBase: path.join(__dirname, 'src'),
historyApiFallback: {
index: '/assets/',
disableDotRule: true,
},
},
```
还有publicPath也做了调整,即:
- `publicPath`变成了`static`下的`publicPath`。
- `contentBase`变成了`static`下的`directory`。



---

## * webpack 4和webpack 3

webpack4将webpack拆分为webpack和webpack-cli。webpack启动命令行的代码放入了webpack-cli 中。如果只安装了webpack,那么它只能在nodejs中使用,不能再命令行中使用。


## 1.mode
webpack增加了一个mode配置,用来指定当前的构建环境是:production、development还是none。设置 mode 可以使用webpack 内置的函数,默认值为 production。

选项 | 描述
---- | ----
development | 设置 `process.env.NODE_ENV`的值为`development`,开启`NamedChunksPlugin` 和 `NamedModulesPlugin`
production | 设置 `process.env.NODE_ENV`的值为`production`,开启 `FlagDependencyUsagePlugin`,`FlagIncludedChunksPlugin`,`ModuleConcatenationPlugin`,`NoEmitOnErrorsPlugin`,`OccurrenceOrderPlugin`,`SideEffectsFlagPlugin`和`TerserPlugin`
none | 不开启任何优化选项

## 2.文件监听

webpack文件监听是在发现源代码发生变化时,自动重新构建出新的输出文件。开启监听模式有两种方式:
- 启动webpack命令时,带上`--watch`参数;
- 在配置webpack.config.js中设置`watch: true`。

如:
``` js
// webpack.config.js

module.exports = {
// 默认为false
watch: true,
// 只有开启监听模式,watchOptions才有意义
watchOptions: {
// 不监听的文件或文件夹,字符串或正则,默认为空
ignored: /node_modules/,
// 监听到变化发生后会等300ms再执行,默认300
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000
}
}

文件监听的大致原理

轮询判断文件的最后编辑时间是否变化。某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout,批量更新。

3.热更新webpack-dev-server

安装

1
npm i webpack-dev-server -D

配置package.json

1
2
3
"scripts": {
"start": "webpack-dev-server --config webpack.config.js"
}

相比于文件监听,WDS不输出文件,而是放在内存中。WSD默认不刷新浏览器,可通过配置devServer.hot: true开启热刷新。

devServer配置

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
const path = require('path');
module.exports = {
//...
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
hot: true, // 整页刷新
port: 9000
}
};

HotModuleReplacementPlugin插件

实现热模块替换(HMR)

当我们使用webpack-dev-server的自动刷新功能时,浏览器会整页刷新。
而热替换的区别就在于,当前端代码变动时,无需刷新整个页面,只把变化的部分替换掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
const path = require('path');
module.exports = {
//...
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
hot: true, // 整页刷新
port: 9000
},
plugins: [
new webpack.HotModuleReplacementPlugin(), //hot module replacement 启动模块热替换的插件
new webpack.NamedModulesPlugin(), //用于启动HMR时可以显示模块的相对路径
// ...
]
};

webpack-dev-middleware

WDM 将 webpack 输出的文件传输给服务器,可用于灵活的定制场景。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express(),
config = require('./webpack.config.js'),
compiler = webpack(config);

app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
})

app.listen(3000, function () {
console.log('listen port 3000');
})

热更新原理

基于webSocket。

  • Webpack Compile:将js编译成Bundle
  • HMR Server:将热更新的文件输出给HMR Runtime
  • Bundle Server:提供文件在浏览器的访问
  • HMR Runtime:会被注入到浏览器,更新文件的变化
  • bundle.js:构建输出的文件

p-1.jpg

具体情况

服务端:

  • 1.启动webpack-dev-server服务器
  • 2.创建webpack实例
  • 3.创建Server服务器
  • 4.添加webpack的done事件回调 编译完成向客户端发送消息(hash和描述文件oldhash.js和oldhash.json)
  • 5.创建express应用app
  • 6.设置文件系统为内存文件系统
  • 7.添加webpack-dev-middleware中间件 负责返回生成的文件
  • 8.创建http服务 启动
  • 9.使用socket 实现浏览器和服务器的通信(这里先发送一次hash,将socket存入到第四步,初次编译完第四步中的socket是空,不会触发hash下发)

客户端:

  • 1.webpack-dev-server/client-src下文件监听hash,保存此hash值
  • 2.客户端收到ok消息执行reload更新
  • 3.在reload中进行判断,如果支持热更新执行webpackHotUpdate,不支持的话直接刷新页面
  • 4.在webpack/hot/dev-server.js监听webpackHotUpdate 然后执行 check() 方法进行检测
  • 5.在check方法里面调用module.hot.check
  • 6.通过调用 JsonpMainTemplate.runtime的hotDownloadmainfest方法,向server端发送ajax请求,服务端返回一个Mainfest文件,该文件包含所有要更新模块的hash值和chunk名
  • 7.调用 JsonpMainTemplate.runtime 的 hotDownloadUpdateChunk方法通过jsonp请求获取到最新的模块代码
  • 8.补丁js取回后调用 JsonpMainTemplate.runtime 的 webpackHotUpdate方法,里面会调用hotAddUpdateChunk方法,用心的模块替换掉旧的模块
  • 9.调用HotMoudleReplacement.runtime.js 的 hotAddUpdateChunk方法动态更新模块代码
  • 10.调用 hotApply 方法热更新

4.文件指纹

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的hash值就会更改
  • Chunkhash:和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值
  • Contenthash:根据文件内容来定义hash,文件内容不变,则contenthash不变。

设置文件指纹

js:output.filename。如:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
// ...
output: {
filename: '[name].[contenthash].js',
// ...
}
}

css: MiniCssExtractPlugin 的 filename,建议使用[contentBase]

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
// ...
plugins: [
new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
})
]
}

注意mini-css-extract-plugin和style-loader不能共存。

图片:设置file-loader的name,建议使用[hash]

占位符名称 含义
[ext] 资源后缀名
[name] 文件名称
[path] 文件的相对路径
[folder] 文件所在的文件夹
[contenthash] 文件内容hash,默认是md5生成
[hash] 文件内容的hash,默认色md5生成
[emoji] 一个随机的指代文件内容的emoji

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
use: [{
loader: 'file-loader',
options: {
name: 'img/[name][hash:8].[ext]'
}
}]
}
]
}
}

5.自动清理构建目录

避免构建前每次都要手动删除dist。

  • 方法1:rm -rf,如rm -rf dist/ && webpack
  • 方法2:rimraf,如rimraf dist/ && webpack
  • 方法3(推荐):clean-webpack-plugin,如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // webpack.config.js
    const CleanWebpackPlugin = require('clean-webpack-plugin');

    module.exports = {
    // ...
    plugins: [
    new CleanWebpackPlugin() // 默认会删除output指定的输出目录
    ]
    }

6.html处理

使用html-webpack-plugin

文件压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ['manifest', 'vendor', 'index', 'commons'],
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
removeAttributeQuotes: false
}
})
]
}

7.css处理

文件压缩

使用 mini-css-extract-plugin或者optimize-css-assets-webpack-plugin

如:

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
// ...
plugins: [
new MiniCssExtractPlugin({
filename: `css/[name][contenthash:8].css`,
chunkFilename: `[id].css`
})
]
}

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
// ...
plugins: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g
})
]
}

cssnano 做文件优化


1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
// ...
plugins: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
]
}

移动端px转rem/vw

可参考《postcss部分插件小记》-postcss-px-to-viewport,postcss-plugin-px2rem,或使用px2rem-loader

8.资源内联

意义

代码层面:

  • 页面框架的初始化脚本;
  • 上报相关打点
  • CSS内联避免页面闪动

请求层面:

  • 减少HTTP网络请求数
  • 小图片或者字体内联(url-loader)

html和js内联

使用raw-loader,注意相对路径。

内联html

1
${require('raw-loader!babel-loader!./meta.html')}

内联js

1
<script>${require('raw-loader!babel-loader!../lib/rem.js')}</script>

css内联


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.scss$/,
use: [
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到`<head>`
singleton: true, // 将所有style标签合并成一个
},
'css-loader',
'sass-loader'
]
}
]
}
}

1
2
3
4
5
6
7
8
9
// webpack.config.js
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin");

module.exports = {
// ...
plugins: [
HTMLInlineCSSWebpackPlugin()
]
}

9.多页面打包通用方案

动态获取entry 和设置html-webpack-plugin数量。利用glob.sync。

10.source map关键字

  • eval:使用eval包裹模块代码
  • source map:产生.map文件
  • cheap:不包含列信息
  • inline:将.map作为DataURI嵌入,不单独生成.map文件
  • module:包含loader的sourcemap

webpack.config.js通过devtool字段配置。

devtool 首次构建 二次构建 是否适合生产环境 可以定位的代码
(none) +++ +++ yes 最终输出的代码
eval +++ +++ no webpack生成的代码(一个个都模块)
cheap-eval-source-map + ++ no 经过loader转换后的代码(只能看到行)
cheap-module-eval-source-map 0 ++ no 源代码(只能看到行)
eval-source-map + no 源代码
cheap-source-map + 0 yes 经过loader转换后的代码(只能看到行)
cheap-module-source-map 0 - yes 源代码(只能看到行)
inline-cheap-source-map + 0 no 经过loader转换后的代码(只能看到行)
inline-cheap-module-source-map 0 - no 源代码(只能看到行)
source-map yes 源代码
inline-source-map no 源代码
hidden-source-map yes 源代码

10.使用SplitChunksPlugin进行公共脚本分离

webpack4内置,替代CommonsChunkPlugin插件(只能统一抽取到父chunk,造成父chunk过大,不可避免的存在重复引入,引入多余代码的问题)。

chunks参数:

  • async:异步引入的库进行分离(默认)
  • initial:同步引入的库进行分离
  • all:所有引入的库进行分离(推荐)

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
}
}
}

基础库分离

思路:将react、react-dom基础包通过cdn引入,不打入bundle中。
方法:使用html-webpack-externals-plugin。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// webpack.config.js
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

module.exports = {
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: '//cdn.cn/XXXXXXXX/react.min.js'
},
{
module: 'react-dom',
entry: '//cdn.cn/XXXXXXXX/react-dom.min.js'
}
]
})
]
}

效果:

1
2
3
4
5
6
7
8
<html>
<head>...</head>
<body>
...
<script src="//cdn.cn/XXXXXXXX/react.min.js"></script>
<script src="//cdn.cn/XXXXXXXX/react-dom.min.js"></script>
</body>
...

页面公共文件分离

通过设置minChunks(最小引用次数),minuSize(分离的包体积的大小)

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module.exports = {
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2
}
}
}
}
}

基础包分离

如分离react,react-dom。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js

module.exports = {
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}

11.tree shaking(摇树优化)

概念:1个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会在uglify阶段被檫除到。

使用:webpack默认支持,在.babelrc里设置modules: false即可

  • production mode情况下默认开启
  • 注意:必须是ES6的语法模块,commonjs的方式不支持

DCE(Elimination)

  • 代码不会被执行,不可到达
  • 代码执行的结果不会被用到
  • 代码只会影响死变量(只写不读)

如:

1
2
3
if (false) {
console.log('never...')
}

原理

利用ES6模块的特点:

  • 只能作为模块顶层的语句出现
  • import的模块名只能是字符串常量
  • import binding是immutable的

代码擦除:uglify阶段删除无用代码。

12.模块转换

被webpack转换后的模块会带上一层包裹,import会被转换成__webpack__require
webpack打包出来的是一个IIFE(匿名闭包),modules是一个数组,每一项是一个模块初始化函数,webpackrequire用来加载模块,返回module.exports,通过WEBPACK_REQUIRE_METHOD(0)启动函数

scope hoisting

优化webpack的模块转换引用,可以减少函数声明代码量和内存开销。原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当得重命名一些变量以防止变量名冲突。

webpack mode为production默认开启

必须是ES6的语法模块,commonjs的方式不支持。

或:

1
2
3
4
5
6
7
// webpack.config.js

module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
]
}

13.代码分割

意义:对于大的web应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被使用到。webpack有一个功能就是将你的代码库分割成chunks(语块),当代码运行到需要它们的时候再进行加载。

使用场景:

  • 抽离相同代码到一个共享块
  • 脚本懒加载,使得初始下载的代码更小

懒加载js方式

  • CommonJS:require.ensure
  • ES6:动态import(目前还没有原生支持,需要babel转换)

使用动态import

安装babel插件:babel-plugin-syntax-dynamic-import

1
npm i @babel/plugin-syntax-dynamic-import -D

配置babel:

1
2
3
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

14.打包库和组件

  • library:指定库的全局变量
  • libraryTarget:支持库引入的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js

module.exports = {
entry: {
"test": "./src/index.js",
"test.min": "./src/index.js",
},
output: {
filename: '[name].js',
library: 'test',
libraryExport: 'default',
libraryTarget: 'umd'
}
}

如何只对.min压缩

通过include设置只压缩min.js结尾的文件


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.config.js

module.exports = {
entry: {
"test": "./src/index.js",
"test.min": "./src/index.js",
},
output: {
filename: '[name].js',
library: 'test',
libraryExport: 'default',
libraryTarget: 'umd'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/
})
]
}
}

设置入口文件

设置package.json的main字段为index.js

1
2
3
4
5
6
// index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/test');
} {
module.exports = require('./dist/test.min');
}

15.SSR

SSR渲染:HTML+CSS+JS+Data -> 渲染后的HTML

服务端:

  • 所有模板等资源都存储在服务器
  • 内网机器拉取数据更快
  • 一个HTML返回所有数据

p-2.jpg

CSR对比SSR

- 客户端渲染 服务端渲染
请求 多个请求 一个请求
加载过程 HTML&数据串行加载 一个请求返回HTML&数据
渲染 前端渲染 服务端渲染

SSR的核心是减少请求。优势是减少白屏时间和对SEO友好。

SSR实现思路

服务端:

  • 使用react-dom/server的renderToString方法将React组件渲染成字符串
  • 服务端路由返回对应的模板

客户端:

  • 打包出针对服务端的组件

webpack打包SSR的问题

浏览器的全局变量(Nodejs没有document,window)

  • 组件适配:将不兼容的组件根据打包环境进行适配
  • 请求适配:将fetch或ajax发送请求的写法改成isomorphic-fetch或者axios

样式问题(Nodejs无法解析CSS)

  • 服务端打包通过ignore-loader忽略掉CSS的解析
  • 将style-loader替换成isomorphic-style-loader

解决样式不显示的问题

使用打包出来的浏览器端html为模板,设置占位符,动态插入组件。

1
2
3
4
<html>
<head>
<!--CSS_PLACEHOLDER-->
</head>

16.打包的统计信息stats

Preset Alternative Description
“errors-only” none 只在发生错误时输出
“minimal” none 只在发生错误或有新的编译时输出
“none” false 没有输出
“normal” true 标准输出
“verbose” none 全部输出

如何优化命令行的构建日志

使用friendly-errors-webpack-plugin

  • success:构建成功的日志提示
  • warning:构建警告的日志提示
  • error:构建报错的日志提示

stats设置为errors-only

如:

1
2
3
4
5
6
7
8
// webpack.config.js

module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin()
],
stats: 'errors-only'
}

异常和中断处理

webpack4之前的版本构建失败不会抛出错误码。

Node.js中的process.exit规范

  • 0:表示成功完成,回调函数中,err为null
  • 非0表示执行失败,回调函数中,err不为null,err.code就是传给exit的数字

webpack的compiler在每次构建结束后会触发done这个hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js
module.exports = {
plugins: [
function () {
this.hooks.done.tap('done', stats => {
if (stats.compilation.errors && stats.compilation.errors.length && ~process.argv.indexOf('watch')) {
console.log('build error');
process.exit(1)
}
})
}
]
}

17.webpack-merge组合配置

1
2
3
4
5
const merge = require('webpack-merge');

// ...

module.exports = merge(baseConfig, devConfig);

18.冒烟测试(smoke testing)

冒烟测试是指对提交测试的软件在进行详细深入的测试之前而进行的预测试,这种预测试的主要目的是暴露导致软件需要重新发布的基本功能失效等严重问题。

  • 构建是否成功
  • 每次构建完成build目录是否有内容输出
    • 是否有js、css等静态资源文件
    • 是否有HTML文件

判断是否构建成功

如:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const path = require('path'),
webpack = require('webpack'),
rimraf = require('rimraf'),
Mocha = require('mocha');
const mocha = new Mocha({
timeout: '10000ms'
});

process.chdir(__dirname);
rimraf('./dist', () => {
const prodConfig = require('../../lib/webpack.prod');
webpack(prodConfig, (err, stats) => {
if (err) {
console.error(err);
return false;
}
console.log(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
}));
console.log('\n Compiler success');
})
})
```

### 判断内容输出
``` js
const glob = require('glob-all');

describe('checking generated file exists', function () {
it('should generate html files', function (done) {
const files = glob.sync([
'./dist/index.html'
])

if (files.length > 0) {
done();
} else {
throw new Error('No html files found');
}
});
it('should generate js & css files', function (done) {
const files = glob.sync([
'./dist/index*.js',
'./dist/index*.css',
])

if (files.length > 0) {
done();
} else {
throw new Error('No html files found');
}
});
})

文档