Power Coding 是我在 2023 年开始的一个 TypeScript 实验,也是后来公司共享 Model 层的起点。
当时做后台页面,经常会遇到一种情况:同一份用户数据,在 interface 里定义一次,在 API 文件里转换一次,到了表单和表格又要重新描述。字段改名或结构发生变化后,这些地方都需要同步修改,很容易遗漏。
我想试试把这些原本就属于业务数据的逻辑放回 Model。它不只负责类型,还知道如何从接口数据创建实例、如何转换回请求参数,以及自己具有什么行为。Power Coding 就是围绕这个想法写出来的。
让 Model 成为数据边界
项目使用 @Model 标记模型,@Field 描述字段类型和转换规则。BaseModel.from() 把接口返回的普通对象转换为 Model,toPlain() 再把实例转换成可以提交给接口的对象。
@Model()
class UserProfile extends BaseModel {
nickname: string
}
@Model()
@Derive(CRUDDeriver('/users'))
class User extends BaseModel {
id: number
@Field({ type: UserProfile })
profile: UserProfile
}
interface User extends CRUD<User> {}经过 User.from(raw) 以后,profile 不再是一个普通对象,而是 UserProfile 实例。页面不需要知道接口原始数据是怎样嵌套的,也不用在每个请求回调里重新构造对象。
@Field 还可以处理字段重命名、自定义转换、单向忽略、默认值以及对象的展开和嵌套。日期、可选字段或前后端命名不一致时,规则写在对应字段上。对页面来说,这些差异已经在数据进入系统时被处理完成。
clone()、merge() 和 mix() 则用于编辑状态。例如修改用户资料时,可以克隆一份 Model 交给表单,保存成功后再合并回原实例。这样既保留 Model 的类型和行为,也不会让输入过程直接污染当前状态。
用 Deriver 组合通用行为
仅仅解决数据转换还不够。很多 Model 会重复实现增删改查、分页、滚动加载或缓存,所以项目又加入了 @Derive。
CRUDDeriver 根据接口地址为 Model 提供 get、create、update 和 delete。分页与滚动列表也可以通过 Deriver 获得刷新、加载更多和结束状态。业务代码仍然可以在 Model 中补充自己的方法,不需要为了复用通用行为继承一组层级很深的基类。
装饰器在运行时可以为 Class 增加方法,但 TypeScript 不会自动识别这些新成员,因此还需要用同名 interface 补充 CRUD<User> 等类型。这不算特别优雅,却清楚划分了运行时行为和静态类型之间的边界。
后来进入实际业务后,缓存也采用了同样的方式。一个 Model 可以同时派生请求和缓存能力,从本地恢复的数据仍然会被还原成原来的 Model,重新请求后也能直接合并到当前实例。页面只决定什么时候读取缓存或刷新数据,不需要自己维护两套结构。
装饰器背后的构建插件
Power Coding 中比较特殊的一部分,是自定义的 Vite 插件 emit-reflect-metadata。
项目需要在运行时知道字段类型、字段列表和初始化器,但 Vite 默认使用的 esbuild 不会生成完整的装饰器元数据。只写 @Field(),并不足以让系统知道一个字段对应的是字符串、嵌套 Model 还是 Model 数组。
这个插件使用 TypeScript Compiler API 分析带 @Model 的 Class,在 Vite 继续编译前补入 design:type、design:fields 和 design:initializer。因此,大部分字段不需要重复手写 type,BaseModel.default() 也能根据初始化器创建具有正确默认值的实例。
后续几次改动主要都在完善这部分:只处理明确标记的 Model、识别嵌套字段、复用 TypeScript Program,并通过 MagicString 保留 Source Map。装饰器让业务代码看起来更简单,但真正支撑这种声明方式的是编译阶段的静态分析。
在实际业务中的使用
GitHub 仓库记录的是早期原型。这套方案后来被整理为公司内部的共享 Model 模块,目前几乎所有前端项目都在使用,并且已经在实际业务中稳定运行。
不同项目面对的数据各不相同,但需要处理的问题很接近:接口字段与前端命名不一致、响应中包含嵌套对象、日期和文件需要转换、部分字段只能单向传递,以及缓存中的普通数据需要恢复为可以继续使用的实例。共享 Model 层为这些问题提供了统一做法,项目不必重新实现一套转换逻辑。
它也让接口、状态管理和缓存使用同一种数据结构。数据从接口进入后先恢复成 Model,业务代码围绕 Model 工作,需要提交或保存时再统一序列化。字段或规则发生变化时,修改集中在模型定义中,不需要逐个检查页面、Store 和请求回调。
TopWidgets⁺ 创作平台 是其中一个较复杂的例子。平台中的大部分数据定义都使用 Model,作品会经过编辑、IndexedDB、自动备份、项目归档、真机调试和发布等流程。Model 层让这些环节共享同一套字段与转换规则,避免每条链路维护自己的数据格式。更完整的产品架构可以参考《TopWidgets⁺ 创作平台:技术架构与实现》。
进入业务后,这套方案也逐渐收紧了边界。早期原型曾把 Ant Design Table 配置和校验反馈放进 Field,最终版本移除了这些 View 层依赖,只保留数据结构、转换、缓存和与接口直接相关的行为。布局、交互与组件状态继续由 View 负责,Model 因此可以脱离具体组件库,在不同项目中复用。
Power Coding 的公开仓库不是最终版本,但它记录了这套方案的起点。后续在公司内部几乎所有前端项目中的长期使用,证明这种 Model 组织方式不仅可以稳定工作,也确实让数据规则更集中,让不同项目之间形成一致的开发方式。
项目链接


