一、基本要素
1、Entry/Output
1.1、单入口配置
module.exports = {
entry: './src/index.js', // 打包的入口文件
output: './dist/main.js', // 打包的输出
};
1.2、多入口配置
const path = require('path');
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {、
filename: '[name].[hash].js', //通过占位符确保文件名称的唯一,可选择设置hash
path: path.join(__dirname, 'dist'),
// publicPath用于设置加载静态资源的baseUrl,例如prod模式下指向cdn,dev模式下指向本地服务
publicPath: process.env.NODE_ENV === 'production' ? `//cdn.xxx.com` : '/', //
},
};
2、Loaders
Loaders函数接收文件类型作为参数,返回转换的结果。目前webpack支持的两种类型分别为JS和JSON,其它类型均需转换
2.1、通配Loaders
module:{
rules:[
{test:/.(js|jsx|ts|tsx)$/,use:'ts-loader'} // 例如ts使用ts-loader
]
},
2.2、内联Loaders
Loaders 还可以直接内联到代码中使用:
import 'style-loader!css-loader!less-loader!./style.less';
2.3、多个Loaders
多个 Loaders 之间执行顺序是和 rules 配置相反的,即从右向左执行
2.3.1、源码逻辑
loader 先进后出,对应出栈顺序从右向左
if (matchResourceData === undefined) {
for (const loader of loaders) allLoaders.push(loader);
for (const loader of normalLoaders) allLoaders.push(loader);
} else {
for (const loader of normalLoaders) allLoaders.push(loader);
for (const loader of loaders) allLoaders.push(loader); // 入栈
}
for (const loader of preLoaders) allLoaders.push(loader); // pre loaders入栈
2.3.2、更改顺序
通过配置 enforce 改变执行顺序,enforce有四个枚举值,其执行顺序是pre、normal、inline、post
module:{
rules:[
{
test:/.less$/,
loader:'less-loader',
enforce:'pre' // 预处理
},
{
test: /.less$/,
loader:'css-loader',
enforce:'normal' // 默认是normal
},
{
test: /.less$/,
loader:'style-loader',
enforce:'post' // 后处理
},
]
},
3、Plugins
Plugins负责优化bundle文件、资源管理和环境变量注入,webpack 内置了很多 plugin。例如 DefinePlugin 全局变量注入插件、IgnorePlugin 排除文件插件、ProgressPlugin 打包进度条插件等
plugins: [new HtmlwebpackPlugin({ template: './src/index.html' })];
4、Mode
指定当前的构建环境,有三个选项,分别是:production、development和none,当 mode 是 production 时会启用内置优化插件,比如TreeShaking、ScopeHoisting、压缩插件等
module.exports = {
mode: 'production', // 会写入到环境变量NODE_ENV
};
也可以通过 webpack cli 参数设置
webpack --mode=production
二、热更新
1、更新流程

