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

173 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 代码撰写规范
本规范规定脚手架里新代码怎么放、怎么命名、怎么接进容器。业务数据流见 [项目架构](./项目架构.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` 布局。