一、基本要素
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
为模板,服务端获取数据后替换占位符
<body>
<div id="root">
<!--HTML_PLACEHOLDER-->
</div>
<!--INITIAL_DATA_PLACEHOLDER-->
</body>
八、常见优化措施
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,
}),
],
};