【工具】webpack4小记
Oct 5, 2019工具工程webpackwebpack 4小记
webpack 5
devServer踩坑
1.配置项更为严格。
报错如:1
2
3
4Invalid 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
3Invalid 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
58devServer: {
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.json1
2
3"scripts": {
"start": "webpack-dev-server --config webpack.config.js"
}
相比于文件监听,WDS不输出文件,而是放在内存中。WSD默认不刷新浏览器,可通过配置devServer.hot: true
开启热刷新。
devServer配置
1 | // webpack.config.js |
HotModuleReplacementPlugin插件
实现热模块替换(HMR)
当我们使用webpack-dev-server的自动刷新功能时,浏览器会整页刷新。
而热替换的区别就在于,当前端代码变动时,无需刷新整个页面,只把变化的部分替换掉。
1 | // webpack.config.js |
webpack-dev-middleware
WDM 将 webpack 输出的文件传输给服务器,可用于灵活的定制场景。
如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const 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:构建输出的文件
具体情况
服务端:
- 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处理
文件压缩
1 | // webpack.config.js |
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 | // webpack.config.js |
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,注意相对路径。
内联html1
${require('raw-loader!babel-loader!./meta.html')}
内联js1
<script>${require('raw-loader!babel-loader!../lib/rem.js')}</script>
css内联
- 方法1:借助style-loader
- 方法2:html-inline-css-webpack-plugin
如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 | // webpack.config.js |
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
3if (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 | // webpack.config.js |
如何只对.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.js1
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返回所有数据
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 | <html> |
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 | const merge = require('webpack-merge'); |
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
57const 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');
}
});
})
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com