一、什么是前端工程化
前端工程化,前端工程化,就是把传统纯手动的前端开发模式 ,升级为标准化、自动化、模块化、易维护、易扩展的现代工程开发范式。
它早已不只是单纯写页面、写业务逻辑,而是依托工具链、规范体系、构建流程和协作机制 ,把前端开发完整链路:编码、代码校验、项目构建、自动化测试、部署上线、线上监控,打造成一套全自动闭环流程
一、整体架构
整个构建体系分三层组成:配置层、执行层、插件层

webpack.base.js 放所有环境共用的配置,webpack.dev.js 和 webpack.prod.js 各自叠加环境专属的部分,通过 webpack-merge 合并。
这样做的好处是:通用配置只写一份,各个环境都有专属的配置,各管各的。
二、入口(Entry) 找出所有的入口文件 利用 glob 遍历所有入口文件,提取文件名作为 入口名称
Webpack 需要知道从哪些文件开始打包,这就是 Entry。传统做法是手动在配置里写死每个入口,每新增一个页面就要改配置
用 glob 遍历所有目录
javascript
// webpack.base.js
const pageEntries = {};
const htmlWebpackPluginList = [];
glob
.sync(path.resolve(process.cwd(), "./app/pages/**/entry.*.js"))
.forEach((file) => {
const entryName = path.basename(file, ".js");
pageEntries[entryName] = file;
htmlWebpackPluginList.push(
new HtmlWebpackPlugin({
filename: path.resolve(
process.cwd(),
`./app/public/dist/${entryName}.tpl`,
),
template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
chunks: [entryName],
}),
);
});
约定规则:在 app/pages/ 下任意目录,只要文件名符合 entry.{pageName}.js 的格式,就会被自动识别为入口。
每个入口同时会生成一个 .tpl 模板文件。这个模板是给 Koa 服务端用的------用户访问 /view/page1 时,Koa 通过 Nunjucks 渲染 entry.page1.tpl,Webpack 已经把打包后的 JS/CSS 注入到了这个模板里。
javascript
// app/controller/view.js --- Koa 服务端渲染页面
async renderPage(ctx) {
await ctx.render(`dist/entry.${ctx.params.page}`, {
name: app.options?.name,
env: app.env.get(),
});
}
三、模块解析 module
用于解析不同模块下的依赖,把它转成浏览器可识别的语言,例如:
- vue --> vue-loader
- less --> less-loader
- css --> css-loader
- js --> bebal-loader
- png --> file-loader
需要注意 :webpack 进行编译时,需要确保已经下载相关loader,因为 webpack 会自动从当前项目中的 node_modules 里面去找,如果找不到可能会 报错!
javascript
module.exports = {
// 模块解析配置(决定了要加载解析哪些模块,以及用什么方式去解析)
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},
{
test: /\.js$/,
include: [
// 只对业务代码进行bebel,加快 webpack 打包速度
path.resolve(process.cwd(), './app/pages')
],
use: {
loader: 'babel-loader'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300,
esModule: false
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
use: 'file-loader'
}
]
},
四、 模块解析设置别名 resolve
javascript
resolve: {
// 解析路径时,自动添加的扩展名
extensions: ['.js', '.vue', '.less', '.css'],
// 设置别名,方便后续开发
alias: {
$pages: path.resolve(process.cwd(), './app/pages'),
$common: path.resolve(process.cwd(), './app/pages/common'),
$widgets: path.resolve(process.cwd(), './app/pages/widgets'),
$store: path.resolve(process.cwd(), './app/pages/store')
}
},
五、产物输出 output
output 是用来配置在不同环境下,产物输出的名称以及文件存放的路径,但是不同的环境下,所需要的内容又是不同的
开发环境 webpack.dev.js
javscript
const webpackConfig = merge.smart(baseConfig, {
// 开发环境 output 配置
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.resolve(process.cwd(), './app/public/dist/dev/'), // 输出文件存储路径
publicPath: `http://${HOST}:${PORT}/public/dist/dev/`, // 外部资源公共路径
globalObject: 'this' // 全局对象,用于在浏览器中访问 webpack 打包后的代码
}
})
生产环境 webpack.prod.js
javascript
module.exports = {
output: {
// 输出文件名;[name] 文件名称,[chunkhash:8] 哈希值8位
filename: 'js/[name]_[chunkhash:8].bundle.js',
// 产物输出目录
path: path.join(process.cwd(), './app/public/dist/prod'),
// 静态资源路径
publicPath: '/dist/prod',
// 让跨域资源获得正确的跨域权限
crossOriginLoading: 'anonymous'
}
}
filename 用于指定输出文件的命名格式,通常搭配 8 位哈希值,以此区分不同版本的文件。这样可以有效规避浏览器缓存问题,当文件内容未变更时,哈希值保持不变,浏览器会直接复用缓存资源。
path 与 publicPath 的作用会根据环境有所不同:
- 开发环境 :
path是编译产物的本地输出目录;publicPath是静态资源的公共访问路径,供webpack-dev-middleware使用,配合webpack-hot-middleware实现 HMR 热更新。 - 生产环境:两者主要用于定义生产构建产物的输出目录与静态资源的存放路径
六、 Plugins配置
Loader 处理单个文件,Plugin 则作用于整个构建流程。
javascript
plugins: [
// 1. 必须:让 vue-loader 工作
new VueLoaderPlugin(),
// 2. 全局注入:业务代码中不需要 import 就能用 Vue、axios、lodash
new webpack.ProvidePlugin({
Vue: "vue",
axios: "axios",
_: "lodash",
}),
// 3. 定义编译时常量:Vue 3 的特性标志
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
}),
// 4. 每个入口生成对应的 HTML 模板
...htmlWebpackPluginList,
];
开发环境
看自己的需求来配置,但是如果要实现HMR的话,就需要做一下配置
javascript
// 开发环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时替换
// 极大的提升开发效率,应为能在不刷新页面的情况下更新模块
new webpack.HotModuleReplacementPlugin({
multiStep: false // 是否启用 multiStep 模式,默认为 false,启用后会在编译完成后等待一段时间再进行下一轮编译,适用于大型项目
})
]
})
生产环境
javascript
// 每次build 前, 清空 public/dist 目录
const webpackConfig = merge.smart(baseConfig, {
.....
plugins:[
new CleanWebpackPlugin(["public/dist"], {
root: path.resolve(process.cwd(), "./app"),
extends: [],
verbose: true,
dry: false,
}),
// 提取 css 的公共部分,有效利用缓存
new MinCssExtractPlugin({
chunkFilename: "css/[name]_[contenthash:8].bundle.css",
}),
// 优化并压缩 css 资源
new CssMinimizerPlugin(),
// 多线程打包 JS 加快打包速度
new HappyPack({
...happypackCommonConfig,
id: "js",
loaders: [
`babel-loader?${JSON.stringify({
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-runtime"],
})}`,
],
}),
// 多线程打包 CSS 加快打包速度
new HappyPack({
...happypackCommonConfig,
id: "css",
loaders: [
{
path: "css-loader",
options: {
importLoaders: 1,
},
},
],
}),
// 浏览器在请求资源不发送用户的身份凭证
new HtmlWebpackInjectAttributesPlugin(),
]
})
七、打包优化 optimization
webpack.base.js
javascript
// 配置打包输出优化(代码分割, 模块合并, 缓存, TreeShaing, 压缩等优化策略)
optimization: {
/**
* 把 js 文件打包成3种类型
* 1. vendor 第三方 Lib 库,基本不会改动,除非版本依赖升级
* 2. common 业务组件代码的公共部分提取出来,改动较少
* 3. entry.${page} 不同页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的: 把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果
*
*/
splitChunks: {
chunks: "all", // 对同步和异步模块进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
maxInitialRequests: 10, // 入口点并行加载的最大并行请求数
cacheGroups: {
vendor: {
// 第三方依赖库
test: /[\\/]node_modules[\\/]/, // 打包 node_modules 中的文件
name: "vendor", // 模块名称
priority: 20, // 优先级, 数字越大优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true, // 复用已有的公共 chunk
},
common: {
// 公共模块
name: "common",
minChunks: 2, // 被两处引用即被归为公共模块
minSize: 1, // 最小分割文件大小(1 byte)
priority: 10, // 优先级,
reuseExistingChunk: true, // 复用已有的公共 chunk
},
},
},
// 将 webpack 生成的 runtime 代码提取到单独的文件中
runtimeChunk: true,
},
webpack.prod.js
javascript
optimization: {
// 使用 TerserPlugin 的并发和缓存,提升压缩阶段的性能
// 清除console.log
minimizer: true,
minimizer: [
new TerserPlugin({
cache: true, // 启用缓存来加速构建过程
parallel: true, // 利用多核 CPU 的优势来加快压缩速度
terserOptions: {
compress: {
drop_console: true, // 移除所有的 console 语句
},
},
}),
],
},
八、 HMR 热模块替换:修改代码不刷新页面
利用 Express 搭建本地开发 devServer,搭配 webpack-dev-middleware 和 webpack-hot-middleware 中间件 实现热更新HMR
webpack-dev-middleware(编译 + 静态服务)
- 实时编译:监听文件变化,自动重新编译 Webpack 代码
- 内存存储 :把编译后的文件存在内存里,不写入硬盘(速度极快)
- 静态服务:把内存中的编译结果,作为静态资源暴露给浏览器访问
- 基础依赖 :是热更新中间件的前置依赖,没有它就无法运行热更新
只负责 编译 + 提供文件在 内存中访问,不负责热更新。
webpack-hot-middleware(热模块替换 HMR)
- 热更新通信:和浏览器建立 WebSocket 长连接,通知文件变化
- 热模块替换 :不刷新整个页面,只替换修改的代码模块
- 状态保留:修改代码后,页面表单输入、组件状态不会丢失
负责 「热更新」 ,让你改代码后页面无需刷新
webpack.dev.js的配置
开发阶段的 entry 配置 需要加入 hmr
javascript
Object.keys(baseConfig.entry).forEach(v => {
// 第三方包不作为 hmr 入口
if (v !== 'vendor') {
baseConfig.entry[v] = [
// 主入口文件
baseConfig.entry[v],
// hmr 更新入口,官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}&timeout=${TIMEOUT}&reload=true`
]
}
})
同时在配上热更新的插件 就可以在本地热更新了
javascript
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时替换
// 极大的提升开发效率,应为能在不刷新页面的情况下更新模块
new webpack.HotModuleReplacementPlugin({
multiStep: false // 是否启用 multiStep 模式,默认为 false,启用后会在编译完成后等待一段时间再进行下一轮编译,适用于大型项目
})
]
九、 总结
深入学习 Webpack 工程化后,我对前端工程化体系有了更深层次的理解。正如哲哥所说,我们学习的从来不是单纯会用工具,而是要学会思考:打包的本质是什么、该如何设计构建流程、以及整个打包构建过程都需要什么东西!!