前言:什么是工程化
工程化是指用软件工程的方法来管理和优化开发流程,让代码从"能跑"变成"好维护、好协作、好交付",举例使用 Webpack5 进行工程化链路搭建(工具不是关键,工程化链路搭建思想才是核心)
工程化带来的好处
- 开发效率 — 热更新、代码提示、自动补全
- 代码质量 — 规范约束、静态检查、单元测试
- 团队协作 — 统一规范、模块化、版本管理
- 性能优化 — 代码压缩、按需加载、缓存策略
- 可维护性 — 清晰的目录结构、组件化、文档化
一、环境概览对比
二、开发环境详解
2.1 执行链路
┌─────────────────────────────────────────────────────────────┐
│ 开发环境启动流程 │
└─────────────────────────────────────────────────────────────┘
1. Node 启动 dev.js
│
├──► 加载 webpack.dev.js 配置
│ │
│ ├──► 合并 webpack.base.js(基础配置)
│ │
│ ├──► 设置 mode: 'development'
│ │
│ ├──► 为每个入口注入 HMR 客户端代码
│ │ │
│ │ └──► entry.page1: [
│ │ './page1/entry.page1.js',
│ │ 'webpack-hot-middleware/client?path=...'
│ │ ]
│ │
│ └──► 添加 HotModuleReplacementPlugin
│
├──► 创建 Webpack Compiler 实例
│
├──► 启动 Express 服务器
│ │
│ ├──► 挂载 webpack-dev-middleware
│ │ │
│ │ ├──► 编译代码存入内存文件系统
│ │ ├──► 监听文件变化自动重编译
│ │ └──► 提供静态资源访问服务
│ │
│ ├──► 挂载 webpack-hot-middleware
│ │ │
│ │ └──► 创建 EventSource 连接 /__webpack_hmr
│ │
│ └──► 监听端口
│
└──► 输出:服务启动成功,监听端口
2.2 核心配置解析
//webpack.dev.js
const webpackConfig = merge.smart(baseConfig, {
mode: 'development', // 开发模式:不压缩,启用 sourcemap
output: {
// 输出到内存,物理路径仅作参考
path: path.resolve(process.cwd(), './app/public/dist/dev'),
// 带 contenthash 用于缓存
filename: 'js/[name]_[contenthash:8].bundle.js',
// 浏览器访问路径(完整 URL)
publicPath: 'http://127.0.0.1:xxxx/public/dist/dev/',
},
plugins: [
// 热更新插件:生成 .hot-update.json/js 补丁文件
new webpack.HotModuleReplacementPlugin({
multiStep: false, // 单步更新,更可靠
}),
],
})
//dev.js(Express 服务器)
const app = express()
// 1. 静态资源目录(图片、字体等)
app.use(express.static(path.resolve(process.cwd(), './app/public')))
// 2. Webpack 开发中间件
app.use(devMiddleware(compiler, {
// 关键:只有 .tpl 模板文件写入磁盘
// 原因:后端 Koa 需要从磁盘读取模板渲染页面
writeToDisk: (filePath) => filePath.endsWith('.tpl'),
// 资源访问路径(必须和 webpack 配置一致)
publicPath: webpackConfig.output.publicPath,
// CORS 配置:允许后端服务(8080)访问前端资源(9002)
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type,Authorization',
},
}))
// 3. 热更新中间件
app.use(hotMiddleware(compiler, {
path: '/__webpack_hmr', // EventSource 路径
log: () => {}, // 关闭冗余日志
}))
app.listen(9002)
2.3 热更新(HMR)完整机制
┌─────────────────────────────────────────────────────────────┐
│ 热更新流程图 │
└─────────────────────────────────────────────────────────────┘
开发者保存代码
│
▼
Webpack 监听文件变化(watch)
│
├──► 重新编译变更模块
│
├──► HotModuleReplacementPlugin 生成热更新补丁
│ ├──► 更新清单:xxx.hot-update.json
│ └──► 更新代码:xxx.hot-update.js
│
▼
webpack-dev-middleware 更新内存中的文件
│
▼
webpack-hot-middleware 检测编译完成
│
▼
通过 EventSource 推送事件到浏览器
│
▼
浏览器 HMR Runtime 接收事件
│
├──► 下载补丁文件(hot-update.json/js)
│
├──► 执行模块替换(module.hot.accept)
│
└──► 更新成功:无刷新更新 / 失败:页面刷新
----------------------------------------------------------------------------------------------
┌─────────────────────────────────────────────────────────────┐
│ 开发环境架构图 │
└─────────────────────────────────────────────────────────────┘
浏览器
│
├──► 请求页面 ───────────────┐
│ ▼
│ ┌───────────────┐
│ │ Koa 后端服务 │
│ │ │
│ │ │
│ │ 渲染 .tpl 模板 │
│ │ 注入初始数据 │
│ └───────┬───────┘
│ │
│◄───────────────────────────┘
│ 返回 HTML(含 JS 引用)
│
├──► 请求 JS/CSS 资源 ─────────┐
│ ▼
│ ┌───────────────┐
│ │ Webpack Dev │
│ │ Server │
│ │ :xxxx │
│ │ │
│ │ 内存文件系统 │
│ │ 热更新推送 │
│ └───────────────┘
│
▼
建立 EventSource 连接
接收热更新事件
三、生产环境详解
3.1执行链路
┌─────────────────────────────────────────────────────────────┐
│ 生产环境构建流程 │
└─────────────────────────────────────────────────────────────┘
1. Node 启动 prod.js
│
├──► 加载 webpack.prod.js 配置
│ │
│ ├──► 合并 webpack.base.js
│ │
│ ├──► 设置 mode: 'production'
│ │
│ ├──► 配置多线程构建(thread-loader)
│ │
│ ├──► 配置 CSS 提取(MiniCssExtractPlugin)
│ │
│ ├──► 配置代码压缩(TerserPlugin)
│ │
│ └──► 配置目录清理(CleanWebpackPlugin)
│
├──► 创建 Webpack Compiler
│
├──► 执行 compiler.run()(单次构建)
│ │
│ ├──► 清空输出目录
│ │
│ ├──► 多线程编译 JS/CSS
│ │
│ ├──► 代码分割(vendor/common/runtime)
│ │
│ ├──► 提取 CSS 到独立文件
│ │
│ ├──► Terser 压缩 JS(删除 console)
│ │
│ ├──► CssMinimizer 压缩 CSS
│ │
│ └──► 生成 HTML 模板(注入资源链接)
│
└──► 输出构建统计信息
│
▼
产物输出到 app/public/dist/prod/
3.2 核心配置解析
//webpack.prod.js
const webpackConfig = merge.smart(baseConfig, {
mode: 'production', // 生产模式:自动优化、压缩
output: {
// 物理输出目录
path: path.join(process.cwd(), 'app/public/dist/prod'),
filename: 'js/[name]_[contenthash:8].bundle.js',
// 相对路径(生产环境同源部署)
publicPath: '/dist/prod/',
crossOriginLoading: 'anonymous', // 跨域加载不发送 cookie
},
module: {
rules: [
// CSS 处理:提取到独立文件(非内联)
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 提取 CSS
{
loader: 'thread-loader', // 多线程
options: {
workers: os.cpus().length - 1, // CPU 核心数-1
},
},
'css-loader',
],
},
// JS 处理:多线程 babel 转译
{
test: /\.js$/,
use: [
'thread-loader',
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime'],
},
},
],
},
],
},
plugins: [
// 1. 清空输出目录
new CleanWebpackPlugin(['public/dist'], {
root: path.resolve(process.cwd(), './app/'),
verbose: true,
}),
// 2. 提取 CSS 到独立文件
new MiniCssExtractPlugin({
filename: 'css/[name]_[contenthash:8].bundle.css',
}),
// 3. 注入 crossorigin 属性(用于 CDN 场景)
new HtmlWebpackInjectAttributesPlugin({
crossorigin: 'anonymous',
}),
],
optimization: {
minimize: true,
minimizer: [
// JS 压缩:并发 + 缓存
new TerserPlugin({
cache: true, // 启用缓存
parallel: true, // 并发压缩
terserOptions: {
compress: {
drop_console: true, // 删除 console.log
},
},
}),
// CSS 压缩
new CssMinimizerPlugin(),
],
},
})
3.3 生产环境构建产物
app/public/dist/prod/
├── js/
│ ├── entry.page1_a1b2c3d4.bundle.js # 页面主代码
│ ├── entry.page2_e5f6g7h8.bundle.js
│ ├── vendor_i9j0k1l2.bundle.js # 第三方库(Vue、Element Plus)
│ ├── common_m3n4o5p6.bundle.js # 公共代码
│ └── runtime~entry.page1_q7r8s9t0.bundle.js # Webpack runtime
├── css/
│ ├── entry.page1_u1v2w3x4.bundle.css # 提取的 CSS
│ └── entry.page2_y5z6a7b8.bundle.css
└── dist/
├── entry.page1.tpl # HTML 模板(注入资源链接)
└── entry.page2.tpl
3.4 多线程构建原理
┌─────────────────────────────────────────────────────────────┐
│ thread-loader 多线程构建 │
└─────────────────────────────────────────────────────────────┘
主进程(Webpack)
│
├──► 启动 worker 线程池(CPU 核心数 - 1)
│ │
│ ├──► Worker 1 ──► 处理模块 A、B、C...
│ ├──► Worker 2 ──► 处理模块 D、E、F...
│ ├──► Worker 3 ──► 处理模块 G、H、I...
│ └──► Worker N ──► ...
│
├──► 分发模块到各个 Worker
│
├──► Worker 执行 babel-loader/css-loader 等
│
└──► 收集结果,合并到主构建流程
配置参数:
- workers: os.cpus().length - 1 # 保留一个核心给主进程
- workerParallelJobs: 50 # 每个 worker 最多 50 个任务
- poolTimeout: 2000 # 2 秒空闲后关闭线程
四、完整请求链路对比
4.1 开发环境请求链路
┌─────────────────────────────────────────────────────────────┐
│ 开发环境请求链路 │
└─────────────────────────────────────────────────────────────┘
用户访问 http://localhost:8080/view/page1
│
▼
Nginx / Koa (8080)
│
├──► 匹配路由 /view/:page
│
├──► Controller 渲染模板
│ │
│ ├──► 读取 app/public/dist/entry.page1.tpl
│ │
│ └──► Nunjucks 注入数据
│ ├──► {{name}} = "Elpis"
│ ├──► {{env}} = "local"
│ └──► {{options}} = {...}
│
▼
返回 HTML(含 xxxx 端口的 JS 引用)
│
▼
浏览器解析 HTML
│
├──► 请求 JS/CSS ──► Webpack Dev Server
│ │
│ └──► 从内存读取返回
│
└──► 建立 EventSource ──► 热更新推送
4.2 生产环境请求链路
┌─────────────────────────────────────────────────────────────┐
│ 生产环境请求链路 │
└─────────────────────────────────────────────────────────────┘
用户访问 http://example.com/view/page1
│
▼
Nginx / Koa (80/443)
│
├──► 匹配路由 /view/:page
│
├──► Controller 渲染模板
│ │
│ └──► 读取 app/public/dist/prod/entry.page1.tpl
│
▼
返回 HTML(含相对路径的 JS/CSS 引用)
│
▼
浏览器解析 HTML
│
├──► 请求 JS/CSS ──► Nginx 静态资源
│ │
│ └──► 返回物理文件
│ ├──► /dist/prod/js/xxx.js
│ └──► /dist/prod/css/xxx.css
│
└──► Vue 挂载激活页面
五、总结
- 自动化:自动扫描入口、自动注入 HTML、自动清理
- 模块化:页面级分割、公共代码提取、组件复用
- 标准化:目录规范、命名约定、代码风格
- 性能优先:代码分割、缓存策略、压缩优化
- 可维护:分层架构、路径别名、环境分离