Add a scaffold script that replaces the bq prefix with the new project name.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
172
doc/项目文档/代码撰写规范.md
Normal file
172
doc/项目文档/代码撰写规范.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 代码撰写规范
|
||||
|
||||
本规范规定脚手架里新代码怎么放、怎么命名、怎么接进容器。业务数据流见 [项目架构](./项目架构.md)。
|
||||
|
||||
## 目录
|
||||
|
||||
- [环境变量](#环境变量)
|
||||
- [目录与分层](#目录与分层)
|
||||
- [文件命名](#文件命名)
|
||||
- [类与函数命名](#类与函数命名)
|
||||
- [依赖注入](#依赖注入)
|
||||
- [Singleton 与 Transient](#singleton-与-transient)
|
||||
- [功能模块入口](#功能模块入口)
|
||||
- [import](#import)
|
||||
- [新增功能模块](#新增功能模块)
|
||||
|
||||
## 环境变量
|
||||
|
||||
环境文件放在 `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`:
|
||||
|
||||
```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)` 注入对应实现。
|
||||
|
||||
```ts
|
||||
@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`:
|
||||
|
||||
```ts
|
||||
import './i18n';
|
||||
import './infra/persistence/security_repository';
|
||||
```
|
||||
|
||||
第一行执行 `registerMessages`。第二行使 `SecurityRepository` 上的 `@bindAs(ISecurityRepository)` 跑起来。安全页在文件顶部引用它:
|
||||
|
||||
```ts
|
||||
import '../../security';
|
||||
```
|
||||
|
||||
`project.ts` 导入本地仓储和远程仓储。使用项目用例的页面同样要先引用它,例如 `import '@/features/project/project'` 或 `import '../../project'`。
|
||||
|
||||
新增仓储或文案包时,把对应文件 import 进这个入口,并保证至少有一个会被路由加载到的页面引用该入口。没有这行 import,容器里没有实现,文案也不会注册。
|
||||
|
||||
## import
|
||||
|
||||
跨目录使用 `@/`,与 `tsconfig.app.json` 的 `paths` 一致。同一功能模块内部使用相对路径。
|
||||
|
||||
```ts
|
||||
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` 布局。
|
||||
|
||||
Reference in New Issue
Block a user