☰
从 30 个重复页面到 1 个配置驱动列表页:前端架构演进实录
2026/10/3 7:22:14 网站建设 项目流程

从 30 个重复页面到 1 个配置驱动列表页:前端架构演进实录

本文属于「码动四季·开源同行」秋季征稿 —— 技术经验体系化沉淀赛道。
记录开源项目 AtomOps 前端从"每个模块一个 Vue 页面"到"一份配置生成所有页面"的架构演进。

痛点:CRUD 页面的重复地狱

运维平台有大量 CRUD 模块:主机管理、脚本管理、作业管理、定时任务、告警规则、账号管理、参数中心……每个模块都需要:

  • 列表表格 + 分页 + 搜索
  • 新增对话框 + 表单校验
  • 编辑对话框 + 数据回填
  • 删除确认
  • 特殊字段处理(密码框、下拉选、开关、文本域)

最初的做法是每个模块写一个.vue文件。写到第 5 个的时候发现:80% 的代码是复制粘贴。

<!-- Hosts.vue --> <el-table :data="list"> <el-table-column prop="hostname" label="主机名" /> <el-table-column prop="ip" label="IP" /> ... </el-table> <el-dialog> <el-form> <el-form-item label="主机名"><el-input v-model="form.hostname" /></el-form-item> ... </el-form> </el-dialog> <!-- Scripts.vue —— 几乎一模一样 --> <el-table :data="list"> <el-table-column prop="name" label="名称" /> <el-table-column prop="script_type" label="类型" /> ... </el-table>

问题清单:

  1. 新增模块要复制 200+ 行模板代码
  2. 修改表格样式要改 N 个文件
  3. 交互逻辑(搜索、分页、CRUD)每个文件各写一遍
  4. Bug 修了一个文件,其他文件还在

演进:配置驱动的 ListPage

核心思想

用一个 JSON 配置描述页面结构,用一个通用组件渲染所有页面。

// menu.js —— 一份配置就是一个页面{path:'/coc/scripts',title:'脚本管理',page:'list',// 使用 ListPage 组件endpoint:'/api/coc/scripts',// API 地址columns:[// 列定义{prop:'name',label:'名称',type:'text',required:true},{prop:'script_type',label:'类型',type:'select',options:[['Shell','shell'],['Python','python']]},{prop:'content',label:'内容',type:'textarea'},],actions:[// 行操作按钮{label:'执行',exec:'script'}]}

路由生成

配置驱动路由,不再手写routes:

// router/index.jsconstmoduleRoutes=allMenus.map((m)=>({path:m.path,component:m.page==='list'?()=>import('../views/ListPage.vue')// 所有 list 页面复用:legacyViews[m.component],props:m.page==='list'?{item:m}:{}// 配置作为 props 传入}))

ListPage.vue —— 一个组件统治所有

<template> <!-- 搜索栏 --> <el-input v-model="search" :placeholder="item.searchPlaceholder" @keyup.enter="load" /> <!-- 表格 --> <el-table :data="list"> <el-table-column v-for="c in columns" :prop="c.prop" :label="c.label"> <!-- 根据字段类型渲染不同组件 --> <el-tag v-if="c.type === 'select'">{{ optionLabel(c, row[c.prop]) }}</el-tag> <el-switch v-else-if="c.type === 'switch'" :model-value="row[c.prop]" disabled /> <span v-else>{{ row[c.prop] }}</span> </el-table-column> <!-- 操作列 --> <el-table-column label="操作"> <el-button v-for="a in actions" @click="handleAction(a, row)">{{ a.label }}</el-button> <el-button @click="open(row)">编辑</el-button> <el-button @click="del(row)">删除</el-button> </el-table-column> </el-table> <!-- 新增/编辑对话框 --> <el-dialog v-model="dialog"> <el-form> <el-form-item v-for="f in fields" :label="f.label"> <el-input v-if="f.type === 'text'" v-model="form[f.prop]" /> <el-select v-if="f.type === 'select'" v-model="form[f.prop]"> <el-option v-for="o in f.options" :label="o[0]" :value="o[1]" /> </el-select> <el-input v-if="f.type === 'textarea'" type="textarea" v-model="form[f.prop]" /> <el-switch v-if="f.type === 'switch'" v-model="form[f.prop]" /> </el-form-item> </el-form> </el-dialog> </template>

字段类型扩展

新增字段类型只需在 ListPage 加一个v-if,不需要改任何配置文件:

<!-- 密码字段:编辑时留空不修改 --> <el-input v-if="f.type === 'password'" type="password" :placeholder="f.placeholder || '留空则不修改'" /> <!-- 作业步骤编辑器 --> <div v-if="f.type === 'steps'"> <div v-for="(step, i) in jobSteps"> <el-input v-model="step.name" placeholder="步骤名称" /> <el-select v-model="step.script_id" @focus="loadScripts"> <el-option v-for="s in scriptOptions" :label="s.name" :value="s.id" /> </el-select> </div> </div> <!-- 定时任务目标选择:动态加载脚本/作业 --> <el-select v-if="f.type === 'target_select'" @focus="loadTargetOptions"> <el-option v-for="o in targetOptions" :label="o.label" :value="o.id" /> </el-select> <!-- 主机多选 --> <el-select v-if="f.type === 'host_multi_select'" multiple filterable @focus="loadAllHosts"> <el-option v-for="h in allHosts" :label="`${h.hostname} (${h.ip})`" :value="h.id" /> </el-select>

效果对比

代码量

指标演进前演进后
每个新模块代码~200 行 Vue~15 行配置
总页面文件15 个 .vue1 个 ListPage.vue
样式修改改 15 个文件改 1 个文件
新增模块耗时2 小时10 分钟

新增模块示例

添加"账号管理"页面,只需在menu.js加一段配置:

{path:'/coc/accounts',title:'账号管理',page:'list',endpoint:'/api/coc/accounts',columns:[{prop:'name',label:'名称',type:'text',required:true},{prop:'resource_type',label:'资源类型',type:'select',options:[['服务器','server'],['数据库','db'],['网络','network']]},{prop:'username',label:'用户名',type:'text',required:true},{prop:'password_encrypted',label:'密码',type:'password',sensitive:true},]}

零行 Vue 代码,零行 API 代码——后端用build_crud_router同样配置驱动生成 CRUD 接口。前后端各加一段配置,新模块上线。


设计原则

1. 配置只描述"是什么",不描述"怎么做"

// ✅ 好的配置:声明式{prop:'name',label:'名称',type:'text',required:true}// ❌ 坏的配置:命令式{prop:'name',render:(row)=>`<input value="${row.name}" />`}

2. 通用组件处理 80%,插槽处理 20%

大部分字段用内置类型(text/select/switch/textarea)。少数特殊需求用具名插槽扩展:

<el-table-column> <template #default="{ row }" v-if="c.type === 'steps'"> <el-tag v-for="(s, i) in row.steps">{{ i+1 }}.{{ s.name }}</el-tag> </template> </el-table-column>

3. 配置和组件分离

menu.js只管配置,ListPage.vue只管渲染。新增字段类型不改配置格式,只改组件。


踩坑记录

坑1:v-if 链条太长

一开始所有字段类型用v-if/v-else-if链条判断,10+ 个类型后模板可读性很差。

解决:把复杂类型(steps、target_select)拆成独立组件,在 ListPage 中按类型分发。

坑2:配置和组件耦合

最初在配置里写onChange回调函数,导致配置文件引用了组件内部状态。

解决:配置只描述字段属性,交互逻辑全部在 ListPage 内部根据type判断。

坑3:编辑时 JSON 字段处理

target_hosts字段存的是 JSON 字符串"[]",但前端el-select multiple需要数组。

解决:在open(row)时解析,在save()时序列化:

functionopen(row){if(f.type==='host_multi_select'){form[f.prop]=JSON.parse(row[f.prop]||'[]')// string → array}}functionsave(){if(f.type==='host_multi_select'){data[f.prop]=JSON.stringify(form[f.prop]||[])// array → string}}

总结

配置驱动不是新概念,但在运维场景特别有效——因为 CRUD 模块多且结构相似。

核心收益:

  1. 开发效率:新增模块从 2 小时降到 10 分钟
  2. 一致性:所有页面交互统一,用户学习成本低
  3. 可维护性:样式/逻辑修改集中在一个文件
  4. 可扩展性:新字段类型只需加一个v-if分支

适用场景:CRUD 为主的内部工具、管理后台、运维平台。不适用于高度定制化的面向 C 端的页面。


项目仓库:https://gitcode.com/cpyaxjq/AtomOps

本文为 AtomGit「码动四季·开源同行」秋季征稿投稿。如果觉得有帮助,欢迎到仓库点个 Star ⭐

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询