1.1、启动阶段 1 -> 2 -> A -> B
- 通过
WebpackCompile将JS文件进行编译成Bundle - 将
Bundle文件运行在Bundle Server,使得文件可通过localhost://xxx访问 - 接着构建输出
bundle.js文件给到浏览器
1.2、热更新阶段 1 -> 2 -> 3 -> 4
WebpackCompile将JS文件进行编译成Bundle- 将
Bundle文件运行在HMR Server - 一旦磁盘里面的文件修改,就将有修改的信息输出给
HMR Runtime - 接着
HMR Runtime局部更新文件的变化
2、配置方式
2.1、WDS + HotMoudleReplacementPlugin
2.1.1、WDS(webpack-dev-server)
WDS 提供了 bundle server 的能力,不输出文件,而是放在内存中,即生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,同时它提供的livereload能力,使得浏览器能够自动刷新
// package.json
"scripts":{
"dev":"webpack-dev-server --open"
}
2.1.2、HotMoudleReplacementPlugin 插件
HotMoudleReplacementPlugin插件给 WDS 提供了热更新的能力,源自它拥有局部更新页面能力的HMR Runtime。一旦磁盘里面的文件修改,HMR Server就将有修改的js module信息发送给HMR Runtime
// webpack.dev.js 仅在开发环境使用
module.exports = {
mode: 'development',
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
contentBase: './dist', //服务基础目录
hot: true, //开启热更新
},
};
2.1.3、交互逻辑
监听到文件修改时,HotMoudleReplacementPlugin 会生成一个 mainifest和 update file,其中 mainifest描述了发生变化的 modules ,紧接着webpack-dev-server通过 websocket 通知 client 更新代码,client 使用 jsonp 请求 server 获取更新后的代码
2.2、WDM(webpack-dev-middleware)
WDM 将 webpack 输出的文件传输给服务器,适用于灵活的定制场景
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);
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
}),
);
app.listen(3000, function () {
console.log('listening on port 3000');
});
三、文件指纹
文件指纹主要用于版本管理,表现于打包后文件名的后缀,如xxx//xxx_51773db.js中的51773db
1、三种类型
| 类型 | 含义 |
|---|---|
| Hash | 和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改 |
| Chunkhash | 和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值 |
| Contenthash | 根据文件内容来定义 hash,文件内容不变,则 contenthash 不变 |
2、常用场景
- 设置
output的filename,使用[chunkhash]
filename: '[name][chunkhash:8].js';
- 设置
MiniCssExtractPlugin的filename,使用[contenthash]
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`,
});
- 设置
file-loader的name,使用[hash]
rules: [
{
test: /.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name][hash:8].[ext]',
},
},
],
},
];
// 占位符解释:[name]:文件名称,[ext]:资源后缀名
注意喔:hash是由代码和路径生成的。因此相同的代码在多台机器打包部署 hash 会不同,导致资源加载 404。一般通过一台机器打包,分发部署到不同机器
四、SourceMap
1、开启配置
开发环境开启,线上环境关闭。线上排查问题的时候可以将 source map 上传到错误监控系统
module.exports = {
devtool: 'source-map',
};
2、类型
| 类型 | 说明 |
|---|---|
| cheap-source-map | 没有列号,只有行号,速度快 |
| cheap-module-source-map | 优化后的 cheap-source-map,避免 babel 等编译过代码行号对不上 |
| eval | 通过内联代码 eval 函数 baseURL 确定代码路径 |
| eval-source-map | sourcemap 放在 eval 函数后 |
| inline-source-map | 放在打包代码最后 |
3、文件格式
利用 mappings 映射表和 names、sourcesContent 就可以还原出源码字符串
{
"version": 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}
五、TreeShaking
- 代码不会被执行,不可到达
- 代码执行的结果不会被用到
- 代码只会影响死变量(只写不读)
TreeShaking会将以上视为废弃的代码在uglify阶段消除
当
mode设置为production的情况下,是默认开启的。通过在.babelrc里设置modules:false进行取消
TreeShaking是利用 ES6 模块的特点进行清除
import只能作为模块顶层的语句出现,且模块名只能是字符串常量
import 导入模块是静态加载,其获取的是变量引用,即当模块内部变更时,import出的变量也会变更。因此 import 不能出现在条件、函数等语句中( export类似),而 commonjs 中 require 获取的是模块的缓存
import binding是immutable的
六、模块机制
webpack打包后,会给模块加上一层包裹,import 会被转换成__webpack_require

1、匿名闭包
webpack打包后是一个匿名闭包,接收的参数 modules 是一个数组,每一项是一个模块初始化函数。通过__webpack_require加载模块,并返回modules.exports,

modules 的每个模块成员都是用 __webpack_require__ 加载的,installedModules 是加载模块的缓存,如果已经__webpack_require__加载过无需再次加载。
2、ScopeHoisting
构建后的代码存在大量的闭包代码,导致运行时创建的函数作用域增多,内存开销大,ScopeHoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突,从而减少函数声明代码和内存开销
七、SSR
对SEO友好的服务端渲染SSR的核心是减少请求,从而减少白屏时间。其实现原理是:服务端通过react-dom/server的renderToString方法将React组件渲染成字符串,返回路由对应的模版。协助的客户端通过打包,生成针对服务端的组件
renderToString 携带有 data-reactid 属性可配合 hydrate 使用,会复用之前节点只进行事件绑定从而优化首次渲染速度。类似的方法还有 renderToStaticMarkup
1、兼容问题
1.1、浏览器的全局变量
node.js中没有document和window,需通过打包环境进行适配
在 react ssr 应用中,读取 document 和 window 可以在 useEffect 或 componentDidMount 中进行,当 nodejs 渲染时就会跳过这些执行,避免报错
- 使用
isomorphic-fetch或axios替换fetch和xhr
1.2、样式问题
node.js无法解析css,可使用ignore-loader忽略 css 的解析
对于 antd 组件库,在babel-plugin-import 设置 style 为false
- 使用
isomorphic-style-loader替换style-loader
2、两端协作
使用打包后的HTML为模板,服务端获取数据后替换占位符
八、常见优化措施
1、代码压缩
1.1、JS 文件的压缩
-
内置了
uglifyjs-webpack-plugin -
CommonsChunkPlugin提取chunks中的公共模块减少总体积
1.2、CSS 文件的压缩
-
使用
optimize-css-assets-webpack-plugin,同时使用cssnano -
extract-text-webpack-plugin将css从产物中分离。
1.3、html 文件的压缩
html-webpack-plugin 通常用来定义 html 模板,也可以设置压缩 minify 参数(production 模式下自动设置 true)
1.4、图片压缩
使用image-webpack-loader
2、自动清理构建目录
利用 CleanWebpackPlugin 自动清理 output 指定的输出目录
3、静态资源内联
首屏渲染的样式尽量选择内联或使用 styled-components。资源内联可减少请求数,可避免首屏页面闪动,可进行相关上报打点,可初始化脚本
3.1、代码层面
- raw-loader:js/html 内联
- style-loader: css 内联
3.2、请求层面
-
url-loader:小图片或字体内联
-
file-loader:可以解析项目中的 url 引入路径,修改打包后文件引用路径,指向输出的文件。
4、基础库分离
4.1、HtmlWebpackExternalsPlugin
将基础包通过cdn,而不压缩进bundle中
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: '//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123',
global: 'React',
},
],
}),
];
4.2、SplitChunksPlugin
可将公共脚本、基础包以及页面公共文件分离
splitChunks:{
chunks:'async',// async:异步引入的库进行分离(默认) initial:同步引入的库进行分离 all:所有引入的库进行分离(推荐)
...
cacheGroups:{
// 1、公共脚本分离
vendors:{
test:/[\/]node_modules[\/]/,
priority:-10
},
// 2、基础包分离
commons:{
test:/(react|react-dom)/,
name:'vendors',
chunks:'all'
},
// 3、页面公共文件分离
commons:{
name:'commons',
chunks:'all',
minChunks:2
}
}
}
4.3、分包
plugins: [
// 使用DLLPlugin进行分包
new webpack.DLLPlugin({
name: '[name]',
path: './build/library/[name].json',
}),
// DllReferencePlugin 对 manifest.json引用
new webpack.DllReferencePlugin({
manifest: require('./build/library/manifest.json'),
}),
];
5、多进程多实例构建
多进程多实例构建,换句话说就是:每次webpack解析一个模块,将它及它的依赖分配给worker线程中,比如HappyPack、ThreadLoader

6、缓存
- 开启缓存:
babel-loader、terser-webpack-plugin - 使用
cache-loader、hard-source-webpack-plugin
7、缩小构建目标、减少文件搜索范围
- 合理配置
loader的test,使用include来缩小loader处理文件范围
module.exports = {
module: {
rules: [
{
test: /.js$/, // 尾部补充$号表示尾部匹配
use: ['babel-loader?cacheDirectory'], // babel-loader 通过 cacheDirectory 选项开启缓存
include: path.resolve(__dirname, 'src'), // 只处理src目录下代码,极大提升编译速度。(如果node_modules下有未编译过的库,这里不建议开启)
},
],
},
};
- 优化 resolve 配置:
module.exports = {
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
extensions: ['.js', '.json'], // extensions尽量少,减少文件查找次数
noParse: [/.min.js$/], // noParse可以忽略模块的依赖解析,对于min.js文件一般已经打包好了
},
};
九、可维护的 webpack 构建配置
1、多个配置文件管理不同环境的 webpack 配置

1.1、通过webpack-merge合并配置
merge = require('webpack-merge');
module.exports = merge(baseConfig, devConfig);
2、webpack 构建分析
2.1、日志分析
在package.json文件的构建统计信息字段添加stats
"scripts":{
"build:stats":"webpack --env production --json > stats.json"
}
2.2、速度分析
利用 speedMeasureWebpackPlugin分析整个打包总耗时和每个插件和loader的耗时情况
const speedMeasureWebpackPlugin = require("speed-measure-webpack-plugin")
const smp = new speedMeasureWebpackPlugin()
const webpackConfig = smp.wrap({
plugins:[
new MyPlugin()
...
]
})
2.3、体积分析
利用bundleAnalyzerPlugin分析依赖的第三方模块文件大小和业务里面的组件代码大小,构建完成后会在 8888 端口展示
const bundleAnalyzerPlugin = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new bundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: 'localhost',
analyzerPort: 8888, // 端口号
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false, // 是否输出到静态文件
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info',
}),
],
};
2.4、编译时进度分析
利用ProgressPlugin分析编译进度和模块处理细节
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProgressPlugin({
activeModules: false,
entries: true,
handler(percentage, message, ...args) {
// 打印实时处理信息
console.info(percentage, message, ...args);
},
modules: true,
modulesCount: 5000,
profile: false,
dependencies: true, // 显示正在进行的依赖项计数消息
dependenciesCount: 10000,
percentBy: null,
}),
],
};

