Elpis 项目构建工具与前端基建实践总结

一、项目背景

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 必须先加载(其他模块依赖配置),extendservice 之前注入工具方法,servicecontroller 之前就位,router-schema 在路由注册前完成参数校验规则的定义。

每个 Loader 扫描 app/ 下对应目录,将文件按 目录路径到命名空间 的映射规则挂载到 app 对象上:

bash 复制代码
app/controller/auth/login.js → app.controller.auth.login

文件名中的 kebab-case 会自动转换为 camelCase(如 auth-login.jsauthLogin),底层通过 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:

  • vendorsnode_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() 热替换

其中三个请求各有分工:

  1. .hot-update.json:更新清单,列出哪些 chunk 发生了变化
  2. entry.page1.xxx.hot-update.js:包含变更后的业务模块代码
  3. 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 解析器,同时声明 Vueaxios 等为全局变量(因为通过 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 提交前自动卡口

这套基建的核心设计哲学是 "约定优于配置"------通过目录结构和文件命名约定,让框架和构建工具自动完成收集、注册和构建工作,开发者只需专注于业务代码本身。这种模式特别适合中小团队在保持灵活性的同时,快速建立起规范化的开发流程。

相关推荐
FungLeo3 小时前
成为全栈·Node 后端篇·后端测试策略:单元、集成与测试数据库
单元测试·node.js·集成测试·测试策略·成为全栈·测试数据库
FungLeo4 小时前
成为全栈·Node 后端篇·评论内容安全:敏感词过滤、三态审核与级联删除
node.js·敏感词过滤·成为全栈·评论内容安全·评论审核·级联删除
FungLeo14 小时前
成为全栈·Node 后端篇·阅读量防刷:去重、冷却与计数写分离
node.js·读写分离·数据去重·接口防刷·成为全栈·数据冷却
脉动数据行情11 天前
Node.js WebSocket 实现贵金属实时行情监听 伦敦金 / 伦敦银自动重连方案
websocket·node.js·vim
不老刘1 天前
一行命令解决 Node.js 版本兼容问题:`--openssl-legacy-provider` 深度解析
node.js
万敏2 天前
Vue3 全栈实战:第一阶段复盘(第1-8周)
vue.js·node.js·全栈
濮水大叔2 天前
舒服了,CabloyJS 的 AI Spec 驱动开发会自动生成甘特图和燃尽图
typescript·node.js·vibecoding
FungLeo2 天前
成为全栈·Node 后端篇·部署上线:从本地起服到真正对外服务
node.js·后端部署·成为全栈
妙码生花2 天前
golang 应用服务端部署(使用 systemd 服务)
开发语言·人工智能·后端·golang·node.js·php·gin