前言
继上次分享了从抖音哲玄大佬《大前端全栈实践》课程中学到的 BFF 层框架封装思路后,我的学习之旅继续推进到了第二章——围绕 Webpack 的前端基建。这一部分同样干货满满。下面,我将继续分享这部分学习带来的收获与思考。
也许你会好奇,现在打包工具层出不穷,vite、esbuild等,速度都比webapck要好,为什么还用webpack?但在我看来,工具只是一个手段,构建等手段和优化等方法都是万变不离其宗的,而且webapck也能让我们更好的了解前端基建设的原理。
作为前端,想必大家对 webpack 都有所了解,下面我就挑一些重点的基建来展开说说。
自动多入口打包配置
从下图⬇️可以看出,我们的项目是要实现一套系统模板,一个模板页,可以延伸出多套系统,从而减少搭建一套系统80%的重复性工作,对剩下的20%工作进行自定义。而一个系统需要一个入口文件(如一个vue项目),多个系统,就需要对多个入口进行打包。
多系统框架:
但如果每新增一个系统,都要修改一次webapck配置,开发体验就不太好,我们需要自动获取所有系统的入口文件并配置到webpack中:
const path = require("path");
const glob = require("glob");
const webpack = require("webpack");
// 多入口配置
const entries = {};
// 获取pages页面目录下的所有入口文件main.js
const pages = glob.sync(path.resolve(process.cwd(), "./app/pages/**/main.js"));
// 不同入口文件打包输出的html配置
const htmlWebpackPlugins = [];
// 遍历获取入口文件路径
pages.forEach((page) => {
const len = page.split("/").length;
const name = page.split("/")[len - 2];
const entryName = `entry.${name}`;
entries[entryName] = page;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.resolve(process.cwd(), `./app/view/entry.tpl`),
filename: `${name}.tpl`, // 生成的html文件名
chunks: [entryName],
})
);
});
module.exports = {
// 入口文件配置
entry: entries,
plugins: [
// 打包输出的html配置
...htmlWebpackPlugins,
// ...其他配置
],
// ...其他配置
};
打包性能优化
代码拆分
为什么要拆分代码?
- 减少代码冗余: 通过将多个文件共用的代码提取到独立的公共 chunk 中,避免重复打包,有效减小最终产物的总体积。
- 提升缓存效率: 将变动频率较低的代码(如第三方库 node_modules)分离到单独的 chunk。只要这些库版本不变,其文件名(通常基于内容哈希)就能保持稳定,浏览器便可以长期缓存这些资源,减少不必要的网络请求,加快页面加载速度。
- 加快首屏加载速度: 从主包中将首屏加载不需要对非关键代码拆分出去,减少首屏加载资源体积,加快加载速度。
- 利用浏览器并发请求: 一个包被拆分成多个包时,浏览器就可以并发获取多个包,提升加载速度。
module.exports = {
module: {
rules: [
// 通过 MiniCssExtractPlugin.loader 处理 css 文件,将 css 文件提取出去
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [
// 将 css 从 js 中拆分出去,有效利用缓存
new MiniCssExtractPlugin({
filename: "css/[name]_[chunkhash:8].bundle.css",
}),
],
optimization: {
// 代码拆分
splitChunks: {
chunks: "all", // 所有chunk类型
maxAsyncRequests: 10, // 异步加载的chunk最大并行请求数量
maxInitialRequests: 10, // 初始加载的chunk最大并行请求数量
cacheGroups: {
// 将node_modules第三方库拆分到vendor包
vendor: {
name: "vendor",
priority: 20,
test: /[\\/]node_modules[\\/]/,
enforce: true, // 强制执行
reuseExistingChunk: true, // 重用已存在的chunk
},
// 将被引用两次以上的公共代码拆分到 common 包
common: {
name: "common",
priority: 10,
minChunks: 2, // 最小被引用次数
minSize: 1024 * 5, // 最小分割文件大小 byte
reuseExistingChunk: true, // 重用已存在的chunk
},
},
},
},
}
代码压缩
生产环境下,对js、css、图片等文件进行压缩,减少资源体积。
- js:webpack5在mode为production时,自动使用terser插件对js进行压缩,无需配置;
- css:使用 css-minimizer-webpack-plugin
module.exports = {
plugins: [
// 压缩css
new CssMinimizerPlugin(),
],
// ...其他配置
}
手动实现 HMR 热更新
webpack5 自带 devServer 功能,但是为了更好的理解热更新的原理,我们选择手动实现devServer
- 通过 webapck-dev-middleware 监听代码文件变化并触发重新编译
- 通过 webpack-hot-middleware 来通知浏览器请求获取最新代码
开发环境 webpack配置:
const { merge } = require("webpack-merge");
const webpack = require("webpack");
// devServer 配置
const DEV_SERVER_CONFIG = {
PORT: 8080,
HOST: "127.0.0.1",
HMR_PATH: "/__webpack_hmr", // 官方规定
TIMEOUT: 20000,
};
// 开发环境给entry添加热模块更新
Object.keys(baseConfig.entry).forEach((key) => {
baseConfig.entry[key] = [
baseConfig.entry[key],
// 热模块更新入口,官方指定的 hmr 路径,注意path必须只想热更新所在服务路径
`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`,
];
});
const webpackConfig = merge(baseConfig, {
mode: "development",
output: {
path: path.join(process.cwd(), "./app/public/dist"),
filename: "js/[name].bundle.js",
// 静态资源请求路径,请求到热更新服务中,服务会将内存中的资源返回
publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/`,
globalObject: "this", // 全局对象,默认是 window
},
plugins: [
// 热模块更新
new webpack.HotModuleReplacementPlugin(),
],
});
module.exports = {
webpackConfig,
DEV_SERVER_CONFIG,
};
热更新服务启动脚本:
const webpack = require("webpack");
const express = require("express");
const path = require("path");
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");
// 实例化 express
const app = require("express")();
// 解析 webpack 配置
const compiler = webpack(webpackConfig);
// 指定静态文件目录,让浏览器从这个目录获取入口 html
app.use(express.static(path.join(__dirname, "../public/dist")));
// 使用 devMiddleware 中间件监听文件变化并触发重新编译
app.use(
devMiddleware(compiler, {
// 指定静态文件目录
publicPath: webpackConfig.output.publicPath,
// 将入口html文件(tpl)落盘,输出到文件夹中,其他文件在内存中
writeToDisk: (filePath) => filePath.endsWith(".tpl"),
// 允许跨域,让浏览器可以跨域访问热更新服务
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With, content-type, Authorization",
},
})
);
// 使用 hotMiddleware 热更新中间件,通知浏览器刷新
app.use(
hotMiddleware(compiler, {
path: DEV_SERVER_CONFIG.HMR_PATH,
log: () => {}, // 不显示日志
})
);
// 提示等待 webpack 打包完成
console.log("请等待webpack初次构建完成提示...");
// 启动热更新服务
app.listen(DEV_SERVER_CONFIG.PORT, () => {
console.log(
`Server is running on http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}`
);
});
全文完,感谢观看。