做后台管理系统,最枯燥的环节不是业务逻辑,而是反复写列表页、表单页、详情页。字段定义写一遍,表格列写一遍,表单控件再写一遍——三份几乎相同的配置,散落在不同文件里。VonaJS 5.1.34 的做法是:把渲染元数据直接绑在 Entity 字段上,一份配置驱动整个 CRUD 页面。底层依赖 Tanstack Table / Form / Query 三件套,框架帮你把"字段→列→控件→查询"这条链路串起来。
字段即配置:Entity 是渲染的起点
传统做法里,Entity(或 Model)只管数据结构,渲染逻辑在另一个文件单独维护。VonaJS 把两者合到一处——字段旁边直接写它"怎么显示":
@Entity<IEntityOptionsStudent>('demoStudent')
export class EntityStudent extends EntityBase {
@Property()
name: string;
@Property({
// 渲染元数据:告诉框架这个字段在表格/表单里怎么呈现
component: 'input',
label: '姓名',
required: true,
})
name: string;
@Property({
component: 'select',
label: '班级',
options: 'classEnum', // 指向枚举或远程数据源
})
classId: number;
@Property({
component: 'datePicker',
label: '入学日期',
format: 'YYYY-MM-DD',
})
enrollDate: string;
}
关键变化:@Property 装饰器不再只描述类型,还携带了 UI 渲染指令。component 决定控件类型,label 决定列头和表单标签,required 直接映射到表单校验。一份定义,表格列、表单字段、校验规则全部推导出来,不需要再写三份配置文件。
从 Entity 到 CRUD 页面的推导链
框架拿到 Entity 定义后,内部做了几件事:
- 表格列推导——每个
@Property字段变成 Tanstack Table 的一列,label作为列头,component决定单元格渲染器(select 字段自动显示枚举文本而非原始 ID)。 - 表单控件推导——新增/编辑表单直接从字段列表生成 Tanstack Form 配置,
component映射到对应输入控件,required变成校验规则。 - 查询参数推导——列表页的筛选条件也从 Entity 字段中提取,标记了
filterable的字段自动生成搜索表单。 - API 调用绑定——Tanstack Query 的 queryKey、请求路径根据 Entity 名称自动生成,新增走 POST、列表走 GET、删除走 DELETE,约定优于配置。
整条链路的核心假设是:大部分后台 CRUD 页面的差异只在字段本身,交互模式高度雷同。框架把雷同的部分模板化,你只需要声明字段"是什么、怎么显示"。
实战:用 VonaJS 搭一个学生管理模块
下面是一个可以直接改造使用的最小示例。假设你已经初始化了 VonaJS 项目(npm create vona@latest),我们来定义一个"课程管理"的 Entity 并生成 CRUD 页面。
第一步:定义 Entity
// src/module/demo/entity/entityCourse.ts
import { Entity, Property, EntityBase } from 'vona';
import type { IEntityOptionsCourse } from './types';
@Entity<IEntityOptionsCourse>('demoCourse')
export class EntityCourse extends EntityBase {
@Property({
component: 'input',
label: '课程名称',
required: true,
filterable: true, // 列表页可按此字段搜索
})
courseName: string;
@Property({
component: 'select',
label: '课程类型',
options: 'courseTypeEnum', // 枚举名,框架自动加载选项
filterable: true,
})
courseType: number;
@Property({
component: 'inputNumber',
label: '学分',
required: true,
min: 1,
max: 10,
})
credit: number;
@Property({
component: 'datePicker',
label: '开课日期',
format: 'YYYY-MM-DD',
})
startDate: string;
@Property({
component: 'textarea',
label: '课程简介',
span: 24, // 表单中占满整行
})
description: string;
}
第二步:注册枚举(框架需要知道 select 的选项)
// src/module/demo/config/enums.ts
export const courseTypeEnum = {
1: '必修课',
2: '选修课',
3: '实验课',
};
第三步:生成 CRUD 页面
VonaJS CLI 提供了生成命令,一条指令产出列表页、新增页、编辑页:
# 在项目根目录执行
vonas generate:crud demoCourse
命令执行后,src/module/demo/pages/ 下会出现:
courseList.vue——基于 Tanstack Table 的列表页,含筛选栏、分页、操作列courseForm.vue——基于 Tanstack Form 的新增/编辑表单页- 相关的路由配置和 API service 文件自动注入
第四步:微调(如果默认生成不够用)
生成的页面是标准模板,大部分场景直接可用。如果需要定制,改两个地方:
// 覆盖表格列配置——比如隐藏 description 列、给 courseName 加链接
const columnOverrides = {
description: { enableHiding: true, defaultHidden: true },
courseName: { cell: (info) => <a href={`/course/${info.row.original.id}`}>{info.getValue()}</a> },
};
// 覆盖表单布局——比如把日期字段和类型字段放同一行
const formLayoutOverrides = {
courseType: { span: 12 },
startDate: { span: 12 },
};
覆盖项和 Entity 定义分离,不污染字段声明本身。
这种做法的边界和取舍
声明式 CRUD 生成不是万能的,它有几个明确的前提和限制:
适合的场景: - 后台管理系统的标准增删改查页面,字段以文本、数字、下拉、日期为主 - 同一模块的列表/表单/详情交互模式一致,只是字段不同 - 团队希望快速出页面原型,后续再逐页精调
需要绕道的场景: - 复杂的嵌套表单(比如主子表联动编辑),Entity 装饰器的表达能力有限,还是得手写 Tanstack Form 配置 - 非标准交互(拖拽排序、行内编辑批量保存),模板推导不出来 - 高度定制的数据可视化页面,和 CRUD 模式无关
一个潜在风险:Entity 文件会变"胖"。 字段定义、渲染配置、校验规则、筛选标记全堆在一起,字段多的时候可读性下降。建议的做法是——简单字段直接写装饰器,复杂渲染逻辑抽到单独的 columnOverrides / formOverrides 文件里,保持 Entity 本身只放"最核心的显示指令"。
另一个值得注意的点:框架强绑定 Tanstack 三件套。 如果你团队已经用 Ant Design Table 或 Element Plus Form,迁移成本不小——不是换个组件库的问题,而是整条"字段→渲染→查询"的推导链路都依赖 Tanstack 的 API 设计。新项目可以直接上,存量项目要评估替换范围。
上手清单
如果你打算试一下 VonaJS 5.1 的声明式 CRUD,可以按这个顺序走:
npm create vona@latest初始化项目,选一个简单模块(比如"分类管理")做试点- 先写一个只有 3-4 个字段的 Entity,跑
generate:crud,看生成的页面是否满足预期 - 尝试加
filterable、component: 'select'、枚举配置,验证筛选和下拉是否自动生效 - 用
columnOverrides/formOverrides做一次微调,确认覆盖机制好用 - 再决定是否把复杂模块也迁移到这套模式
声明式 CRUD 的价值不在"少写代码"本身,而在把重复的配置收敛到一处、让字段定义成为唯一的数据源。如果你的后台系统有大量模式雷同的管理页面,这套思路能省下可观的对齐和维护成本。