项目总结:踩过的坑、经验、可复用模式
14 篇博客的最后一篇。不做新东西了,坐下来复盘------这个项目到底踩了哪些坑、沉淀了什么可以带走的经验、有哪些模式能在下个项目直接复用。
一、先看数据
把整个项目的数字摆出来,不是为了凑字数,而是让复盘有一个量化的锚点:
| 维度 | 数据 | 备注 |
|---|---|---|
| 开发周期 | 1 周 | 工作日每天 6~8h,周末每天 4~6h |
| 代码量 | ~8,000 行 | 含 Vue 组件 + JS 逻辑 + SCSS + Mock 数据 |
| 文件数 | ~50 个 | .vue 22 个,.js 18 个,.scss 6 个,其余配置/脚本 |
| 通用组件 | 11 个 | AppTable / AppDialog / AppPagination / StatusTag 等 |
| 业务页面 | 12 个 | 覆盖登录 + API 管理 + 模型汇聚 + 模型发布四大模块 |
| Figma Frame | 17 个 | API 管理 8 帧 / 模型汇聚 6 帧 / 模型发布 3 帧 / 登录 1 帧 |
| Mock 数据 | 105 条 | API 注册 30 条 + 模型汇聚 40 条 + 模型发布 35 条 |
| 博客产出 | 14 篇 | 从项目背景、技术选型、设计系统、业务实现到性能优化、像素验收 |
一个人、一周时间,从零到可演示原型 + 14 篇博客。规模不算大,但密度高------几乎每天都要做完一个完整模块、写完对应的博客。
二、踩过的 10 个坑
每个坑都花了至少半小时甚至半天去排查。写下来既是对自己的提醒,也希望读者能绕过。
坑 1:Element Plus 中文 locale 默认是英文
el-pagination 的分页文字、el-table 的空数据提示、el-date-picker 的日历文字------全部是英文。查了半小时文档才发现需要手动引入中文包:
js
// main.js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, { locale: zhCn })
教训:Element Plus 虽然是国内团队开发,但默认 locale 是英文(国际化考虑),每个新项目第一件事就是挂 locale。
坑 2:SCSS 全局变量需要在 Vite 预配置
想把 tokens.css 里定义的颜色变量在 .vue 组件的 <style lang="scss"> 中复用,直接 @import 报错。原因是 SCSS 的全局变量需要 Vite 在编译层注入:
js
// vite.config.js
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
}
})
注意:用 @use 代替 @import(Sass 新语法),且 as * 让变量暴露到全局命名空间,否则每个文件都得写 variables.$color-primary。
教训 :CSS 变量(:root 声明)可以直接用,SCSS 变量需要编译层注入。两者在工程中承担的职责不同------CSS 变量负责运行时主题切换,SCSS 变量负责编译时一致性。
坑 3:路径别名需要 Vite + jsconfig 双重配置
Vite 里配了 @ → src 别名,编辑器还是报红波浪线。因为编辑器(VS Code)只看 jsconfig.json 或 tsconfig.json,不看 vite.config.js。
json
// jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
教训 :路径别名是一个三方协同的事------Vite 负责编译、JS/TS config 负责编辑器提示、ESLint 需要额外插件(eslint-import-resolver-alias)。缺一个环节就报错。
坑 4:Figma 嵌套节点导出 SVG 的层级问题
Figma 里图标可能是一个 FRAME 包着一个 VECTOR,导出时用 mcp__figma__download_figma_images 可能只导出外层 FRAME 而丢失内部路径。解决方法是递归遍历节点,找到最底层的 VECTOR / COMPONENT 才能正确导出。
教训:Figma 的节点树比你想象中深。设计稿里看起来是"一个图标",在节点树里可能是 FRAME → GROUP → COMPONENT → VECTOR 的三四层嵌套。导出时要穿透到叶子节点。
坑 5:mockjs 在生产环境要自动失效
Mock 数据是用 Mock.mock() 全局注册的,如果不加环境判断,打出来的生产包也会拦截 axios 请求。解决方案是用 import.meta.env.DEV 包裹:
js
// mock/index.js
if (import.meta.env.DEV) {
import('./api-registry.mock.js')
import('./model-hub.mock.js')
import('./model-publish.mock.js')
}
import.meta.env.DEV 是 Vite 在构建时替换的常量,生产构建中为 false,Tree Shaking 会直接删掉整个 if 块。
教训 :Mock 数据只属于开发环境。Vite 的 import.meta.env 常量 + 动态 import 是最干净的方案。
坑 6:KeepAlive + 动态路由的匹配失效
想用 <keep-alive> 缓存模型详情的 3 个 Tab,切换回来时保留 Tab 状态。结果切回来还是重新渲染了。
html
<!-- ❌ 这样不生效 -->
<keep-alive>
<router-view />
</keep-alive>
原因:keep-alive 按组件 name 缓存,但动态路由 /model-hub/detail/:id 里不同 id 对应的是同一个组件。需要特殊配置:
html
<keep-alive :include="['ModelHubList', 'ModelDetail']">
<router-view :key="$route.fullPath" />
</keep-alive>
教训:keep-alive 的缓存粒度是"组件名 + key",不是"路由路径"。不同参数的同名组件会被复用,需要显式设置 key。
坑 7:大列表渲染卡顿
模型汇聚列表一共 40 条数据,每条含 6 个字段 + 1 个头像 + 状态标签。在 Chrome 里渲染时间正常,但在演示用的低配笔记本上,列表滚动有肉眼可见的掉帧。
排查后发现是 el-table 的列太多(每列都计算宽度 + 排序箭头渲染),且没有分页。
修复:
- 加上 AppPagination,每页 10 条(不再一次性渲染 40 行)
- 对于确实需要大列表的场景,后续可用
el-table-v2虚拟滚动
教训:"数据不多"不代表"渲染不卡"。表格渲染的瓶颈在 DOM 节点数,40 行 × 8 列 = 320 个表格单元格,每个单元格内有文字 + 样式 + 事件绑定,低配设备上会卡。分页是最简单的解决方案。
坑 8:复选框跨页保持选择
API 注册列表有全选/单选功能,切换分页后之前选中的行丢了。因为 el-table 的 selection 只记住当前页。
修复方案:在 Pinia 中维护一个 selectedIds 数组,翻页时手动同步:
js
// 翻页时
const handlePageChange = () => {
nextTick(() => {
tableRef.value?.toggleRowSelection(row, selectedIds.value.includes(row.id))
})
}
教训 :Element Plus 的 el-table 的 selection 是"当前页级"的,跨页选择必须在状态管理层(Pinia / Vuex)自己维护。
坑 9:Tab 切换 URL 同步的时机问题
模型详情的 3 个 Tab,设计上是 ?tab=interface / ?tab=files / ?tab=script 同步到 URL。但直接 router.replace({ query: { tab } }) 会在 Tab 切换时触发页面重新渲染------因为 router-view 检测到了路由变化。
解决:Tab 切换只改 query,不触发组件销毁重建。用 router.replace 且确保组件内的状态不受 query 变化的副作用影响。
js
// ✅ Tab 切换时:改 query,但不触发组件 rebuild
const handleTabChange = (tab) => {
router.replace({ query: { tab } }) // 不改变 path,不会重新创建组件
}
并且组件内 watch query 变化来驱动 Tab 切换,而不是反过来。
教训:URL 同步是"双刃剑"------它提升了可分享性,但也可能引入意外的路由变化副作用。原则是:手动操作驱动 URL 变化,URL 变化驱动 UI 状态,但要防止"URL 变化 → 组件重建 → 状态丢失"的死循环。
坑 10:多步骤表单的步骤校验时机
模型新增弹窗是 3 步骤表单(基本信息 → 接口配置 → 文件上传)。用户在第 2 步时回头改了第 1 步的内容,第 3 步的"提交"按钮没有感知到变更。
修复:每个步骤的表单校验结果存入一个 reactive 对象,提交时统一校验 3 个步骤:
js
const stepValidation = reactive({
step1: false,
step2: false,
step3: false,
})
const canSubmit = computed(() => {
return stepValidation.step1 && stepValidation.step2 && stepValidation.step3
})
教训:多步骤表单的校验逻辑不在"当前步骤的表单内",而在"跨步骤的验证状态管理器"中。每个步骤只负责更新自己的状态位,提交流程汇总所有状态。
三、5 个可复用的工程模式
坑踩完了,说收获。下面是 5 个我在这个项目中验证过、可以直接复用到下一个项目的模式。
模式 1:设计 Token + CSS 变量双层架构
不要直接在组件里写 background-color: #C8E1FF,也不要只定义 CSS 变量就完事。用双层架构:
Layer 1: tokens.css ── 纯设计 token,如 --color-bg-page: #C8E1FF
Layer 2: element-override.scss ── 把 token 映射到 Element Plus 变量
好处:改主题色只需要改一个 CSS 变量文件;组件库不从 token 层直接取值,而是通过覆写层中转,保证一致性。
模式 2:AppTable 通用表格 + 业务配置分离
11 个通用组件中最值钱的就是 AppTable。它的设计思想是:把 Element Plus 的 el-table 的"通用能力"和"业务列配置"分离开。
html
<AppTable
:data="list"
:columns="columns"
:loading="loading"
:showSelection="true"
@selection-change="handleSelectionChange"
>
<template #actions="{ row }">
<el-button @click="handleEdit(row)">编辑</el-button>
</template>
</AppTable>
columns 是一个纯配置数组(不含 JSX、不含 v-if),由业务页面声明;AppTable 负责渲染、排序、分页、复选框联动。新页面只需配置 columns 就能得到一个完整表格。
模式 3:mockjs + axios 拦截器 + 环境开关
Mock 数据策略的三件套:
- mockjs 生成差异化数据(
@ctitle/@cname/@datetime/@pick) - axios 拦截器做请求路径匹配,模拟接口响应
- 环境开关 (
import.meta.env.DEV)让 Mock 只在 dev 生效
把这个模式抽成一个 createMock(moduleName, config) 工厂函数,新模块接入只需一行:
js
// mock/api-registry.mock.js
export default createMock('api-registry', {
basePath: '/api/registry',
listFields: ['name', 'protocol', 'url', 'method', 'status'],
listCount: 30
})
模式 4:路由 + 布局嵌套 + meta 驱动的菜单
后台系统的路由和菜单天然是一一对应的。不要让菜单组件硬编码路由路径,而是用 route.meta 驱动:
js
{
path: '/model-hub',
component: MainLayout,
meta: { title: '模型汇聚', icon: 'hub', module: 'model-hub' },
children: [
{ path: '', component: ModelHubList, meta: { title: '模型列表' } },
{ path: 'detail/:id', component: ModelDetail, meta: { title: '模型详情', hidden: true } }
]
}
侧栏组件遍历路由树自动生成菜单,meta.hidden 的页面不出现在菜单中(如详情页)。新增一个模块 = 新增一组路由配置,菜单自动跟上。
模式 5:Playwright 像素对比验收流水线
第 13 篇讲过的 Playwright + pixelmatch 方案,这里再做一次提炼------它是一个独立于前端框架的通用验收模式:
Figma 导出截图 → Playwright 自动化截图 → pixelmatch 像素对比 → 差异报告
3 个脚本文件(screenshot-all.js / compare-all.js / diff-report.js),换一组页面 URL 就能复用。不需要依赖任何前端框架,纯 Node.js 工具链。
四、性能优化数据回顾
第 12 篇的优化结果在这里汇总,作为项目复盘的"成绩单":
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏时间 (FCP) | 3.2s | 0.9s | -72% |
| JS 体积 (gzip) | 1.8MB | 580KB | -68% |
| CSS 体积 (gzip) | 320KB | 48KB | -85% |
| Lighthouse 评分 | 65 | 92 | +27 |
核心优化手段:路由懒加载(砍 73% 首屏 JS)+ Element Plus 按需引入(省 84% CSS)+ vendor chunk 手动拆分 + KeepAlive 缓存 + Mock 生产剔除。
这个成绩对于"原型"来说已经超标。如果是正式生产项目,还可以再往下挖------HTTP/2 Server Push、CDN 边缘缓存、Service Worker 离线化,但那些属于另一个话题了。
五、三条最重要的经验沉淀
14 篇博客写下来,三条认知反复出现,我把它们提炼出来:
5.1 前端工程师 = 设计稿的"翻译官"
不是抄图。Figma 给你的是一个"视觉答案",你的工作是把它翻译成工程语言------CSS 变量、组件 props、路由配置、状态管理。翻译质量的衡量标准不是"颜色一样",而是"改一个设计变量时只有一处改动,改完后全部页面同步生效"。
5.2 Mock 数据是原型的灵魂
交互逻辑对不对,代码写得再好也看不出来------得灌入数据。而灌入数据的真实度决定了原型的说服力。用 mockjs 生成 105 条有差异性的数据,字段名符合业务逻辑、时间用最近日期、状态分布合理,演示时客户不会问"这些是假数据吗"。
5.3 通用组件是业务提速器
AppTable、AppDialog、AppPagination 这三个组件让后三个业务模块的开发从"搭 UI"变成了"配 JSON"。06 篇布局写完后的模块实现,每个模块的核心开发时间不超过 4 小时------因为 80% 的 UI 已经被通用组件覆盖了。
六、这个项目的"可复用性"
"做完就扔"的项目不值得写 14 篇博客。这个项目的真正价值在于------它是一个可复用的后台原型模板。
模板的技术要素:
- Vue 3 + Vite 5 + Element Plus 2.4 工程脚手架
- 设计 Token → CSS 变量 → Element Plus 覆写 的完整链路
- 11 个通用组件(AppTable / AppDialog / AppPagination / StatusTag 等)
- mockjs + axios 拦截器的 Mock 数据方案
- 路由懒加载 + 按需引入 + vendor chunk 拆分的性能配方
- Playwright + pixelmatch 的像素对比验收工具链
换一个 Figma 设计稿能复用多少?
| 层 | 可复用度 | 说明 |
|---|---|---|
| 工程脚手架 | 100% | 直接复制,改项目名 |
| 设计 Token 体系 | 80% | 颜色/字体需要重新提取,架构保持不变 |
| 通用组件 | 70% | AppTable/AppDialog/AppPagination 直接复用,StatusTag 等需要调整 |
| Mock 数据方案 | 90% | createMock 工厂函数通用,数据模板需要按新业务重写 |
| 性能优化 | 100% | 懒加载/按需引入/KeepAlive/vendor chunk 与业务无关 |
| 像素验收工具 | 95% | 只改页面 URL 列表 |
结论:换一个 Figma 文件、换一组业务模块,约 60% 的代码可以直接复用,另外 30% 做配置调整,只有 10% 是完全新的业务代码。
七、后续可以做什么
这个项目做到现在,"演示原型"的目标已经完成。如果要继续推进,有 5 个方向:
| 方向 | 内容 | 优先级 |
|---|---|---|
| TypeScript 迁移 | 当前是 JS,对大型项目维护性不足 | ⭐⭐⭐ |
| 单元测试 | vitest 覆盖通用组件(AppTable / AppDialog) | ⭐⭐⭐ |
| E2E 测试 | Playwright 不只会截图,还能做交互测试 | ⭐⭐ |
| 替换 Mock 接真实后端 | 对接 Python / Java 后端 API,去掉 mockjs | ⭐⭐ |
| 移动端适配 | 当前只适配 1920×1080,考虑响应式 | ⭐ |
其中 TypeScript + 单元测试是最值得做的两个,因为这两项直接提升代码的可维护性,且不依赖后端就绪。
八、写给跟读了全系列的读者
如果你从第 01 篇一路读到这里------谢谢。
14 篇博客、50 个文件、8000 行代码、1 周时间。这不是一个"技术炫技"的项目,而是一个"把工程规范落到实战"的项目。我想通过这个系列表达的核心观点只有一句:
企业级开发不是会用什么技术,而是能把一组技术组织成一条可维护的工程链路。
设计稿 → 设计 Token → CSS 变量 → 工程脚手架 → 通用组件 → 业务模块 → Mock 数据 → 性能优化 → 像素验收 → 复盘总结------这条链路中的每一步,分开看都不难,难的是把它们串起来,让每一步的输出正好是下一步的输入。
如果你读完这 14 篇,能带走其中一两个模式用到自己的项目里,这个系列就值了。
下一篇:系列完结,感谢阅读。
全系列回顾:
- 01 - 项目背景:为什么我想用 Vue3 还原一个企业级后台
- 02 - 技术选型:Vue3 vs React,为什么选 Element Plus
- 03 - Figma 数据提取:用 MCP 工具读懂设计稿
- 04 - 设计系统:从设计稿到 CSS 变量
- 05 - 工程脚手架:5 分钟搭出标准项目结构
- 06 - 主布局:顶栏 + 侧栏的工程化设计
- 07 - 通用组件:分页器、表格、对话框的封装
- 08 - 业务实现 1:API 注册管理模块
- 09 - 业务实现 2:模型汇聚模块(3 Tab 详情)
- 10 - 业务实现 3:模型标准发布模块
- 11 - Mock 数据策略:让原型看起来真实
- 12 - 性能优化:路由懒加载、组件按需引入
- 13 - 像素级验收:如何对比 Figma 截图与开发成品
- 14 - 项目总结:踩过的坑、经验、可复用模式(本文)