前言
现如今前端构建打包工具层出不穷,有 Webpack
、Vite
、Rollup
...,但无论是哪一个工具,还是从一个工具切换到一个新颖的工具,实际上最终都离不开 解析编译,模块分包,压缩优化三个阶段。本文使用 Webpack5
解释并配置这三个阶段。
三大阶段
整篇文章都会围绕以下这张图讲解。最左边是多入口的业务文件,中间是构建打包三个阶段,最右边是产物文件。
解析编译
入口
Webpack 在读取配置的时候会先读取 entry 字段,该字段就是入口文件地址。
entry: {
'entry1': path.resolve(process.cwd(), './app/entry1/entry1.js'),
'entry2': path.resolve(process.cwd(), './app/entry2/entry2.js')
}
输出路径
不同环境文件的输出路径有所不同。
生产环境
output: {
// 文件名
filename: 'js/[name]_[chunkhash:8].bundle.js',
// 输出路径
path: path.resolve(process.cwd(), './app/public/dist/prod/'),
// 公共路径
publicPath: '/dist/prod/'
},
开发环境
开发环境不需要文件落地,通过 devServer (下面会讲如何使用中间件实现)放置到内存中。
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(), './app/public/dist/dev/'), // 输出文件存储路径
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 外部资源文件公共路径
},
模块解析
有一些模块需要通过解析器进行被更好的识别/兼容/优化。比如 .vue 结尾的文件需要转换成 .js 结尾才能被浏览器识别,.less 结尾的文件需要转换成 .css 结尾的文件,.css 结尾的文件需要转换成 style 标签 ...
模块解析需要配置在 module 字段下面。
// 模块解析配置(决定了要加载解析哪些模块, 以及用什么方式去解释)
module: {
rules: [],
},
处理 .vue 文件
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
},
},
处理 .js 文件
{
test: /\.js$/,
include: [
// 只对业务代码进行 babel 处理
path.resolve(process.cwd(), './app/pages'),
],
use: {
loader: 'babel-loader',
},
},
处理资源文件
对小图片进行 base64 转换。
{
test: /\.(png|jpe?g|gif|svg)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300
},
},
},
在引入其他静态文件的时候,输出到 output 目录,并且修改成正确的 url。
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
use: 'file-loader',
},
处理 css 相关文件
less -> css
css -> <style>
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
文件扩展名和路径别名
resolve: {
// import xxx from './xxx.vue' -> import xxx from './xxx'
extensions: ['.js', '.vue', '.less', '.css'],
alias: {
// 配置别名 ./app/pages/xxx -> $pages/xxx
$pages: path.resolve(process.cwd(), './app/pages'),
$store: path.resolve(process.cwd(), './app/pages/store'),
},
},
plugins
plugins: [
// 处理 .vue 文件
// 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里。
// 例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。
new VueLoaderPlugin(),
// 把第三方库暴露到 window context 下
new webpack.ProvidePlugin({
Vue: 'vue',
axios: 'axios',
_: 'lodash',
}),
// 构造最终渲染的页面模板 entry1
new HtmlWebpackPlugin({
// 产物 (最终模板) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
// 指定要使用的模板文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块 与入口对应
chunks: ['entry1'],
}),
// 构造最终渲染的页面模板 entry2
new HtmlWebpackPlugin({
// 产物 (最终模板) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
// 指定要使用的模板文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块 与入口对应
chunks: ['entry2'],
})
],
模块分包
把 js 文件打包成三个类型:
- vendor: 第三方库,基本不会改,除非依赖升级。
- common: 业务组件的公共部分抽取出来,改动较少。
- entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动。
目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果
// 配置打包输出优化 (代码分割, 模块合并 等优化策略)
optimization: {
splitChunks: {
chunks: 'all', // 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每个异步加载模块最多的并行请求数
maxInitialRequests: 10, // 一个入口的最大并行请求数
cacheGroups: {
vendor: {
// 第三方依赖库
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的模块
name: 'vendor', // 模块名称
priority: 20, // 优先级 数字越大优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true, // 复用已经存在的 chunk
},
common: {
// 公共模块
test: /[\\/]common|widgets[\\/]/,
name: 'common',
minChunks: 2, // 最少引用次数
minSize: 1, // 最小分割文件大小 字节为单位
priority: 10,
reuseExistingChunk: true,
},
},
},
// 将 webpack 运行时的代码单独抽离出来 runtime.js
runtimeChunk: true,
}
压缩/优化
生产环境
css
多线程处理 happypack
抽离公共部分 MiniCssExtractPlugin
压缩 CSSMinimizerPlugin
// 多线程 build 设置
const happypackCommonConifig = {
debug: false,
threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
};
module: {
rules: [{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css'],
}]
},
plugins: [{
// 提取 css 的公共部分
new MiniCssExtractPlugin({
chunkFilename: 'css/[name]_[chunkhash:8].bundle.css', // 非入口 chunk 的名称
}),
// 优化并压缩 css 资源
new CSSMinimizerPlugin(),
// 多线程打包 CSS,加快打包速度
new HappyPack({
...happypackCommonConifig,
id: 'css',
loaders: [
{
path: 'css-loader',
options: {
importLoaders: 1,
},
},
],
}),
}]
js
多线程处理 loader happypack
并行压缩 TerserWebpackPlugin
module: {
rules: [{
test: /\.js$/,
include: [
// 只对业务代码进行 babel 处理,加快 webpack 打包速度
path.resolve(process.cwd(), './app/pages'),
],
use: ['happypack/loader?id=js'],
}]
},
plugins: [
// 多线程打包 JS,加快打包速度
new HappyPack({
...happypackCommonConifig,
id: 'js',
loaders: [
`babel-loader?${JSON.stringify({
presets: ['@babel/preset-env'],
plugins: [
'@babel/plugin-transform-runtime'
],
})}`,
],
}),
]
optimization: {
// 使用 TerserPlugin 的并发和缓存,提升压缩阶段性能
// 清除 console.log
minimize: true,
minimizer: [
new TerserWebpackPlugin({
parallel: true, // 利用多核 CPU 进行压缩
cache: true, // 启动缓存来加速构建过程
terserOptions: {
compress: {
drop_console: true, // 删除所有的 `console` 语句
},
},
}),
],
}
其他优化
打包前清空目录
plugins: [
// 每次 build 前,清空 public/dist 目录
new CleanWebpackPlugin(['public/dist'], {
root: path.resolve(process.cwd(), './app/'),
exclude: [],
verbose: true,
dry: false,
}),
]
开发环境
热更新 HMR
开启 devServer 去热更新,需要具备两种能力,一种是监控文件变化的能力,一种是通知页面可以去更新代码的能力。
启动 devServer 的时候,可以将打包构建的其他代码放入内存,将双向通信的代码片段注入模板文件,当启动模块页的服务时就能建立与 devServer 双向通信的桥梁。
devServer
前置配置
const DEV_SERVER_CONFIG = {
HOST: '127.0.0.1',
PORT: 9002,
HMR_PATH: '__webpack_hmr', // 官方规定
TIMEOUT: 20000,
};
这里使用 express 启动服务器,然后使用下面两个中间件:
监听文件改动:webpack-dev-middleware
通知文件更新:webpack-hot-middleware
// 本地开发启动 devServer 配置
const express = require('express');
const path = require('path');
const webpack = require('webpack');
const consoler = require('consoler');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');
const webpackDevConfig = require('./config/webpack.dev.js');
const app = express();
// 从 webpack.dev.js 获取 webpack 配置 和 devServer 配置
const { webpackConfig, DEV_SERVER_CONFIG } = webpackDevConfig;
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname, '../public/dist')));
// 引用 webpack-dev-middleware 中间件 (监控文件改动)
app.use(
devMiddleware(compiler, {
// 落地文件: 生成的 tpl
writeToDisk: (filePath) => filePath.endsWith('.tpl'),
// 资源路径
publicPath: webpackConfig.output.publicPath,
// headers 配置
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Method': 'GET,POST,PUT,DELETE,OPTIONS,PATCH',
'Access-Control-Allow-Headers':
'X-Requested-With,content-type,Authorization',
},
// 控制台输出
stats: {
colors: true,
},
}),
);
// 引用 webpack-hot-middleware 中间件 (热更新通讯)
app.use(
hotMiddleware(compiler, {
log: () => {},
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
}),
);
consoler.info('请等待 webpack 初次构建完成提示...');
// 启动 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
console.log(`app listening on port ${port}`);
});
HMR 相关的 Webpack 配置
entry: {
// 注入代码
'entry1': [
path.resolve(process.cwd(), './app/entry1/entry1.js'),
`webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}?timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`,
],
'entry2': [
path.resolve(process.cwd(), './app/entry2/entry2.js'),
`webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}?timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`,
]
},
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(), './app/public/dist/dev/'), // 输出文件存储路径
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 外部资源文件公共路径
},
// 开发阶段插件
plugins: [
// 实现热模块替换
// 模块热替换允许在运行时更新各种模块
new webpack.HotModuleReplacementPlugin({
multiStep: false,
}),
]
其他优化
开启 sourceMap,呈现代码的映射关系,便于在开发过程中调试代码
devtool: 'eval-cheap-module-source-map',
总结
以上就是使用 Webpack5
实现前端构建打包过程中的三个阶段。这里无论是哪种构建工具都可以实现这三个阶段,工具可以变,使用方式可以变,但核心原理核心阶段都是不变的。