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:
27
doc/项目文档/基础应用/*基础应用文档撰写说明.md
Normal file
27
doc/项目文档/基础应用/*基础应用文档撰写说明.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 基础应用文档撰写说明
|
||||
|
||||
适用文件:`doc/基础应用/<中文名>(目录名).md`。一个 `src/app` 子目录一篇。
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 功能定义](#1-功能定义)
|
||||
- [2. 使用示例](#2-使用示例)
|
||||
- [3. 数据流与时序](#3-数据流与时序)
|
||||
|
||||
示例见 [基础应用(示例)](./基础应用(示例).md)。
|
||||
|
||||
## 1. 功能定义
|
||||
|
||||
一段话说明这个基础应用做什么。下列出对外入口:类或函数、文件路径、各自负责的事。
|
||||
|
||||
## 2. 使用示例
|
||||
|
||||
给出调用这个入口的代码,并注明代码所在文件。示例表现功能模块或路由怎样使用它。一个入口一段示例;入口很小且调用方式相同时可以合在一段里。
|
||||
|
||||
## 3. 数据流与时序
|
||||
|
||||
一条行为优先用时序图。时序图已经能说明调用顺序时,不要再画数据流图。数据流图只在时序图表达不了数据经过哪些对象时再补。图中只用本目录的类名、函数名、方法名。图下用中文复述这条路径。
|
||||
|
||||
本节标题按实际采用的图来写。只有时序图时,标题为「时序」。
|
||||
|
||||
不画功能模块的调用链,例如 `SecurityViewModel` 到 `SecurityApplication`。不把其他基础应用的内部步骤画进本篇。功能模块何时调用这些入口,由功能模块文档的时序图说明。使用示例里的代码片段可以来自调用方。
|
||||
49
doc/项目文档/基础应用/主题(theme).md
Normal file
49
doc/项目文档/基础应用/主题(theme).md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 主题
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [切换主题](#切换主题)
|
||||
|
||||
## 功能定义
|
||||
|
||||
主题保存 `light`、`dark` 或 `system`,解析成实际的 `light` 或 `dark`,并交给界面库。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `getThemeMode()` | `src/app/theme/theme.ts` | 读取保存的 `ThemeMode`,缺省为 `system` |
|
||||
| `resolveTheme(mode)` | 同上 | 把 `ThemeMode` 解析为 `ResolvedTheme` |
|
||||
| `setThemeMode(mode)` | 同上 | 保存模式、涂色,并通知订阅者 |
|
||||
| `subscribeTheme(listener)` | 同上 | 订阅主题变化 |
|
||||
| `useThemeMode()` | `src/app/theme/use_theme.ts` | 读取当前 `ThemeMode` |
|
||||
| `useResolvedTheme()` | 同上 | 读取当前 `ResolvedTheme` |
|
||||
| `antdTheme(resolved)` | `src/app/theme/antd_theme.ts` | 按解析结果选择界面库算法 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
应用外壳把解析结果交给界面库。文件:`src/app/routes/app_shell.tsx`。
|
||||
|
||||
```ts
|
||||
const resolved = useResolvedTheme();
|
||||
<ConfigProvider theme={antdTheme(resolved)} />
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 切换主题
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant setThemeMode
|
||||
participant applyTheme
|
||||
participant resolveTheme
|
||||
participant subscribeTheme
|
||||
|
||||
setThemeMode->>applyTheme: applyTheme(mode)
|
||||
applyTheme->>resolveTheme: resolveTheme(mode)
|
||||
setThemeMode->>subscribeTheme: emit()
|
||||
```
|
||||
|
||||
`setThemeMode` 写入本地后调用 `applyTheme`。`resolveTheme` 在模式为 `system` 时读取系统配色,否则直接采用 `light` 或 `dark`。涂色写到 `documentElement.dataset.theme`,然后通知 `subscribeTheme` 的监听者。`useResolvedTheme` 通过这个订阅更新界面。首次涂色在根组件挂载之后执行,水合完成前不改 `<html>` 上的属性。
|
||||
67
doc/项目文档/基础应用/会话(session).md
Normal file
67
doc/项目文档/基础应用/会话(session).md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 会话
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [凭证变化](#凭证变化)
|
||||
|
||||
## 功能定义
|
||||
|
||||
会话负责登录凭证的保存、读取和清除,并在凭证变化后通知界面。`AUTH_CHANGED` 只表示凭证变了,不是领域事件。登录是否成功由安全模块的 `LoginResult` 决定。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `TokenStore.save(token)` | `src/app/session/token_store.ts` | 写入凭证并调用 `notifyAuthChanged` |
|
||||
| `TokenStore.get()` | 同上 | 读出凭证;没有则返回 `null` |
|
||||
| `TokenStore.remove()` | 同上 | 清除凭证并调用 `notifyAuthChanged` |
|
||||
| `TokenStore.has()` | 同上 | 当前是否持有凭证 |
|
||||
| `hasSession()` | `src/app/session/auth_session.ts` | 从根容器取出 `TokenStore` 并调用 `has()` |
|
||||
| `useAuthenticated()` | `src/app/session/use_authenticated.ts` | 订阅 `AUTH_CHANGED`,得到当前是否已登录 |
|
||||
| `notifyAuthChanged()` | `src/app/session/auth_event.ts` | 发出 `AUTH_CHANGED` |
|
||||
| `subscribeAuth(listener)` | 同上 | 登记 `AUTH_CHANGED` 的监听,并返回取消函数 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
安全模块在登录成功后写入凭证,登出时清除。文件:`src/features/security/application/security_application.ts`。
|
||||
|
||||
```ts
|
||||
async login(credentials: AuthCredentials): Promise<void> {
|
||||
const result = await this.repository.login(credentials);
|
||||
this.tokenStore.save(result.accessToken);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.tokenStore.remove();
|
||||
}
|
||||
```
|
||||
|
||||
路由用 `hasSession()` 判断能否进入。文件:`src/app/routes/layouts/auth.layout.tsx`。
|
||||
|
||||
```ts
|
||||
const policy = decideRoutePolicy('require-auth', hasSession(), url.pathname, url.search);
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 凭证变化
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TokenStore
|
||||
participant notifyAuthChanged
|
||||
participant subscribeAuth
|
||||
participant useAuthenticated
|
||||
participant hasSession
|
||||
|
||||
useAuthenticated->>subscribeAuth: subscribeAuth(sync)
|
||||
TokenStore->>TokenStore: save(token)
|
||||
TokenStore->>notifyAuthChanged: notifyAuthChanged()
|
||||
notifyAuthChanged->>subscribeAuth: AUTH_CHANGED
|
||||
subscribeAuth->>useAuthenticated: sync()
|
||||
useAuthenticated->>hasSession: hasSession()
|
||||
hasSession->>TokenStore: has()
|
||||
```
|
||||
|
||||
`TokenStore.save` 和 `TokenStore.remove` 改完本地凭证后调用 `notifyAuthChanged`。`notifyAuthChanged` 发出 `AUTH_CHANGED`。`useAuthenticated` 事先通过 `subscribeAuth` 登记 `sync`,收到事件后再次调用 `hasSession`。`hasSession` 只调用 `TokenStore.has`。
|
||||
66
doc/项目文档/基础应用/依赖注入(DI).md
Normal file
66
doc/项目文档/基础应用/依赖注入(DI).md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 依赖注入
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [绑定端口](#绑定端口)
|
||||
|
||||
## 功能定义
|
||||
|
||||
依赖注入提供全库共用的根容器,并把功能模块的端口绑定到具体实现。容器默认单例,且对带 `@injectable` 的类自动绑定。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `rootContainer` | `src/app/di/root_container.ts` | 根容器。`autobind` 打开,`defaultScope` 为 `Singleton` |
|
||||
| `bindAs(port, name?)` | `src/app/di/bind_as.ts` | 把抽象端口绑定到实现类。同一端口有多个实现时传入 `name` |
|
||||
|
||||
## 使用示例
|
||||
|
||||
安全模块把 `ISecurityRepository` 绑到 `SecurityRepository`。文件:`src/features/security/infra/persistence/security_repository.ts`。
|
||||
|
||||
```ts
|
||||
@bindAs(ISecurityRepository)
|
||||
@injectable('Singleton')
|
||||
export class SecurityRepository implements ISecurityRepository {}
|
||||
```
|
||||
|
||||
`@bindAs` 写在 `@injectable` 上面。类装饰器从靠近类的那一个开始执行,`@injectable` 先写入构造参数类型,`bind(port).to(impl)` 才能读到。
|
||||
|
||||
项目模块把同一个 `IProjectRepository` 绑两次,用名称区分:
|
||||
|
||||
```ts
|
||||
@bindAs(IProjectRepository, ProjectStore.LOCAL)
|
||||
@injectable('Singleton')
|
||||
export class LocalProjectRepository implements IProjectRepository {}
|
||||
```
|
||||
|
||||
`RemoteProjectRepository` 使用 `ProjectStore.REMOTE`。用例按名称注入抽象端口:
|
||||
|
||||
```ts
|
||||
constructor(
|
||||
@inject(IProjectRepository) @named(ProjectStore.LOCAL) private localRepository: IProjectRepository,
|
||||
@inject(IProjectRepository) @named(ProjectStore.REMOTE) private remoteRepository: IProjectRepository,
|
||||
) {}
|
||||
```
|
||||
|
||||
模块入口负责导入这些实现,绑定才会执行。文件:`src/features/security/security.ts`。
|
||||
|
||||
```ts
|
||||
import './infra/persistence/security_repository';
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 绑定端口
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant bindAs
|
||||
participant rootContainer
|
||||
|
||||
bindAs->>rootContainer: bind(port).to(impl).whenNamed(name)
|
||||
```
|
||||
|
||||
`bindAs` 返回的装饰器在类定义时把端口绑到实现。带名称时用 `whenNamed` 区分同一端口的多个实现。同一个实现再次执行时,先卸掉上一次的绑定再绑。用例构造函数用 `@inject` 指定端口,用 `@named` 指定实现,容器创建用例时传入对应的仓储。
|
||||
54
doc/项目文档/基础应用/国际化(i18n).md
Normal file
54
doc/项目文档/基础应用/国际化(i18n).md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 国际化
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [注册文案](#注册文案)
|
||||
|
||||
## 功能定义
|
||||
|
||||
国际化保存各模块的文案,并按当前语言取出文本。默认回退语言是 `zh`。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `registerMessages(feature, locales)` | `src/app/i18n/catalog.ts` | 把一个功能模块的 `zh`、`en` 文案写入目录 |
|
||||
| `buildResources()` | 同上 | 把已登记的文案交给 i18next |
|
||||
| `I18n.translate(key)` | `src/app/i18n/i18n.ts` | 按键取出当前语言的文本 |
|
||||
| `I18n.changeLanguage(lng)` | 同上 | 切换 `zh` 或 `en` |
|
||||
| `I18n.currentLanguage` | 同上 | 当前语言 |
|
||||
|
||||
功能模块自己的键缺失时,`parseMissingKeyHandler` 会改查 `common` 下的同名键。
|
||||
|
||||
## 使用示例
|
||||
|
||||
安全模块在加载时登记文案。文件:`src/features/security/i18n/index.ts`。
|
||||
|
||||
```ts
|
||||
registerMessages('security', { zh: zhCN, en: enUS });
|
||||
```
|
||||
|
||||
页面 ViewModel 取文本。文件:`src/features/security/presentation/security/security_view_model.ts`。
|
||||
|
||||
```ts
|
||||
this.errorMessage = this.i18n.translate(currentForm.validationErrorKey);
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 注册文案
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant I18n
|
||||
participant buildResources
|
||||
participant registerMessages
|
||||
participant i18n
|
||||
|
||||
I18n->>buildResources: buildResources()
|
||||
registerMessages->>registerMessages: bundles
|
||||
registerMessages->>i18n: addResourceBundle
|
||||
```
|
||||
|
||||
`I18n` 构造时用 `buildResources` 初始化。此后 `registerMessages` 写入 `bundles`;i18next 已经初始化时,再调用 `addResourceBundle`。尚未初始化时只写入 `bundles`,等 `I18n` 构造时一并载入。
|
||||
45
doc/项目文档/基础应用/数据库(Database).md
Normal file
45
doc/项目文档/基础应用/数据库(Database).md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 数据库
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [打开并升级](#打开并升级)
|
||||
|
||||
## 功能定义
|
||||
|
||||
数据库打开名为 `local_db` 的 IndexedDB,并按 `schemaVersions` 从旧版本逐级升级。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `Database.open()` | `src/app/db/database.ts` | 打开数据库。同一时刻复用同一次打开 |
|
||||
| `Database.reset()` | 同上 | 关闭进行中的打开,并删除 `local_db` |
|
||||
| `schemaVersions` | `src/app/db/schema/index.ts` | 按版本排列的升级函数。`DB_VERSION` 等于它的长度 |
|
||||
| `upgradeV1` | `src/app/db/schema/v1.ts` | 第一版。创建 `projects` 对象仓库,`keyPath` 为 `id` |
|
||||
|
||||
## 使用示例
|
||||
|
||||
本地项目仓储在读写前打开数据库。文件:`src/features/project/infra/persistence/local_project_repository.ts`。
|
||||
|
||||
```ts
|
||||
const db = await this.database.open();
|
||||
```
|
||||
|
||||
对象仓库不存在时,仓储关闭连接并调用 `Database.reset()`,再打开一次。
|
||||
|
||||
## 时序
|
||||
|
||||
### 打开并升级
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Database
|
||||
participant schemaVersions
|
||||
|
||||
Database->>Database: open()
|
||||
Database->>Database: connect()
|
||||
Database->>schemaVersions: upgrade(oldVersion)
|
||||
```
|
||||
|
||||
`open` 若已有进行中的连接,直接返回该连接。`onupgradeneeded` 时,`upgrade` 从 `oldVersion` 走到 `newVersion`,依次执行 `schemaVersions` 里对应的函数。版本变化导致连接关闭时,下一次 `open` 重新连接。
|
||||
52
doc/项目文档/基础应用/生命周期(lifecycle).md
Normal file
52
doc/项目文档/基础应用/生命周期(lifecycle).md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 生命周期
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [页面 ViewModel](#页面-viewmodel)
|
||||
|
||||
## 功能定义
|
||||
|
||||
生命周期负责创建页面 ViewModel,并在进入、刷新、离开时回调。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `PageViewModel` | `src/app/lifecycle/page_view_model.ts` | 页面 ViewModel 的基类。回调为 `created`、`ready`、`beforeRefresh`、`dispose` |
|
||||
| `useViewModel(identifier)` | `src/app/lifecycle/use_view_model.ts` | 从根容器取出 ViewModel,并接上上述回调 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
编辑器页面取出 `EditorViewModel`。文件:`src/features/editor/presentation/editor/editor.tsx`。
|
||||
|
||||
```ts
|
||||
const vm = useViewModel(EditorViewModel);
|
||||
```
|
||||
|
||||
ViewModel 继承 `PageViewModel`,需要在容器里可解析。文件:`src/features/editor/presentation/editor/editor_view_model.ts`。
|
||||
|
||||
```ts
|
||||
@injectable('Transient')
|
||||
export class EditorViewModel extends PageViewModel {}
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 页面 ViewModel
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant useViewModel
|
||||
participant rootContainer
|
||||
participant PageViewModel
|
||||
|
||||
useViewModel->>rootContainer: get(identifier)
|
||||
rootContainer-->>useViewModel: PageViewModel
|
||||
useViewModel->>PageViewModel: created()
|
||||
useViewModel->>PageViewModel: ready()
|
||||
useViewModel->>PageViewModel: beforeRefresh()
|
||||
useViewModel->>PageViewModel: dispose()
|
||||
```
|
||||
|
||||
`created` 在首次取出实例时调用。`ready` 在效果提交后调用。窗口触发 `pagehide` 时调用 `beforeRefresh`。离开页面时去掉监听并调用 `dispose`。
|
||||
51
doc/项目文档/基础应用/线程脚本(WebWorker).md
Normal file
51
doc/项目文档/基础应用/线程脚本(WebWorker).md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 线程脚本
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [请求与回复](#请求与回复)
|
||||
|
||||
## 功能定义
|
||||
|
||||
线程脚本管理一个 Worker 的创建、请求和崩溃。当前没有功能模块调用它。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `WorkerSession.request(message)` | `src/app/worker/worker_session.ts` | 保证 Worker 存在,发送 `WorkerMessage`,并等待对应 `WorkerReply` |
|
||||
| `WorkerSession.dispose()` | 同上 | 拒绝尚未完成的请求,并终止 Worker |
|
||||
| `WorkerRuntime.listen(handlers)` | `src/app/worker/worker_runtime.ts` | 在 Worker 线程按 `type` 分发,并用 `WorkerReply` 回复 |
|
||||
| `WorkerMessage` | `src/app/worker/entities/worker_message.ts` | 请求体。含 `id`、`type`、`payload` |
|
||||
| `WorkerReply` | `src/app/worker/entities/worker_reply.ts` | 回复体。按 `id` 交回等待中的请求 |
|
||||
| `WorkerError` | `src/app/worker/entities/worker_error.ts` | Worker 失败时的错误 |
|
||||
|
||||
连续崩溃超过一次后,`WorkerSession` 停止再创建,后续 `request` 直接拒绝。
|
||||
|
||||
## 使用示例
|
||||
|
||||
调用方传入创建函数,再发送一条消息。
|
||||
|
||||
```ts
|
||||
const worker = new WorkerSession(() => new Worker('worker.js'));
|
||||
const message = new WorkerMessage('type', payload);
|
||||
const result = await worker.request(message);
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 请求与回复
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant WorkerSession
|
||||
participant WorkerMessage
|
||||
participant WorkerReply
|
||||
|
||||
WorkerSession->>WorkerSession: ensure()
|
||||
WorkerSession->>WorkerMessage: toJson()
|
||||
WorkerSession->>WorkerReply: fromJson(data)
|
||||
WorkerReply-->>WorkerSession: unwrap()
|
||||
```
|
||||
|
||||
`ensure` 没有实例时调用构造函数传入的工厂。`request` 按 `WorkerMessage.id` 登记等待。Worker 线程里的 `WorkerRuntime.listen` 按 `type` 调用处理函数,并用 `WorkerReply` 回复。`onmessage` 用 `WorkerReply.fromJson` 找到同一 `id`,再 `unwrap` 完成该请求。`onerror` 或 `onmessageerror` 时终止当前 Worker,并拒绝尚未完成的请求。
|
||||
42
doc/项目文档/基础应用/网络(network).md
Normal file
42
doc/项目文档/基础应用/网络(network).md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 网络
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [本站请求](#本站请求)
|
||||
|
||||
## 功能定义
|
||||
|
||||
网络提供两个 HTTP 客户端。`Http` 访问本站接口,自动附带凭证,并在 401 时清除凭证。`ExternalHttp` 访问第三方接口,响应体原样返回。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| `Http.instance` | `src/app/network/http.ts` | 本站 Axios 实例。请求拦截器读取凭证,响应拦截器按 `code` 解包 |
|
||||
| `ExternalHttp.instance` | `src/app/network/external_http.ts` | 第三方 Axios 实例。成功时返回 `response.data` |
|
||||
|
||||
`Http` 在响应 `code` 不是 200 时拒绝。状态码为 401 时调用 `TokenStore.remove`。凭证如何保存见 [会话](./会话(session).md)。
|
||||
|
||||
## 使用示例
|
||||
|
||||
安全仓储通过 `Http.instance` 提交登录。文件:`src/features/security/infra/persistence/security_repository.ts`。
|
||||
|
||||
```ts
|
||||
return await this.http.instance.post('/auth/login', plainDto);
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 本站请求
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Http
|
||||
|
||||
Http->>Http: interceptors.request
|
||||
Http->>Http: Authorization
|
||||
Http->>Http: interceptors.response
|
||||
```
|
||||
|
||||
请求拦截器读取凭证,有凭证时写入 `Authorization`。响应拦截器在 `code` 为 200 时返回 `data`,否则拒绝。状态码为 401 时清除凭证并离开当前页。`ExternalHttp` 不写入 `Authorization`,也不读取 `code`。
|
||||
54
doc/项目文档/基础应用/路由(routes).md
Normal file
54
doc/项目文档/基础应用/路由(routes).md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 路由
|
||||
|
||||
## 目录
|
||||
|
||||
- [功能定义](#功能定义)
|
||||
- [使用示例](#使用示例)
|
||||
- [时序](#时序)
|
||||
- [访问策略](#访问策略)
|
||||
|
||||
## 功能定义
|
||||
|
||||
路由把 URL 指到功能模块页面,并用布局策略决定未登录或已登录时能否进入。
|
||||
|
||||
| 入口 | 文件 | 定义 |
|
||||
|---|---|---|
|
||||
| 路由表 | `src/app/routes.ts` | 声明页面路径和所属布局 |
|
||||
| `decideRoutePolicy` | `src/app/routes/layouts/decide_route_policy.ts` | 根据 `require-auth` 或 `require-guest` 返回 `allowed` 或 `denied` |
|
||||
| `AuthLayout` | `src/app/routes/layouts/auth.layout.tsx` | 已登录区域。未登录时拒绝 |
|
||||
| `GuestLayout` | `src/app/routes/layouts/guest.layout.tsx` | 访客区域。已登录时拒绝 |
|
||||
|
||||
路由表:
|
||||
|
||||
| 路径 | 布局 | 页面 |
|
||||
|---|---|---|
|
||||
| `/` | `app_shell` | `home` |
|
||||
| `/security` | `guest` | `security` |
|
||||
| `/profile` | `auth` | `security` 的资料页 |
|
||||
| `/projects/remote` | `auth` | `project` |
|
||||
| `/projects/local` | `auth` | `project` |
|
||||
| `/editor/:projectId` | `auth` | `editor` |
|
||||
|
||||
## 使用示例
|
||||
|
||||
已登录布局把 `hasSession()` 交给策略。文件:`src/app/routes/layouts/auth.layout.tsx`。
|
||||
|
||||
```ts
|
||||
const policy = decideRoutePolicy('require-auth', hasSession(), url.pathname, url.search);
|
||||
if (policy.status === 'denied') throw redirect(policy.redirect.to);
|
||||
```
|
||||
|
||||
## 时序
|
||||
|
||||
### 访问策略
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant AuthLayout
|
||||
participant decideRoutePolicy
|
||||
|
||||
AuthLayout->>decideRoutePolicy: decideRoutePolicy(require-auth, authenticated, pathname, search)
|
||||
decideRoutePolicy-->>AuthLayout: denied
|
||||
```
|
||||
|
||||
`require-auth` 且未登录时,结果是 `denied`,`redirect.to` 指向 `/security`,并带上当前路径。`require-guest` 且已登录时,`redirect.to` 为 `/`。其余情况为 `allowed`。`GuestLayout` 使用 `require-guest`,步骤相同。
|
||||
Reference in New Issue
Block a user