这份文档是什么:把 BIE(brand-intelligence-engine)的前后端结构提炼成一套可复用、可照抄的全栈 DDD 代码模型。目标是让你看完能回答三件事——①后端一个模块该怎么摆、②前端一个上下文该怎么摆、③前后端怎么用一根契约缝起来。配套一个"加新功能分步清单",直接能上手。
配套阅读:13 讲(后端代码模型)、14 讲(一致性)、17 讲扩展分析(前后端契约)。
0. 全局心智模型:一次请求的一生
学这套之前,先把"一个功能从点击到落库再回显"这条线刻进脑子。后面所有目录都是为这条线服务的:
[前端] [后端]
features/ui 组件 modules/<ctx>/api/v1/endpoints.py
│ 调用 ▲ 路由命中
▼ │
entities/<ctx>/api/use-xxx │ 写:application/<ctx>_service.py(Command)
│ (加缓存 + adapter 适配) │ └→ domain(业务规则)→ infrastructure/repository_impl(落库)
▼ │ 读:application/<ctx>_query.py(Query,直接查只读视图)
shared/api/generated/endpoints(orval 生成) ──HTTP──┘
▲ 类型来自 contract: /openapi.json
shared/api/models(orval 生成) ◄────orval 读──── FastAPI 从 endpoints + schemas 自动产出
🪞 一句话:前端把请求顺着"组件 → 领域 hook → 生成客户端"往下递,过 HTTP 打到后端"endpoint → 应用服务(写聚合/读视图) → 仓储",而连接两端的不是口头约定,是 OpenAPI 这份机器合同。
1. 后端代码模型(modular monolith)
1.1 顶层结构
server/
├── app/
│ ├── main.py / _bootstrap.py / router.py # 启动装配:建 FastAPI app、挂各模块 router
│ ├── core/ # 技术内核(不含业务):llm_runtime、memos…
│ ├── shared/ # 跨模块共享:jobs、observability、outbox(发件箱)
│ ├── modules/ # ★核心★ 每个限界上下文一个文件夹
│ │ └── <ctx>/ # project / knowledge / chat / auth …
│ │ ├── api/v1/ # 接口层:endpoints.py + schemas.py
│ │ ├── application/ # 应用层:<ctx>_service.py(写) + <ctx>_query.py(读)
│ │ ├── domain/ # 领域层:实体/值对象/领域服务/仓储接口
│ │ └── infrastructure/ # 基础层:orm.py(PO) + repository_impl.py
│ └── studio/ # 跨上下文聚合 / 面向前端的编排(轻量 BFF 角色)
├── alembic/versions/ # 数据库迁移
└── pyproject.toml # 含 [tool.importlinter] 边界合同
1.2 模块内四层:职责 + 真实文件
以 modules/project/ 为例(每个模块都长一个样):
| 层 | 目录 | 职责 | BIE 真实文件 |
|---|---|---|---|
| 接口层 | api/v1/ |
定义 HTTP 路由、入参/出参 DTO、鉴权;把请求转成应用层调用 | endpoints.py、schemas.py |
| 应用层 | application/ |
用例编排,读写分离:写侧 Command 服务 + 读侧 Query 服务 | project_service.py(写)、project_query.py(读)、project_asset_service.py |
| 领域层 | domain/ |
实体/聚合根(充血)、值对象、领域服务、仓储接口 | (project 偏 CRUD,domain 较薄;memory_kernel 这类有 memory_item.py 等充血实体) |
| 基础层 | infrastructure/ |
仓储实现、ORM 持久化对象(PO)、外部客户端 | orm.py、repository_impl.py、asset_archive_orm.py |
1.3 五个必须吃透的后端模式
① endpoint(接口层)= 系统对外的脸
@router.post("", response_model=BIEResponse[ProjectDetail], summary="创建项目")
async def create_project(
body: CreateProjectRequest, # 入参 DTO(接口层自己的 schema)
user_id: UUID = Depends(get_current_user_id), # 鉴权(横切,收敛在这层)
svc: ProjectService = Depends(_svc), # 写侧服务
) -> BIEResponse[ProjectDetail]:
project = await svc.create_project(...) # 写:走 Command
result = await ProjectQueryService(db).get_detail_view(project.id, user_id) # 读:用 Query 读回
return BIEResponse.ok(result) # 统一响应包装
② CQRS:写服务 / 读服务分家。写走 *_service.py,读走 *_query.py,职责完全分离。project_query.py 的文件头注释把读侧的设计原则写得明明白白,照抄过来体会:
"""项目只读查询服务(CQRS Query Side)。
设计原则:
- 只负责读操作,绝不执行写操作(无 add / commit / rollback)。
- 直接操作 SQLAlchemy Core 层(select / join),绕开 ORM 实体水合(Hydration),
避免 Python 层的 N+1 循环拼装。
- 返回 Pydantic DTO,不暴露 ORM 对象到上层。
- 对应 FSD 的 Page/Widget 层数据需求,按使用场景定制查询形状。
"""
写侧则相反——project_service.py 是真正的"用例编排",一个 create_project 里串了好几步业务动作:
async def create_project(self, *, name, description, creator_id, agent_ids=None):
workspace_id = await WorkspaceService(self.session).get_personal_id(creator_id) # 跨上下文取数据
project = ProjectORM(name=name, ...); self.session.add(project); await self.session.flush()
await ProjectAssetService(self.session).bind_default_workflow(project.id, ...) # 创建后置动作
if agent_ids: ... # 建关联
💡 注意 endpoint 里"写完用
ProjectQueryService.get_detail_view读回再返回"——写侧只管把事做成,回显交给读侧,保证前端拿到的永远是读模型的一致视图,而不是写侧临时拼的对象。
③ Repository + 依赖倒置(含 BIE 的务实简化)。教科书做法是"仓储接口在 domain/、实现在 infrastructure/"。BIE 对 CRUD 型模块做了简化——repository_impl.py 直接继承一个泛型基类,省掉了为每个模块手写接口:
class ProjectRepositoryImpl(BaseRepository[ProjectORM]): # 复用 app/core/base_repository
def __init__(self, session): super().__init__(ProjectORM, session)
💡 这是"务实 > 教条"的典型:project 这种 CRUD 模块没有复杂领域规则,硬给它写一套 domain 仓储接口纯属样板。但对有真实业务规则的核心模块(如 memory_kernel),就该回到"接口在 domain、实现在 infra"的完整形态——用接口把领域逻辑和持久化解耦。后面第 9 节会专门分析这个取舍。
④ DTO 只在边界转一次。api/v1/schemas.py 的 DTO 是对外契约;内部映射到 application 的 Command/参数,不让内部模型裸奔给前端。
⑤ 统一响应 BIEResponse[T]。所有 endpoint 返回同一个信封(ok/err + data),前端拿到的结构永远一致。
1.4 边界执法:import-linter(这是它"硬"的关键)
pyproject.toml 里写成合同,pre-commit/CI 自动查(违规直接红):
[[tool.importlinter.contracts]] # domain 不许依赖外层
source_modules = ["...modules.*.domain"]
forbidden_modules = ["...modules.*.api", "...modules.*.application", "...modules.*.infrastructure"]
[[tool.importlinter.contracts]] # core 不许依赖业务模块
source_modules = ["...core"]
forbidden_modules = ["...modules"]
# 还有:"graph 节点禁止反向写业务表""shared.jobs 禁止依赖任何 modules" 等
2. 契约缝合层:OpenAPI + orval(前后端的"那根线")
这是全栈的灵魂,把第 1 节和第 3 节缝成一体:
后端 endpoints.py + schemas.py
│ FastAPI 自动产出
▼
/openapi.json ← 机器可读的"接口合同"(单一事实源)
│ orval(web/orval.config.mjs)读取
▼
前端 src/shared/api/generated/
├── endpoints/<ctx>/<ctx>.ts # React Query hooks:useCreateProject 等
└── models/ # 与后端同源的 TS 类型
三个 npm 脚本管住它:
| 命令 | 作用 |
|---|---|
npm run openapi:generate |
拉 OpenAPI → orval 全量重生成(clean:true,杜绝手改残留) |
npm run openapi:check |
校验文档、统计 paths、缓存——检测契约漂移 |
npm run openapi:mock |
用 msw 起 mock server——后端没写完前端可先开发 |
💡 铁律:
generated/下的代码不可手改,它是后端契约的投影;后端改字段→重生成→前端编译期就报错。这就是"前后端永不对不上"的根。
生成的 hook 不直接 fetch,而是统一走 orval 配的 customInstance(shared/api/client.ts)——一个 axios 实例 + 拦截器,把"所有请求都要做的横切事"收在一处:
export const apiClient = Axios.create({ baseURL: apiBaseUrl, withCredentials: true });
// 为所有请求自动注入 JWT Bearer Token
apiClient.interceptors.request.use((config) => {
if (!config.headers.Authorization) config.headers.Authorization = getAuthorizationHeader();
return config;
});
💡 这就是 17 讲"API 网关"职责在前端的落点:鉴权头注入、统一 baseURL、错误归一、token 刷新——全收敛在
client.ts这一处,业务代码(features)完全不用操心 token。
3. 前端代码模型(Feature-Sliced Design)
3.1 顶层结构(FSD:上层依赖下层,同层不互相依赖)
web/src/
├── app/ # 应用装配:providers、全局初始化
├── routes/ # 路由(TanStack Router)
├── widgets/ # 大块复合 UI(多个 feature 拼成的区域)
├── features/ # 一个个"用户能做的事":projects、chat-agent、artifact-review…
├── entities/ # ★前端侧的限界上下文★:project、agent、chat、session…(对称后端 modules)
└── shared/ # 通用底座:api、components/ui、lib、store、config
FSD 的依赖方向:
app → routes → widgets → features → entities → shared,只能从上往下依赖,同层之间不许互相 import。这就是前端版的"分层 + 边界"。
3.2 三层 api 漏斗(最该学的前端模式)
生成的原始 hook 不直接给 UI 用,中间垫两层:
shared/api/generated/endpoints/projects → useCreateProject() ① 原始:机器生成、纯传输、不可手改
↓ 包装
entities/project/api/use-projects.ts → useProjects() ② 领域包装:缓存策略 + adapter 适配
(配 shared/api/adapters/project-adapter.ts = 前端防腐层)
↓ 消费
features/projects/ui/create-project-modal.tsx ③ UI:只管渲染和交互
| 漏斗层 | 真实文件 | 职责 | 类比后端 |
|---|---|---|---|
| ① 生成层 | shared/api/generated/{endpoints,models} |
后端契约原始映射,不可手改 | 接口层裸 DTO |
| ② 领域层 | entities/<ctx>/api/use-*.ts + shared/api/adapters/*-adapter.ts |
套缓存(staleTime)、失效(invalidate)、把后端 DTO 转成前端领域模型 | 防腐层 ACL + 仓储 |
| ③ 业务层 | features/<ctx>/ui/*.tsx |
用 useXxx() 拿数据,做交互 |
应用层用例 |
entities/<ctx>/ 内部也分层:model/(types.ts、领域 adapter)、api/(use-* hooks、mutations)、ui/(该上下文的展示组件)。
看真实的 ② 领域包装层 entities/project/api/use-projects.ts,三个设计点都在注释里写明了:
const PROJECTS_STALE_TIME_MS = 30_000; // 侧边栏数据少变,30s 内不重发
export function useProjects(params?) {
return useListProjects(params, { // ← 调①生成层的原始 hook
query: {
staleTime: PROJECTS_STALE_TIME_MS, // a. 定缓存策略
select: selectProjectsFromResponse, // b. 用 select 在缓存层把 BIEResponse 拍扁成 Project[]
},
});
}
// 注释原话:反模式守护——**不**把结果同步到任何 Zustand store;
// 多组件并发调用本 hook,React Query 自动 dedupe,网络只发一次。
再看 ② 里的 adapter(shared/api/adapters/project-adapter.ts)——它就是前端的防腐层,负责"后端 DTO → 前端领域模型"的转换,还能补后端没有的纯前端字段:
// 后端没给 color,前端按 project_id 哈希取色——纯前端样式字段不污染后端契约
function hashColor(id: string): string { ... }
// 职责边界(注释原话):字段重命名(snake_case→camelCase)、补前端专用字段、屏蔽后端结构变化
export function adaptBackendProjectToDomain(dto: ProjectListItem): Project { ... }
💡 前后端限界上下文对称:后端
modules/project↔ 前端entities/project,用同一套词汇——DDD 的"通用语言"跨越了前后端。adapter 这层让"后端怎么变、前端领域模型不动"成为可能。
4. 端到端走一遍:「创建项目」碰到的每个文件
把前面所有层串起来,看一个真实功能完整的一生:
① [前端 UI] features/projects/ui/create-project-modal.tsx
用户填表单 → 调 useCreateProject()
② [前端 生成层] shared/api/generated/endpoints/projects/projects.ts
useCreateProject() → 经 shared/api/client.ts(注入 token) 发 POST /projects
─────────────────────────── HTTP ───────────────────────────
③ [后端 接口层] modules/project/api/v1/endpoints.py :: create_project()
校验入参 CreateProjectRequest、鉴权拿 user_id
④ [后端 应用层] modules/project/application/project_service.py :: create_project() (写/Command)
⑤ [后端 领域层] modules/project/domain/…(业务规则;project 偏 CRUD 故较薄)
⑥ [后端 基础层] modules/project/infrastructure/repository_impl.py + orm.py(落库)
⑦ [后端 读回] modules/project/application/project_query.py :: get_detail_view() (读/Query)
endpoint 用 BIEResponse.ok(view) 返回
─────────────────────────── HTTP ───────────────────────────
⑧ [前端 缓存] entities/project/api 里 mutation 成功 → invalidate getListProjectsQueryKey
列表自动刷新,UI 回显新项目
💡 看懂这条线,BIE 的整套结构就通了——每个目录都是这条线上的一站。你以后读任何一个功能,都能照这 8 步定位。
5. 约定与铁律速查(前后端对照)
| 维度 | 后端 | 前端 |
|---|---|---|
| 顶层切分 | 按限界上下文 modules/<ctx>/ |
按上下文 entities/<ctx>/ + 能力 features/<ctx>/ |
| 分层 | api / application / domain / infrastructure | app / routes / widgets / features / entities / shared |
| 依赖方向 | api→application→domain←infrastructure;domain 谁都不依赖 | 上层→下层;同层不互依赖 |
| 跨上下文 | 只发领域事件 / 调对方 application;禁 import 对方内部 | feature 间不互依赖;共享走 shared 或 entities |
| 读写 | 写 *_service.py / 读 *_query.py(CQRS) |
mutation hook / query hook |
| 防腐 | DTO 在 api 边界转一次 | shared/api/adapters/*-adapter.ts |
| 契约 | 产出 OpenAPI(源头) | 消费 OpenAPI(orval 生成,不可手改) |
| 边界执法 | ✅ import-linter(CI 强制) | ⚠️ 暂无(建议补 steiger / eslint-plugin-boundaries) |
6. 加一个新功能的分步清单(照着做就行)
假设要加"标签 tag"能力:
后端
- 建模块目录
modules/tag/{api/v1, application, domain, infrastructure}(照抄一个现有模块)。 domain/:写实体/值对象(有业务规则就充血)+ 仓储接口。infrastructure/:写orm.py(PO) +repository_impl.py(实现接口)。application/:写tag_service.py(写) 和tag_query.py(读)。api/v1/:写schemas.py(DTO) +endpoints.py(路由),在app/router.py挂上。- 跑
import-linter+ 测试,确认没越界。 - (如需要)建 alembic 迁移。
契约
8. 启动后端,cd web && npm run openapi:check 看新端点进了 OpenAPI,再 npm run openapi:generate。
前端
9. entities/tag/:model/types.ts + model/tag-adapter.ts(如需)+ api/use-tags.ts(包装生成 hook、定缓存策略)。
10. features/tag/ui/:写交互组件,调 entities/tag/api 的 hook。
11. 在 routes/ 或某个 widgets/ 里接进去。
💡 顺序很重要:永远"后端先定 endpoint → 重生成契约 → 前端再消费"。反过来手写前端接口,就破坏了"契约单一事实源"。
7. 这套的强项与唯一缺口
强项:前后端各自 DDD 分层 + 限界上下文对称 + OpenAPI 契约零漂移 + 全链路类型安全 + 后端边界机器执法。这是同类项目的第一梯队。
唯一明显缺口:前端 FSD 的分层规则没有工具强制(后端有 import-linter,前端靠自觉)。补一个 steiger 或 eslint-plugin-boundaries,把"同层不互依赖、不许跨层、generated 不可手改"写成规则,前端就和后端一样"机器执法边界"了。
可选的进一步加固(见之前讨论):① api 边界加 zod 运行期校验(相信契约→验证契约);② CI 加 OpenAPI 破坏性变更门。
8. 建议的学习路径(拿 BIE 当活教材)
- 先走一遍第 4 节那条线:在 BIE 里从
create-project-modal.tsx一路点到后端endpoints.py,亲手把 8 站走通。 - 再读一个"充血"模块:project 偏 CRUD、domain 薄;去看
modules/memory_kernel(或 auth)的domain/,体会真正的实体/领域服务长啥样。 - 读懂契约缝合:跑一次
npm run openapi:generate,看generated/被整体重写——理解"前端 api 层是后端契约的投影"。 - 照第 6 节清单加一个小功能:哪怕是个最简单的只读列表,前后端各走一遍,这套就真正长在你手上了。
🪞 最后一句:这套模型的精髓不是"目录怎么摆",而是两条不变量——①每一层只依赖该依赖的(边界);②前后端共用一份机器契约(一致)。目录只是这两条不变量的外形。把这两条记牢,换语言换框架都能复刻。
9. 每个设计为什么这样做(决策详解)
前面讲的是"怎么做",这一节讲"为什么这样做更好"。每条都用同一个格式:这样做 → 好在哪 → 不这样会怎样 → 取舍。这是这套模型真正的"内功"——记住理由,比记住目录名重要十倍。
后端的决策
① 顶层按"限界上下文"切,而不是按技术层切
- 好在哪:一个功能的代码集中在
modules/<ctx>/一个文件夹,改动局部化;要拆微服务时整文件夹搬走。 - 不这样会怎样:按技术层切(controllers/ services/ models/ 各一个大目录),加一个功能要在四个目录间反复横跳,且谁都能调谁,时间一长退化成大泥球。
- 取舍:模块多了根目录会长;但这是"良性的长"——长的是业务边界,不是混乱。
② 模块内仍分四层(api/application/domain/infrastructure)
- 好在哪:关注点分离——HTTP 细节、用例编排、业务规则、持久化各管一段;配合依赖倒置,换数据库/换 Web 框架只动两头,中间业务不动。
- 不这样会怎样:业务规则和 SQL、HTTP 参数缠在一起,单测要起数据库、换技术要改业务,牵一发动全身。
- 取舍:小模块会觉得"四层有点重"——所以 CRUD 模块允许 domain 薄、仓储用泛型基类(见⑥⑦)。
③ CQRS:读写服务分家
- 好在哪:读写诉求根本不同。写要保证一致性(加载→改→落库);读要快、要按页面定制形状。
project_query.py直接走 SQL Core、绕开 ORM 水合,避免 Python 层 N+1;写侧则专心编排。 - 不这样会怎样:读写挤在一个 service 里,查列表也得加载完整实体再拼装,慢且代码拧巴;想优化读又怕动到写逻辑。
- 取舍:多一个 query 文件、读写两套 DTO;但换来读写各自能独立优化,值。
④ endpoint 有自己的 DTO,不复用 application 的 schema
- 好在哪:对外契约稳定且可控——内部模型随便重构,只要 endpoint DTO 不变,前端就不受影响;还能在 DTO 层收窄字段,不让内部结构/敏感字段裸奔。
- 不这样会怎样:内部模型直接当返回值,改个内部字段就破坏前端、甚至泄露不该给前端的字段。
- 取舍:多一层 DTO 转换;但这正是"防腐"的代价,必要。
⑤ 所有 endpoint 用统一信封 BIEResponse[T]
- 好在哪:前端拿到的结构永远一致(成功/错误/data 同形状),错误处理、loading、拦截器都能统一写一份。
- 不这样会怎样:每个接口返回形状各异,前端到处写 if 判断,错误处理散落。
- 取舍:成功响应多包一层;可忽略不计。
⑥ 仓储 + 依赖倒置;但 CRUD 模块用泛型 BaseRepository 简化
- 好在哪:领域逻辑不依赖具体数据库(接口隔离);同时对没有复杂规则的 CRUD 模块,泛型基类省掉一堆样板接口。
- 不这样会怎样:不用仓储→业务里直接写 SQL,换库即重写;反过来,给每个 CRUD 模块都硬写一套 domain 接口→纯样板浪费。
- 取舍:“够用就好”——核心域用完整"接口在 domain、实现在 infra",CRUD 域用泛型基类。判断点见⑦。
⑦ domain 允许偏薄(BIE 现状),但核心域必须充血
- 现状诚实说:BIE 的
domain/(如 project)偏贫血,业务逻辑多在 application service——对 CRUD 型模块这没问题。 - 好在哪(务实):没有真实领域规则的模块,强行充血是表演;逻辑放 service 反而直白。
- 不这样会怎样(危险区):有真实规则的核心模块(memory_kernel、计费、风控)如果也让 domain 空着、规则全堆 application,就会出现"领域层被架空"——规则散落、难复用、易被绕过(14 讲反复强调的退化信号)。
- 取舍/判断点:删掉这段逻辑会算错业务/违反规则 → 必须进 domain(充血);只是流程编排 → 留在 application。 按模块重要性分级对待,别一刀切。
⑧ 用 import-linter 把边界做成"机器执法"
- 好在哪:边界从"君子协定"变成"CI 红灯",AI 或队友写歪了立刻被拦——确定性,不靠人自觉。
- 不这样会怎样:分层规则写在文档里没人遵守,半年后 domain 又 import 了 infrastructure,架构悄悄烂掉。
- 取舍:要维护合同;但这是低成本高回报,尤其 AI 代写时代。
⑨ core/shared 不许依赖任何业务模块
- 好在哪:内核保持"纯技术、可复用",不被某个业务的特殊性污染;任何模块都能安全依赖它。
- 不这样会怎样:core 里偷偷 import 了 project,project 就再也拆不走了,内核变成另一个耦合中心。
- 取舍:有些"看起来通用其实属于某业务"的代码要忍住别往 core 放——这恰恰是边界训练。
契约层的决策
⑩ 用 OpenAPI 当单一事实源 + 代码生成前端客户端
- 好在哪:前后端只有一份契约,前端 api 层是它的投影;后端改字段→重生成→前端编译期报错,漂移在编码期被掐死,且全链路类型安全、省掉手写 api 的样板。
- 不这样会怎样:手写 axios + 手维护 TS 类型,后端改字段前端运行时才 400,类型和实际偷偷不一致。
- 取舍:多一道 codegen 工序、强依赖后端 OpenAPI 质量;但这是"前后端分离"真正安全的唯一现实路径。
⑪ generated/ 不可手改
- 好在哪:它是"契约的投影"不是"资产",每次
clean:true全量重生成;手改必被覆盖。坚持这条,前端 api 层就永远和后端一致。 - 不这样会怎样:有人在生成代码里手改一笔,下次重生成被冲掉,或更糟——大家不敢重生成,契约同步就废了。
⑫ 配 msw mock
- 好在哪:后端没写完,前端照契约就能开发联调;契约即 mock。
- 不这样会怎样:前端干等后端、或自己造假数据(还可能和真契约不符)。
前端的决策
⑬ 用 FSD 按上下文切(entities)+ 按能力切(features)
- 好在哪:和后端
modules/对称,前后端同一套限界上下文词汇;依赖单向(上层→下层)、同层不互依赖,内聚可控。 - 不这样会怎样:按"components/pages/utils"技术分类摆,一个业务散落各处,组件互相乱 import 成网。
- 取舍:前期要想清楚"什么是 entity、什么是 feature",有学习成本。
⑭ 三层 api 漏斗:generated 不直接给 UI 用
- 好在哪:中间垫
entities/api+adapter,把"后端变化、缓存策略、模型适配"挡在 features 之外;features 只认前端领域模型。 - 不这样会怎样:features 直接调 generated hook,后端 DTO 一变、几十个组件跟着改;缓存策略散落各组件。
- 取舍:多两层包装;但这正是"防腐"和"可维护"的来源。
⑮ adapter 当前端防腐层(后端 DTO → 前端领域模型)
- 好在哪:后端 DTO 是"传输形状",前端要的是"展示领域模型",两者诉求不同;adapter 负责字段重命名、补前端专用字段(如按 id 哈希取的 color)、屏蔽后端结构变化。
- 不这样会怎样:组件里到处
snake_case、到处兼容后端字段缺失,后端一改全线飘红。 - 取舍:每个上下文多写个 adapter;改后端时只改这一处,回报远大于成本。
⑯ React Query 当唯一"服务端真相源",不往 Zustand 同步
- 好在哪:服务端数据只有一份真相(缓存),多组件并发自动 dedupe、自动失效刷新;避免"store 里一份、请求回来又一份"的双真相不一致。
- 不这样会怎样:把请求结果手动塞进全局 store,就要自己管同步、失效、并发——状态 bug 的重灾区。
- 取舍:要转变"什么都往 store 塞"的习惯;但这是现代前端数据层的共识。
⑰ 横切收敛到 client.ts(统一 mutator)
- 好在哪:鉴权头注入、baseURL、错误归一、token 刷新只写一处,业务代码不碰这些。
- 不这样会怎样:每处请求各自加 token、各自处理 401,重复且容易漏。
全局的决策
⑱ 前后端限界上下文对称(modules ↔ entities)
- 好在哪:同一套业务词汇贯穿前后端(DDD 通用语言);改一个功能,前后端定位代码的心智完全一致。
- 不这样会怎样:后端按业务切、前端按页面切,沟通时"你说的 project 是我哪个组件"对不上。
⑲ 唯一缺口:前端边界还没机器执法
- 现状:后端有 import-linter,前端 FSD 规则靠自觉。
- 为什么该补:FSD 的"同层不互依赖、不许跨层、generated 不可手改"全靠人记,AI 代写时极易破坏。
- 怎么补:加
steiger或eslint-plugin-boundaries,让前端也"机器执法边界",和后端对称。
一句话把所有决策收口
上面 19 条,本质都在服务两条不变量:
- 边界——每层/每模块只依赖该依赖的,且这条由工具强制(后端 import-linter,前端待补 FSD linter)。
- 一致——前后端共用一份机器契约(OpenAPI),用同一套限界上下文词汇。
所有"为什么这样做",最终都能归到这两句:要么是在划清边界,要么是在保证一致。 看懂这一层,你就不是在背 BIE 的目录,而是掌握了一套能迁移到任何项目的判断标准。
