Files
bq_react/doc/项目文档/代码撰写规范.md

9.7 KiB
Raw Permalink Blame History

代码撰写规范

本规范规定脚手架里新代码怎么放、怎么命名、怎么接进容器。业务数据流见 项目架构。

目录

环境变量

环境文件放在 src/。vite.config.ts 的 envDir 是 src。

文件 脚本
src/.env.debug npm run debug
src/.env.development npm run dev、npm run build
src/.env.production npm run prod、npm run build:prod

只有 VITE_ 开头的变量会进入浏览器。命名是 VITE_ 加大写蛇形,例如 VITE_API_URL。

这里只放会公开到前端的地址和开关。密钥不放进这些文件。业务代码用 import.meta.env.VITE_* 读取,并在使用处给出缺省值。Http 的基地址写在 src/app/network/http.ts:

baseURL: import.meta.env.VITE_API_URL || 'https://mybackend.com'

目录与分层

src/app 放全库能力,不放业务用例。src/features/<name> 放一个功能模块。

层 放置 可以依赖
domain 聚合、值对象、领域事件、接口 本模块 domain
application 用例 本模块 domain,以及 src/app 里该用例需要的入口
infra 接口的实现 本模块 domain,以及 src/app 的网络、数据库、依赖注入
presentation 页面、ViewModel、展示组件 本模块 application,以及标明可使用的其他模块类型

domain 不引用 React,也不引用 src/app。没有内容的层不建空目录。

聚合根是外部唯一可以修改的实体。Project 包含 Document 和图形,落笔是否成立、撤销和重做都写在 Project 上。页面和其他模块不调用 Document.addShape,也不替换文档快照。

仓储接口只表达取出和保存聚合,不表达本地或远程。local 与 remote 留在 application,用来选择哪个 infra 实现。接口的返回值是领域对象。登录返回 User,不返回未标明类型的 user。

登录成功先形成领域结果,再由用例调用 TokenStore.save。AUTH_CHANGED 只通知界面,不是领域事件。

路由只在 src/app/routes.ts 里把 URL 指到 presentation 下的页面。需要登录的页面放进 auth 布局,仅访客可进的页面放进 guest 布局。

文件命名

目录和文件用小写蛇形。

种类 形式 例子
页面 与所在目录同名 home/home.tsx
ViewModel *_view_model.ts security_view_model.ts
样式 与组件同名的 .scss home.scss
实体 聚合或业务名文件 project.ts、document.ts、user.ts
抽象接口 i_ 前缀 i_security_repository.ts
仓储实现 *_repository.ts,放在 infra/persistence security_repository.ts
布局 点号标出角色 auth.layout.tsx
文案 语言码 zh_cn.ts、en_us.ts
模块入口 与功能目录同名 security.ts、project.ts

类与函数命名

类用帕斯卡命名。函数和钩子用小驼峰,钩子以 use 开头。

种类 形式 例子
用例 名称Application SecurityApplication
页面模型 名称ViewModel,继承 PageViewModel HomeViewModel
仓储实现 名称Repository LocalProjectRepository
接口 抽象类,I 前缀 ISecurityRepository
实体 业务名,不加 Entity 后缀 Project、User、AuthCredentials
枚举 名称Enum AuthEnum

实体使用 User、Project 这样的业务名,类名不带 Entity 后缀。

路由页面使用默认导出。src/app/routes.ts 按文件加载页面。

依赖注入

全库只有 src/app/di/root_container.ts 里的 rootContainer。容器打开 autobind,默认 scope 是 Singleton。

对象 写法
用例、TokenStore、Http、Database @injectable('Singleton')
页面 ViewModel @injectable('Transient')
接口实现 @injectable('Singleton'),并加 @bindAs(接口)

Singleton 与 Transient

Singleton 在整个应用里只创建一次。之后无论哪个页面、哪次 rootContainer.get,拿到的都是同一个实例。适合没有页面现场、需要一直留着的对象:用例、仓储、TokenStore、Http、Database。登录后的凭证、数据库连接和用例方法要在首页、编辑器之间共用,所以这些类用 Singleton。

Transient 每次 rootContainer.get 都新建一个实例。useViewModel 在页面挂载时取一次 ViewModel,离开页面后这次实例不再使用。适合只属于当前页的状态:表单、errorMessage、尚未落笔的草稿。撤销栈属于 Project,不放在 ViewModel 里。SecurityViewModel、HomeViewModel、EditorViewModel、ProjectListViewModel 都用 Transient。

ViewModel 若标成 Singleton,再次进入同一页会看到上次留下的表单、错误和草稿。用例若标成 Transient,每个页面都会各持有一份,凭证和仓储无法在页面之间共用。

无参的 @injectable() 会落到容器默认的单例,效果与 Singleton 相同。新代码写出 scope。现有仓储仍是无参写法。

bindAs 定义在 src/app/di/bind_as.ts。装饰器在类定义时绑定,并且写在 @injectable 上面。类装饰器从靠近类的那一个开始执行,@injectable 先写入构造参数类型,bind(port).to(impl) 才能读到。同一个实现再次执行时,先卸掉上一次的绑定再绑。同一个接口有多个实现时传入名称,例如 @bindAs(IProjectRepository, ProjectStore.LOCAL)。用例构造函数用 @inject(IProjectRepository) 和 @named(ProjectStore.LOCAL) 注入对应实现。

@bindAs(ISecurityRepository)
@injectable('Singleton')
export class SecurityRepository implements ISecurityRepository {}

只在 infra 的接口实现上使用 @bindAs。用例和 ViewModel 由 autobind 按类本身注册,不再包一层接口。

功能模块入口

src/features/<name>/<name>.ts 只做副作用 import,不导出类,不写用例。它让文案注册和 @bindAs 在页面渲染前执行。

src/features/security/security.ts:

import './i18n';
import './infra/persistence/security_repository';

第一行执行 registerMessages。第二行使 SecurityRepository 上的 @bindAs(ISecurityRepository) 跑起来。安全页在文件顶部引用它:

import '../../security';

project.ts 导入本地仓储和远程仓储。使用项目用例的页面同样要先引用它,例如 import '@/features/project/project' 或 import '../../project'。

新增仓储或文案包时,把对应文件 import 进这个入口,并保证至少有一个会被路由加载到的页面引用该入口。没有这行 import,容器里没有实现,文案也不会注册。

import

跨目录使用 @/,与 tsconfig.app.json 的 paths 一致。同一功能模块内部使用相对路径。

import { useViewModel } from '@/app/lifecycle/use_view_model';
import { ProjectApplication } from '@/features/project/application/project_application';
import { SecurityViewModel } from './security_view_model';

副作用 import 放在文件最上方,单独成行,不写绑定。类型使用 import type。

其他模块可以引用对方的 application,以及标明可使用的 entity 和展示组件。不引用对方的 infra。

新增功能模块

  1. 在 src/features/<name>/domain 写实体和接口。接口是 I 前缀的抽象类。
  2. 在 application 写用例,并标 @injectable('Singleton')。
  3. 在 infra 写实现,标 @injectable('Singleton') 和 @bindAs(接口)。
  4. 把实现 import 进 src/features/<name>/<name>.ts。有文案时,同时 import 该模块的 i18n。
  5. 在 presentation 写页面和 ViewModel。ViewModel 继承 PageViewModel,标 @injectable('Transient')。页面先 import 模块入口,再用 useViewModel 取出 ViewModel。页面默认导出。
  6. 在 src/app/routes.ts 登记路径。需要登录的页面放进 auth 布局。