一、项目背景
Elpis 是一个全栈 Web 项目,前端采用 Vue 3 + Element Plus ,后端基于 Koa 2 。为了支撑业务开发,项目从零搭建了一套自定义的轻量级框架 Elpis-Core,并围绕它构建了完整的 Webpack 构建体系、多页应用(MPA)开发流程和工程化基建。
本文将从框架层、构建层、开发体验层三个维度,系统梳理这套基建的设计思路与实现细节。
二、Elpis-Core:轻量级约定式框架
2.1 设计思想
Elpis-Core 借鉴了 Egg.js 的约定式加载理念,通过 Loader 机制 将 app/ 目录下的业务代码自动收集并注入到 Koa 实例上,开发者只需按约定放置文件,无需手动注册。
2.2 Loader 流水线
框架启动时,按严格的依赖顺序依次执行 7 个 Loader:
arduino
config → extend → service → controller → middleware → router-schema → router
这个顺序不可调换------config 必须先加载(其他模块依赖配置),extend 在 service 之前注入工具方法,service 在 controller 之前就位,router-schema 在路由注册前完成参数校验规则的定义。
每个 Loader 扫描 app/ 下对应目录,将文件按 目录路径到命名空间 的映射规则挂载到 app 对象上:
bash
app/controller/auth/login.js → app.controller.auth.login
文件名中的 kebab-case 会自动转换为 camelCase(如 auth-login.js → authLogin),底层通过 lodash.set 构建嵌套对象。
2.3 多环境管理
通过环境变量 _ENV 区分三套环境(local / beta / production),elpis-core/env.js 提供统一的判断接口:
js
// npm run dev → _ENV='local'
// npm run beta → _ENV='beta'
// npm run prod → _ENV='production'
每个环境对应独立的配置文件 config.{env}.js,由 configLoader 在启动时合并加载。
三、Webpack 构建体系
3.1 三层配置架构
Webpack 配置采用经典的 base / dev / prod 三层结构,通过 webpack-merge 合并:
csharp
webpack.base.js ← 公共配置(入口、模块规则、插件、优化策略)
├── webpack.dev.js ← 开发环境叠加(HMR、sourcemap、dev server)
└── webpack.prod.js ← 生产环境叠加(压缩、CSS 提取、多线程打包)
3.2 多页应用自动发现
这是整个构建体系的核心设计。项目采用 glob 动态扫描 实现多页入口的自动发现:
js
// webpack.base.js
const entryList = glob.sync(path.resolve(process.cwd(), './app/pages/**/entry.*.js'));
entryList.forEach(entry => {
const entryName = path.basename(entry, '.js'); // e.g. "entry.page1"
pageEntries[entryName] = entry;
htmlWebpackPluginList.push(
new HtmlWebpackPlugin({
filename: `./app/public/dist/${entryName}.tpl`,
template: './app/view/entry.tpl',
chunks: [entryName] // 每个页面只加载自己的 JS
})
);
});
约定 :在 app/pages/ 下新建一个目录,放入 entry.xxx.js 入口文件和对应的 .vue 组件,Webpack 就会自动识别并构建出一个新页面,无需手动修改任何配置。
每个页面入口通过统一的 boot.js 启动 Vue 应用:
js
// app/pages/page1/entry.page1.js
import boot from "$pages/boot.js";
import Page1 from "./page1.vue";
boot(Page1);
boot.js 负责创建 Vue 实例,并统一挂载 Element Plus、Pinia、路由等插件,屏蔽了重复的初始化逻辑。
3.3 路径别名
为简化模块引用,配置了四个路径别名:
| 别名 | 实际路径 | 用途 |
|---|---|---|
$pages |
app/pages |
页面根目录 |
$common |
app/pages/common |
公共工具模块 |
$widgets |
app/pages/widgets |
公共组件 |
$store |
app/pages/store |
状态管理 |
3.4 代码分割策略
通过 splitChunks 将打包产物分为三类 chunk:
- vendors :
node_modules中的第三方库,单独提取,命中浏览器强缓存 - common:被两处及以上引用的公共模块,自动提取
- runtime :Webpack 运行时代码,通过
runtimeChunk: 'single'隔离到runtime.js
js
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 20, enforce: true },
common: { name: 'common', minChunks: 2, minSize: 1, priority: 10 }
}
},
runtimeChunk: 'single'
}
四、开发环境:双服务器 + HMR 热更新
4.1 双服务器架构
开发环境采用两个独立服务器的设计:
| 服务器 | 端口 | 职责 |
|---|---|---|
| ElpisCore | 8080 | 后端 API + 页面模板渲染 |
| Webpack Dev Server | 9002 | 前端资源构建 + HMR 推送 |
Dev Server 基于 Express 搭建,挂载两个关键中间件:
webpack-dev-middleware:将 Webpack 编译产物保存在内存中,通过中间件直接响应资源请求webpack-hot-middleware:通过 SSE(Server-Sent Events)推送编译事件,驱动浏览器热更新
4.2 HMR 工作流程
当开发者修改并保存文件时,完整的更新链路如下:
arduino
文件变更 → Webpack 重新编译 → Dev Server SSE 推送通知
→ 浏览器 HMR 客户端收到通知 → 请求 .hot-update.json(更新清单)
→ 请求对应的 entry chunk 和 runtime chunk → 执行 module.hot.apply() 热替换
其中三个请求各有分工:
.hot-update.json:更新清单,列出哪些 chunk 发生了变化entry.page1.xxx.hot-update.js:包含变更后的业务模块代码runtime.xxx.hot-update.js:更新后的模块映射表(因为runtimeChunk: 'single'将运行时隔离到独立 chunk,每次编译 hash 都会变化)
4.3 HMR 入口注入
开发环境的每个入口都需要注入 HMR 客户端:
js
// webpack.dev.js
Object.keys(baseConfig.entry).forEach(v => {
if (v !== "vendor") {
baseConfig.entry[v] = [
baseConfig.entry[v], // 原始入口
`webpack-hot-middleware/client?path=http://127.0.0.1:9002/__webpack_hmr?timeout=20000&reload=true`
];
}
});
注意 :访问入口点很关键------如果通过 8080 端口访问页面,HMR 客户端会尝试连接 9002 端口;如果 9002 没有正常运行或存在跨域问题,就会出现 404。开发时应确保
npm run build:dev已启动。
五、生产环境构建优化
5.1 CSS 提取与压缩
生产环境使用 MiniCssExtractPlugin 将 CSS 从 JS 中提取为独立文件,避免 FOUC(Flash of Unstyled Content),配合 CssMinimizerPlugin 进行压缩:
js
new MiniCssExtractPlugin({ chunkFilename: "css/[name]_[contenthash:8].css" })
new CssMinimizerPlugin()
5.2 多线程打包
引入 HappyPack 利用多进程并行处理 JS 和 CSS 编译:
js
new HappyPack({
threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
id: "js",
loaders: ["babel-loader?..."]
})
5.3 JS 压缩与清理
使用 TerserPlugin 进行代码压缩,启用缓存和多进程,并自动移除 console.log:
js
new TerserPlugin({
cache: true,
parallel: true,
terserOptions: { compress: { drop_console: true } }
})
5.4 构建前清理
CleanWebpackPlugin 在每次生产构建前自动清空 public/dist 目录,避免残留旧产物。
5.5 跨域资源加载
生产环境配置 crossOriginLoading: 'anonymous',配合 HtmlWebpackInjectAttributesPlugin 为 <script> 标签自动添加 crossorigin="anonymous" 属性,支持 CDN 场景下的跨域资源加载和错误捕获。
六、前端基建:页面启动流程
6.1 服务端渲染链路
bash
用户访问 /view/page1
→ Koa 路由匹配(router/view.js)
→ ViewController.renderPage()
→ 渲染模板 dist/entry.page1.tpl(Nunjucks)
→ 返回 HTML(包含对应 chunk 的 JS 引用)
→ 浏览器加载 JS → boot.js 启动 Vue → 挂载到 #root
6.2 前端统一初始化
boot.js 是所有页面的统一启动入口,它封装了 Vue 应用创建的全套流程:
js
export default (component, { routes, libs } = {}) => {
const app = createApp(component);
app.use(ElementPlus); // UI 库
app.use(pinia); // 状态管理
// 可选:额外插件
if (libs?.length) libs.forEach(lib => app.use(lib));
// 可选:路由
if (routes?.length) { /* 创建路由并等待 ready */ }
app.mount("#root");
};
6.3 请求封装
common/curl.js 封装了基于 Axios 的统一请求方法,内置:
- API 签名 :自动附加
s_t(时间戳)和s_sign(MD5 签名)请求头 - 错误分类处理 :根据后端返回的
code展示不同的 Element Plus 提示 - 超时兜底:统一返回结构化错误对象
七、工程化规范
7.1 ESLint 代码检查
配置了 eslint-plugin-vue 规则集,使用 babel-eslint 解析器,同时声明 Vue、axios 等为全局变量(因为通过 ProvidePlugin 注入)。
7.2 Git Hooks
通过 ghooks 在 Git 提交前自动执行检查:
json
"ghooks": {
"pre-commit": "npm run lint",
"commit-msg": "validate-commit-msg"
}
- pre-commit:运行 ESLint,阻止不合规代码进入仓库
- commit-msg:校验提交信息格式,保证 Git 历史可读性
八、总结与思考
| 维度 | 方案 | 亮点 |
|---|---|---|
| 框架层 | Elpis-Core Loader 机制 | 约定优于配置,零手动注册 |
| 构建层 | Webpack 5 三层配置 | base/dev/prod 职责清晰 |
| 多页应用 | glob 自动发现 + 统一 boot | 新增页面零配置 |
| 开发体验 | Express + HMR 双服务器 | 内存编译,热更新 |
| 生产优化 | HappyPack + TerserPlugin + CSS 提取 | 多线程并行,产物精简 |
| 工程化 | ESLint + ghooks + validate-commit-msg | 提交前自动卡口 |
这套基建的核心设计哲学是 "约定优于配置"------通过目录结构和文件命名约定,让框架和构建工具自动完成收集、注册和构建工作,开发者只需专注于业务代码本身。这种模式特别适合中小团队在保持灵活性的同时,快速建立起规范化的开发流程。