对前端工程化的理解
前端工程化不是某一个工具,而是 “规范 + 工具链 + 自动化流程” 的组合 。它的本质是让前端开发从 “单兵作战” 的作坊模式,转向 “团队协作” 的工业化模式,适配现代前端项目的复杂度和规模 **。
工程化的核心目标
- 提升开发效率
- 保障代码质量
- 优化项目性能
- 方便团队协助
在里程碑二中主要灌输一种工程化的思想和设计模型。在里程碑一中我们把业务文件通过解析器,也就是我们的服务引擎elpis-core,通过各种loader解析出我们的目标文件。在我们的前端工程化亦是如此,也是把业务文件通过解析引擎产出我们的目标文件。
前端工程化的解析引擎需要处理什么?
前端工程化的解析引擎主要做这三件事。
- 解析编译:源码的 “翻译与转换” 这是解析引擎的基础能力,核心是识别不同类型的文件并完成语法 / 格式转换,解决 “浏览器不认识的代码” 问题。 譬如:vue-loader 解析和编译 .Vue 文件
- 模块分包:代码的 “拆分与重组” 这是解析引擎处理大型项目的核心能力,目的是优化加载性能,实现按需加载,避免单文件体积过大。 譬如:公共模块common的抽离
- 压缩优化:产物的 “瘦身与提效” 是解析引擎的收尾优化环节,核心是减小产物体积,提升运行性能,面向最终上线的产物。 譬如:移除 console
前端工程化的解析引擎课程使用的是Webpack,当然你也可以用Vite等工具,但主要的思想还是没有变。还是那句话工具不是最重要的,只要适合你的项目就行,最重要的是工程化的思想和设计模型。
在webpack中,理论上,我们只需要配置这三个核心选项,webpack就能完成最基础的打包工作。
- entry:打包入口
- module:模块解析配置
- output:产物输出路径
“读入源码 → 转换处理 → 输出产物”。在这个流程骨架上,我们为了开发便利,提升开发效率,避免手写冗长路径,新增了resolve 配置模块解析的具体行为。譬如增加路径别名来简化业务代码中的导入路径。
为了实现更复杂的打包,实现自动化、性能优化,新增了 plugins 处理整个打包流程,提取公共部分、优化并压缩资源、生成HEML文件并自动注入资源、每次打包前自动清理输出目录、注入环境变量等。
为了解决打包的产物体积大、加载慢的问题,新增了 optimization 优化打包产物的性能。譬如代码分割,抽离第三方依赖库、抽离公共的js模块、使用多线程加速打包等
在webpack中拓展性最高的就是 plugins 模块,你可以引用其他的第三方库,来构建出你认为项目中最理想的打包流程
多页面开发
背景
因为我们项目是多页面构建,每一个页面都是一个独立的入口,因此我们不可能每创建一个页面就在entry中配置多一个入口和新增一个HtmlWebpackPlugin为每个页面生成最终的产物文件,这就违背了自动化的理念。
目的
所以为了实现页面新增的自动化配置,避免手动修改 Webpack 配置的重复劳动,我们可以基于前端工程化的自动化、规范化理念,制定一套标准方案:所有页面入口文件统一存放于 app/pages 目录下,并严格遵循 entry.xxx.js 的命名规范。
实现方案
通过 path 模块解析文件路径,结合 glob 库的文件匹配能力,自动扫描符合命名规范的入口文件,动态生成 Webpack 所需的 pageEntries 入口配置和 htmlWebpackPluginList 插件列表,最终注入 Webpack 核心配置中,实现新增页面 “零配置接入”。
前端内容基建
背景
上文提到本项目为 多页面Vue项目,每个页面都需要重复进行 createApp、注册插件Element Plus 、Pinia 还有配置路由,挂载实例等操作,导致代码冗余度高,维护成本也高。
因此在基建中就需要把一些通用的启动逻辑抽取封装,形成统一的页面入口函数,实现 一处配置、多处复用。
目的
- 所有页面的启动流程、插件注册方式保持一致,降低团队协作成本
- 抽离公共逻辑,业务页面无需重复编写初始化代码,专注于核心功能开发
- 支持个性化配置(路由模式、自定义插件),适配不同页面的差异化需求
实现方案
封装一个 Vue 页面通用启动函数,统一完成 Vue 应用的初始化工作,具体包含以下核心逻辑:
- 创建 Vue 应用实例,全局注册
Element Plus组件库和Pinia状态管理; - 引入全局自定义样式文件;
- 支持传入第三方插件列表,自动批量注册扩展能力;
- 支持传入路由配置,自动创建并挂载 Hash 模式路由,路由就绪后再挂载应用;无路由时直接挂载应用到
#root节点; - 业务页面只需传入根组件和个性化配置(路由、插件),即可一键启动,无需重复编写初始化代码。
分包策略配置的核心
背景
在开发中,随着业务迭代,项目体积会迅速膨胀。如果不进行合理的分包配置,Webpack 默认会将所有依赖(node_modules)和业务代码打包成一个巨大的 bundle.js 文件。这会导致以下两个核心问题:
1. 首屏加载慢(Load Performance):浏览器必须下载完整个巨大的 JS 文件,并进行解析(Parse)和编译(Compile)后,页面才能渲染。这会导致白屏时间过长,用户体验极差。
2. 缓存利用率低(Caching Efficiency):只要修改了一行代码(例如改了个 Bug),整个巨大的 bundle.js 的 Hash 就会变化,导致用户浏览器缓存完全失效,必须重新下载所有内容。
目的
实施分包策略的核心目标可以归纳为两个词:性能(Performance) 与 缓存(Caching)
- 减小首屏体积,按需加载:
- 将“首屏关键资源”与“非关键资源”分离。
- 将“当前页面的代码”与“其他页面的代码”分离。
- 实现并行加载:浏览器可以同时请求多个较小的文件,利用 HTTP 并发特性缩短加载时间。
- 最大化长效缓存(Long-term Caching):
- 业务与第三方分离:将业务代码(Business)和第三方库(Vendor)隔离。分离后,业务更新不影响 Vendor 的缓存。
- 公共代码抽离:如果多个页面都用到了某个组件,将其抽离成 Common 包,避免重复打包,节省带宽。
实现方案
通过Webpack中的optimization.splitChunks配置项来实现,我们采用三级分包策略:
第三方lib库(vendor):基本不会改动的第三方库,除非是依赖升级,基本不改动模块
公共业务(common):业务组件代码中的公共部分,改动较少的模块
基础(entry.{page}):不同页面 entry 里的业务代码的差异部分,经常改动的模块
// 配置打包输出优化(代码分割,模板合并,缓存,TreeShaing,压缩等优化策略)
optimization: {
splitChunks: {
chunks: "all", // 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
maxInitialRequests: 10, // 入口点的最大并行请求数
cacheGroups: {
vendor: {
// 第三方依赖库
test: /[\\/]node_modules[\\/]/, // 打包 node_module 中的文件
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.js
runtimeChunk: true,
},
热更新的理解
背景
在前端工程化体系中,与 elpis-core 一样,我们需要明确区分生产环境与开发环境,针对不同环境设计差异化的构建策略
- 生产环境:目标是生成可部署的产物包,执行
build命令后,所有文件会落地到磁盘(如dist目录),直接用于线上部署; - 开发环境:若每次修改代码都执行完整
build,会导致开发效率极低,因此需要实现无需手动构建、实时更新的开发体验。
目的
开发环境下,实现代码修改后自动编译 + 热更新,无需手动执行 build 命令,也无需刷新浏览器即可看到最新修改效果。
实现方案
基于 Express 框架搭建 devServer 开发服务器,配合 Webpack 的两个核心中间件完成热更新流程:
webpack-dev-middleware(简称devMiddleware)
- 核心作用:替代传统的
webpack build命令,启动 Webpack 以监听模式编译代码; - 关键特性:编译后的产物不落地磁盘,而是直接存入内存,大幅提升编译速度;当监控到业务文件改动时,自动触发 Webpack 增量编译,更新内存中的产物。
webpack-hot-middleware(简称hotMiddleware)
- 核心作用:基于 WebSocket 协议建立浏览器与
devServer的实时通信; - 工作流程:当
devMiddleware完成内存产物更新后,hotMiddleware会将更新通知发送到浏览器端,触发前端的热模块替换(HMR)逻辑,实现页面无刷新更新(如替换修改的组件、样式)。
整体流程:文件修改 → 监控工具检测变化 → devMiddleware 触发增量编译 → 产物存入内存 → hotMiddleware 通知浏览器 → 前端执行热更新。
const path = require("path");
const merge = require("webpack-merge");
const webpack = require("webpack");
// 基类配置
const baseConfig = require("./webpack.base.js");
// dev-server 配置
const DEV_SERVER_CONFIG = {
HOST: "127.0.0.1",
PORT: 9002,
HMP_PATH: "__webpack_hmr", // 官方规定
TIMEOUT: 20000,
};
// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((v) => {
// 第三方包不作为 hmr 入口
if (v !== "vendor") {
baseConfig.entry[v] = [
// 主入口文件
baseConfig.entry[v],
// hmr 更新入口, 官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMP_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`,
];
}
});
// 开发环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
// 指定开发环境模式
mode: "development",
// source-map 开发工具, 呈现代码的映射关系,便于在开发过程中调试代码
devtool: "eval-cheap-module-source-map",
// 开发阶段的 output 配置
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/`, // 外部资源公共路径
globalObject: "this",
},
// 开发阶段插件
plugins: [
// HotModuleReplacementPlugin 用于实现热模块替换(Hot Module Replacement 简称 HMR)
// 模块热替换允许在应用程序运行时替换模块
// 极大的提升开发效率,因为能让应用程序一直保持运行状态
new webpack.HotModuleReplacementPlugin({
multiStep: false,
}),
],
});
module.exports = {
// webpack 配置
webpackConfig,
// devServer 配置 暴露给 dev.js 使用
DEV_SERVER_CONFIG,
};
// 本地开发启动 devServer
const express = require("express");
const path = require("path");
const consoler = require("consoler");
const webpack = require("webpack");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");
// 从 webpack.dev.js 获取 webpack配置 和 devServer配置
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");
const app = express();
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));
// 引用 devMiddleware 中间件 (监控文件改动)
app.use(
devMiddleware(compiler, {
// 落地文件
writeToDisk: (filePath) => filePath.endsWith(".tpl"),
// 资源路径
publicPath: webpackConfig.output.publicPath,
// headers 配置
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Request-With, content-type, Authorization",
},
stats: {
colors: true, // 打印的时候有颜色
},
})
);
// 引用 hotMiddleware 中间件 (实现热更新通讯)
app.use(
hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMP_PATH}`,
log: () => {},
})
);
consoler.info("请等待webpack初次构建完成提升...");
// 启动 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
console.log(`app listening on port ${port}`);
});