本文内容引用 哲玄-大前端全栈实践
1. 咱们的痛点:为什么天天在写重复代码?
兄弟们,做 B 端后台开发的日常是不是这样的? 早上来了,产品经理说:"加个用户管理页面。" 你想了想:
- 写个 Router 路由。
- 画个 Search Bar(搜索栏),里面放 Input 和 Select。
- 画个 Table(表格),搞定分页逻辑。
- 搞个 Dialog(弹窗),写表单验证。
- 调 API,绑定数据......
下午,产品经理又来了:"再加个订单管理页面。" 你一看,这特么跟上午那个页面长得有 90% 是一样的啊! 只是字段从"用户名"变成了"订单号",接口换了一个而已。
于是,我们变成了毫无感情的 Ctrl+C / Ctrl+V 机器。
这套 DSL 的设计初衷就是: 把那 80% 重复的 "头部、侧边栏、搜索、表格"抽象成 JSON 配置;剩下的 20% 复杂的逻辑,留给你去挥洒才华。
先附上图和完整的DSL配置 
js
module.exports = {
"mode": "dashboard",
// 头部菜单
"menu": [{
// 菜单唯一描述
"key": "",
// 菜单name
"name": "",
// 菜单类型 group分组 / module
"menuType": "",
// 子菜单 当menuType为group时有效
"subMenu": [{
}, ...],
// 菜单行为,当menuType为module时有效。 枚举值:iframe / custom / schema / sider
"moduleType": "",
// 当moduleType为sider时有效
siderConfig:{
menu:[{},...]
},
// 当moduleType为iframe时有效
iframeConfig: {
path: "", // iframe地址
},
// 当moduleType为custom时有效
customConfig: {
path: "" // 自定义路由路径
},
// 当moduleType为schema时有效
schemaConfig: {
api: "", // 数据源api地址,遵循restful风格
// json-schema描述
schema: {
type: "object",
properties: {
key: {
...schema, //标准json-schema描述
type: "string",// 字段类型
label: "字段名称",// 字段名称
// 字段在table中的配置
tableOption:{
...elTableColumnConfig, // 标准el-table-column配置
tofixed: 2, // 数字类型时的小数位数
visible: true // 是否在table中显示
},
// 字段在search-bar中的配置
searchOption:{
...elComponentConfig, // 标准el-component-column组件配置
comType:'', // 配置组件类型 input/select/dynamicSelect/date-picker/date-range等
default:'', // 默认值
// comType为select生效
enumList:[
{
label:'',
value:''
}
],
// comType为dynamicSelect生效
api:''
}
},
...
},
},
tableConfig: {
headerButtons: [
{
label:'', // 按钮名称
eventKey:'', // 按钮事件名
eventOption:{
// 按钮事件参数配置
params:{
"paramsKey":"schema::fieldKey" // schema:: 开头表示取schema中的字段值
}
}, // 按钮事件配置
...elButtonConfig, // 标准el-button配置
}
],
rowButtons: [
{
label:'', // 按钮名称
eventKey:'', // 按钮事件名
eventOption:{}, // 按钮事件配置
...elButtonConfig, // 标准el-button配置
}
]
}, // table相关配置
searchConfig: {}, // 搜索相关配置
components: {}, // 模块组件
}, ...]
}
2. 宏观架构:像"搭积木"一样组装页面
先看这张架构大图,别被吓到了,其实它就讲了两件事: "怎么配" 和 "怎么染" 。
2.1 底座:BFF Server 的"继承大法"
看架构图的最底下(红色虚线框区域)。 我们借鉴了面向对象的思想。比如你要做一个"电商后台":
- 领域模型(基类) :我们定义好一套所有页面通用的规则。比如,所有表格默认都有"创建时间",所有搜索栏默认都有"重置"按钮。
- 项目配置(子类) :具体到"订单管理"页面时,你只需要继承基类,然后说"我要加个订单金额字段"。
好处? 修改基类,一百个页面同时生效,不用一个个文件去改。
2.2 页面骨架:路由分发与组件分工
看中间蓝色的区域,它在工程实现上其实就是一套精心设计的 Vue Router 配置。
我们没有写死页面,而是预先定义好了几个"核心容器组件"(View),就像这是几辆不同功能的"车",JSON 数据就是"乘客",路由决定了把乘客装进哪辆车里。
来看看这段核心路由代码:
JavaScript
javascript
import Dashboard from "./dashboard.vue";
import boot from "@/boot";
// 1. 定义"引擎"列表:这里就是我们的策略库
const componentList = [
{
path: "iframe", // 对应 moduleType: iframe
component: () => import("./complex-view/iframe-view/iframe-view.vue"),
},
{
path: "schema", // 对应 moduleType: schema(核心低代码页)
component: () => import("./complex-view/schema-view/schema-view.vue"),
},
{
path: "todo", // 待办/自定义页
component: () => import("./todo/todo.vue"),
},
];
// 2. 动态生成扁平路由
const routes = componentList.map((item) => ({
path: `/view/dashboard/${item.path}`,
component: item.component,
}));
// 3. 侧边栏布局(Sider Layout)策略
// 如果配置了侧边栏,就让 sider-view 作为父路由,把 componentList 作为子路由嵌套进去
routes.push({
path: "/view/dashboard/sider",
component: () => import("./complex-view/sider-view/sider-view.vue"),
children: componentList,
});
// 4. 侧边栏兜底策略(Wildcard Route)
// 处理多级深层菜单的情况,保证 url 即使很长也能匹配到 sider 布局
routes.push({
path: "/view/dashboard/sider/:chapters+",
component: () => import("./complex-view/sider-view/sider-view.vue"),
});
boot(Dashboard, { routes });
这段代码揭示了 DSL 运行的实质流程:
(1) 路由即分发(The Router Dispatcher)
- 当 URL 匹配到
/view/dashboard/schema时,Vue Router 自动加载Schema-View组件。 - 当 URL 匹配到
/view/dashboard/iframe时,加载Iframe-View组件。 - 侧边栏的巧妙处理 :代码中专门为
/sider路径配置了children,这意味着如果你的页面配置了侧边栏,系统会先渲染sider-view框架,再把具体的内容(schema 或 iframe)渲染到<router-view>插槽中。
(2) Schema-View 的内部构造
一旦路由命中了 Schema-View,这个组件内部其实又做了一次精细化分工。它不是一个巨大的黑盒,而是由两个核心子面板组成的:
- 上层:Search-Panel(搜索面板) 它负责接收 JSON 中的
searchOption配置,动态生成 Input、Select 等表单项。 - 下层:Table-Panel(表格面板) 它负责接收 JSON 中的
tableOption配置,负责数据的展示、分页以及行列操作。
总结一下: 路由负责**"选车" (选 Sider 还是 Schema 还是 Iframe),而 Schema-View 负责"装货"**(把 JSON 拆分成搜索配置和表格配置,分发给上下两个面板)。这样一来,结构清晰,维护也非常容易。
3. JSON 核心揭秘:一份配置,掌控全局
接下来我们对着那段 JSON 代码,看看它是怎么指挥前端干活的。
3.1 路由的大脑:menu 和 moduleType
JSON 最外层的 menu 决定了系统的导航结构。这里有个最关键的开关叫 menuType 和 moduleType。
这也是为了防止"一刀切"。我们不能因为用了低代码,就写不了复杂页面。
- 如果是标准增删改查 :设
moduleType: "schema"。引擎自动干活,你喝咖啡。 - 如果是超复杂的数据大屏 :设
moduleType: "custom"。引擎让路,加载你手写的 Vue 组件。 - 如果是老系统页面 :设
moduleType: "iframe"。直接内嵌完事。
3.2 字段的"单源真理":最骚的操作在这里
请重点看 JSON 里的 properties 字段。这是整个 DSL 最精华的部分。
以往我们写代码,搜索栏写一遍 <el-input v-model="name">,表格里又写一遍 <el-table-column prop="name">。两边是割裂的。
在这个 DSL 里,我们把一个字段(比如 key)的所有属性聚合在一起:
JavaScript
arduino
key: {
label: "字段名称", // 通用名称
// 1. 告诉表格怎么展示
tableOption: {
visible: true,
tofixed: 2 // 假如是数字,自动保留2位小数
},
// 2. 告诉搜索栏怎么搜索
searchOption: {
comType: 'select', // 自动渲染成下拉框
enumList: [...] // 下拉选项
}
}
看懂了吗? 你只需要定义一次 key。解析器读取 searchOption 就在上面渲染搜索框,读取 tableOption 就在下面渲染表格列。 改一个字段名,搜索和表格同时更新。 这才叫"不重复造轮子"。
3.3 按钮与事件:schema:: 的魔法
表格肯定要有操作按钮,比如"编辑"、"删除"。 在 headerButtons 或 rowButtons 里,你可能会疑惑这一行: "paramsKey": "schema::fieldKey"
这是我们约定的一种动态取值语法。
- 场景 :点击"删除"按钮,需要调 API 删掉当前行,API 需要
id参数。 - 原理 :当前端引擎看到
schema::开头时,它就知道:"哦,用户不是要传死字符串,而是要我去当前这一行的数据 里,把fieldKey对应的值取出来传给后端。"
4. 运行流程总结
把图和代码串起来,整个流程是这样的:
-
加载:你打开页面,BFF Server 把合并好的 JSON 扔给前端。
-
路由 :前端 Router 看到
moduleType: "schema",就把任务交给通用模板页。 -
渲染:
- Search 引擎 遍历 JSON 里的
properties,把带有searchOption的字段挑出来,生成搜索栏。 - Table 引擎 遍历
properties,把带有tableOption的字段挑出来,生成表格列。
- Search 引擎 遍历 JSON 里的
-
交互:用户点"搜索",引擎自动收集所有搜索框的值,拼接 API URL,刷新表格数据。
5. 写在最后
这套 DSL 的本质,不是为了炫技,而是为了偷懒(褒义)。
它把我们从繁琐的 DOM 结构和 UI 库 API 中解放出来,让我们只关注业务数据本身 。毕竟,作为开发者,我们的价值应该体现在解决复杂的业务逻辑上,而不是比谁写的 <el-table-column> 更多,对吧?