Power Coding 是我在 2023 年开始的一个 TypeScript 实验,也是后来公司共享 Model 层的起点。

当时做后台页面,经常会遇到一种情况:同一份用户数据,在 interface 里定义一次,在 API 文件里转换一次,到了表单和表格又要重新描述。字段改名或结构发生变化后,这些地方都需要同步修改,很容易遗漏。

我想试试把这些原本就属于业务数据的逻辑放回 Model。它不只负责类型,还知道如何从接口数据创建实例、如何转换回请求参数,以及自己具有什么行为。Power Coding 就是围绕这个想法写出来的。

🧩
公开仓库记录的是这套方案的起点。它后来被整理为公司内部共享 Model 模块,目前已在几乎所有前端项目中稳定运行。

让 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> {}
Model、字段转换与 CRUD Deriver 的基础组合

经过 User.from(raw) 以后,profile 不再是一个普通对象,而是 UserProfile 实例。页面不需要知道接口原始数据是怎样嵌套的,也不用在每个请求回调里重新构造对象。

@Field 还可以处理字段重命名、自定义转换、单向忽略、默认值以及对象的展开和嵌套。日期、可选字段或前后端命名不一致时,规则写在对应字段上。对页面来说,这些差异已经在数据进入系统时被处理完成。

clone()merge()mix() 则用于编辑状态。例如修改用户资料时,可以克隆一份 Model 交给表单,保存成功后再合并回原实例。这样既保留 Model 的类型和行为,也不会让输入过程直接污染当前状态。

用 Deriver 组合通用行为

仅仅解决数据转换还不够。很多 Model 会重复实现增删改查、分页、滚动加载或缓存,所以项目又加入了 @Derive

CRUDDeriver 根据接口地址为 Model 提供 getcreateupdatedelete。分页与滚动列表也可以通过 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:typedesign:fieldsdesign:initializer。因此,大部分字段不需要重复手写 typeBaseModel.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 组织方式不仅可以稳定工作,也确实让数据规则更集中,让不同项目之间形成一致的开发方式。

项目链接

GitHub - WayneWu98/power-coding
Power Coding 的公开原型与源码。
GitHub 仓库
TypeScript 装饰器实践:用 Class Model 组织 OOP 开发
以 TypeScript 装饰器为核心,介绍 Class Model、声明式序列化、字段映射和接口调用。
设计思考
TopWidgets⁺ 创作平台:可视化编辑器的技术架构与实现
从编辑器内核、本地项目归档到移动端交付,拆解 TopWidgets⁺ 创作平台的完整技术架构。
实际业务案例