用了这么久的webpack,你不会还没掌握原理?
source link: https://my.oschina.net/jill1231/blog/5407157
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
一、基本要素
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',
};
类型
说明
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
- 开启缓存:
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,
}),
],
};
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK