@umacloud/knowledge 1.0.47 → 1.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ ---
2
+ id: stack-typescript-node-engineering-standards
3
+ title: TypeScript + Node.js 后端工程规范(商业级·分层·反屎山)
4
+ domain: development
5
+ category: 01-standards
6
+ difficulty: advanced
7
+ tags: [TypeScript, Node, Nest, Express, 分层, 分包, 命名规范, 反屎山, backend, DTO]
8
+ quality_score: 92
9
+ last_updated: 2026-07-12
10
+ ---
11
+
12
+ # TypeScript + Node.js 后端工程规范(商业级·分层·反屎山)
13
+
14
+ > 本文是语言无关分层方法论在 **TypeScript + Node.js** 服务端(Nest / Express 风格)上的落地细则。商业级后端不是"路由里塞满逻辑能跑通",而是**按 feature 分包、controller 薄、service 承载业务、repository 隔离数据、DTO 与 domain 分离、类型严格、错误结构化**。写接口前先定分层骨架,再填实现。route handler 里直接查库、`any` 满天飞、逻辑塞进 controller、把杂物丢进 `utils/`,都是不合格的。
15
+
16
+ ## 0. 一句话原则
17
+
18
+ **依赖向内:controller 依赖 service,service 依赖 repository 抽象,domain 不依赖任何外设。HTTP、数据库、第三方 SDK 都是可替换的边缘,业务规则居核心。**
19
+
20
+ ## 1. 分层模型与目录结构
21
+
22
+ ```
23
+ HTTP 入口 Controller/Route ─▶ 应用服务 Service ─▶ 仓储 Repository(接口) ─▶ 数据库/ORM
24
+ │(只做 HTTP↔DTO) │(业务编排/事务) │(唯一 SQL/ORM 出口)
25
+ └─ 校验 DTO 入参 └─ 依赖注入拿依赖 └─ 返回 domain/entity
26
+ └─ 调 domain 纯逻辑
27
+ ```
28
+
29
+ - **Controller / Route handler**:只负责 HTTP 边界——解析请求、校验 DTO、调用 service、映射响应状态码。**不写业务逻辑、不碰数据库、不做跨实体编排。**
30
+ - **Service(应用服务)**:承载业务用例、编排多个 repository、管理事务边界、调用 domain 纯逻辑。业务的家在这里。
31
+ - **Repository**:数据访问的**唯一**出口,封装 ORM/SQL;对上暴露接口(`OrderRepository`),对下用具体实现(Prisma/TypeORM/knex)。service 依赖接口而非实现。
32
+ - **Domain / Entity**:领域实体与纯业务规则(计算、状态机、校验),无 IO、无框架依赖、可独立单测。
33
+ - **DTO**:跨越 HTTP 边界的数据形状(入参/出参),与 domain 实体**分离**(见 §2)。
34
+
35
+ 按 **feature 分包**(不按技术类型堆大筐)。每个业务域自带全套分层:
36
+
37
+ ```
38
+ src/
39
+ ├─ modules/ # 按业务域(feature)分包 ← 推荐
40
+ │ ├─ orders/
41
+ │ │ ├─ orders.controller.ts # HTTP 边界,薄
42
+ │ │ ├─ orders.service.ts # 业务用例编排
43
+ │ │ ├─ orders.repository.ts # 数据访问唯一出口
44
+ │ │ ├─ dto/
45
+ │ │ │ ├─ create-order.dto.ts # 入参 DTO + 校验
46
+ │ │ │ └─ order-response.dto.ts # 出参 DTO
47
+ │ │ ├─ entity/
48
+ │ │ │ └─ order.entity.ts # domain 实体 + 业务规则
49
+ │ │ ├─ types.ts # 该域内部类型
50
+ │ │ └─ orders.module.ts # Nest module 装配(Express 用 index.ts 组装)
51
+ │ ├─ payments/
52
+ │ └─ auth/
53
+ ├─ shared/ # 跨域复用:无业务的基础设施
54
+ │ ├─ errors/ # AppError 体系
55
+ │ ├─ middleware/ # 错误中间件、鉴权、日志
56
+ │ ├─ config/ # 环境配置(校验后的强类型)
57
+ │ └─ lib/ # 通用工具(有明确领域归属就别放这)
58
+ └─ main.ts # 应用装配入口
59
+ ```
60
+
61
+ - 跨 feature 只通过对方模块公开出口引用,**禁止深层 import** 对方 repository 内部。
62
+ - Nest 项目用 module 边界;Express 项目按目录 + 显式组装函数,同样保持 feature 边界优先。
63
+ - 中型项目可用 feature + 少量共享层,但 feature 边界优先于技术分层。
64
+
65
+ ## 2. 命名与类型规范
66
+
67
+ **命名约定(一致性本身就是可读性):**
68
+
69
+ | 目标 | 约定 | 例 |
70
+ |---|---|---|
71
+ | 文件名 | kebab-case,带角色后缀 | `create-order.dto.ts`、`orders.service.ts` |
72
+ | 类 / 接口 / 类型 / 枚举 | PascalCase | `OrderService`、`OrderStatus` |
73
+ | 变量 / 函数 / 方法 | camelCase | `createOrder`、`totalAmount` |
74
+ | 常量 / 枚举成员 | UPPER_SNAKE_CASE | `MAX_RETRY`、`OrderStatus.PENDING` |
75
+ | 布尔字段 | is/has/can/should 前缀 | `isPaid`、`hasShipped`、`canCancel` |
76
+ | 时间字段 | 语义 + At/时区显式 | `createdAt`、`expiresAt`(存 UTC ISO) |
77
+ | 金额字段 | 最小单位 + 币种,禁浮点 | `amountCents: number`、`currency: 'USD'` |
78
+
79
+ - **接口不用 `I` 前缀**(`Order` 不是 `IOrder`);`interface` 用于可扩展的对象契约,`type` 用于联合/交叉/工具类型。二者取舍:对象形状默认 `interface`,需要 union/mapped/条件类型时用 `type`。
80
+ - **DTO 与 domain/entity 分离**:DTO 是传输形状(可被外部污染、可选字段多),entity 是业务真相(不变量收紧)。二者用显式映射函数转换,不要图省事复用同一个类型贯穿全栈。
81
+ - **禁 `any`**:外部输入用 `unknown` 接住再收窄。开启 `strict`(含 `strictNullChecks`、`noImplicitAny`)。禁 non-null 断言 `!`(用收窄或显式判空)。导出函数写**显式返回类型**。
82
+
83
+ 正例:
84
+
85
+ ```typescript
86
+ // tsconfig.json: "strict": true, "noUncheckedIndexedAccess": true
87
+
88
+ export const OrderStatus = {
89
+ PENDING: 'pending',
90
+ PAID: 'paid',
91
+ CANCELLED: 'cancelled',
92
+ } as const;
93
+ export type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
94
+
95
+ // domain 实体:不变量收紧
96
+ export interface Order {
97
+ readonly id: string;
98
+ readonly amountCents: number;
99
+ readonly currency: 'USD' | 'EUR';
100
+ readonly status: OrderStatus;
101
+ readonly createdAt: string; // UTC ISO
102
+ }
103
+
104
+ // DTO:传输形状,与 entity 分离
105
+ export interface CreateOrderDto {
106
+ amountCents: number;
107
+ currency: 'USD' | 'EUR';
108
+ }
109
+
110
+ // unknown 收窄,显式返回类型
111
+ function parseAmount(raw: unknown): number {
112
+ if (typeof raw !== 'number' || !Number.isInteger(raw) || raw <= 0) {
113
+ throw new ValidationError('amountCents must be a positive integer');
114
+ }
115
+ return raw;
116
+ }
117
+ ```
118
+
119
+ 反例:
120
+
121
+ ```typescript
122
+ // 反例:any 泛滥、I 前缀、DTO 复用 entity、non-null 断言、隐式返回
123
+ interface IOrder { id; amount: any; created: string } // 隐式 any、字段无类型
124
+
125
+ function handle(body: any) { // any 入参
126
+ const order = body!.order!; // non-null 断言链
127
+ return order.amount * 1.1; // 浮点算金额,隐式返回类型
128
+ }
129
+ ```
130
+
131
+ ## 3. Service 层纪律
132
+
133
+ - **业务逻辑不写在 controller / route handler**:handler 只做 HTTP↔DTO 与状态码,业务下沉到 service。
134
+ - **依赖注入而非 `new` / 全局单例**:service 通过构造函数接收 repository/其他 service(Nest 用 DI 容器,Express 用手动组装/工厂),便于替换与测试。禁止在 service 内部 `new Repository()` 或引用模块级全局单例。
135
+ - **纯函数与副作用隔离**:计算/校验/派生用 domain 纯函数(无 IO),IO(查库、发消息、调外部 API)留在 service 边缘,事务边界显式。
136
+
137
+ 正例:
138
+
139
+ ```typescript
140
+ // controller:薄,只做边界
141
+ @Post()
142
+ async create(@Body() dto: CreateOrderDto): Promise<OrderResponseDto> {
143
+ const order = await this.orders.placeOrder(dto); // 业务全在 service
144
+ return toResponseDto(order);
145
+ }
146
+
147
+ // service:DI 拿依赖,编排业务,调 domain 纯逻辑
148
+ export class OrderService {
149
+ constructor(
150
+ private readonly repo: OrderRepository, // 依赖抽象,注入
151
+ private readonly payments: PaymentGateway,
152
+ ) {}
153
+
154
+ async placeOrder(dto: CreateOrderDto): Promise<Order> {
155
+ const order = createOrder(dto); // domain 纯函数,无 IO
156
+ await this.repo.save(order); // IO 在边缘
157
+ return order;
158
+ }
159
+ }
160
+ ```
161
+
162
+ 反例:
163
+
164
+ ```typescript
165
+ // 反例:逻辑塞进 handler、new 依赖、全局单例、handler 直接查库
166
+ app.post('/orders', async (req, res) => {
167
+ const repo = new OrderRepository(globalDb); // new + 全局单例
168
+ if (req.body.amount > 0 && req.body.currency) { // 业务判断写在 handler
169
+ const row = await globalDb.query('INSERT ...'); // handler 直接碰库
170
+ res.json(row);
171
+ }
172
+ });
173
+ ```
174
+
175
+ ## 4. 反屎山硬规则
176
+
177
+ 违反下列任一条即判不合格,需拆分/重构:
178
+
179
+ - **单文件 ≤ 300–400 行**:超了说明职责过载,按子域拆。
180
+ - **单函数 ≤ 50 行**:超了抽子函数或下沉 domain 逻辑。
181
+ - **圈复杂度 ≤ 10**:分支/循环过多就用早返回、策略表、状态机拆。
182
+ - **参数 ≤ 4**:超过用 options 对象(命名参数),避免"布尔陷阱"位置参数。
183
+ - **禁 god module**:一个 module 什么都管即拆按业务域。
184
+ - **禁 `utils/` / `helpers/` 黑洞**:有明确领域归属的 helper 放进对应 feature;`shared/lib` 只放真正通用、无业务的原语。
185
+ - **禁 barrel 循环依赖**:`index.ts` 只重导出,不产生 A↔B 环;跨 feature 走公开出口。
186
+ - **嵌套 ≤ 3 层**:用 **早返回**(guard clause)压平,别写金字塔 if。
187
+
188
+ 正例(options 对象 + 早返回):
189
+
190
+ ```typescript
191
+ interface SendEmailOptions {
192
+ to: string;
193
+ subject: string;
194
+ body: string;
195
+ cc?: string[];
196
+ replyTo?: string;
197
+ }
198
+
199
+ function sendEmail(opts: SendEmailOptions): Promise<void> { /* ... */ }
200
+
201
+ function priceFor(order: Order): number {
202
+ if (order.status === 'cancelled') return 0; // 早返回,压平嵌套
203
+ if (order.currency !== 'USD') throw new ValidationError('unsupported currency');
204
+ return order.amountCents;
205
+ }
206
+ ```
207
+
208
+ 反例(布尔陷阱 + 深嵌套):
209
+
210
+ ```typescript
211
+ // 反例:位置参数(谁记得第 4 个 true 是啥)、5 层嵌套
212
+ function send(to: string, sub: string, body: string, html: boolean, urgent: boolean) {}
213
+ send('a@x.com', 'Hi', '...', true, false); // 布尔陷阱
214
+
215
+ function price(order: Order): number {
216
+ if (order) { // 金字塔
217
+ if (order.status !== 'cancelled') {
218
+ if (order.currency === 'USD') {
219
+ if (order.amountCents > 0) {
220
+ return order.amountCents;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ return 0;
226
+ }
227
+ ```
228
+
229
+ ## 5. 异步与错误处理
230
+
231
+ - **async/await 一致**:不要混用裸 `.then()` 链与 `await`;并发用 `Promise.all` 而非串行 await。
232
+ - **禁未处理 promise**:每个 `Promise` 要么 `await`、要么显式 `.catch()`、要么 `void` 标注有意 fire-and-forget;开 `no-floating-promises` lint。
233
+ - **错误用 `Error` 子类或 Result,禁 `throw` 字符串**:定义 `AppError` 体系带类型码,便于中间件分类映射 HTTP 状态。
234
+ - **统一错误中间件**:一处集中把 domain 错误映射到状态码 + 结构化响应;handler 里不散落 try/catch 拼 JSON。
235
+ - **不吞错误**:禁空 `catch {}`;至少记录并重抛或转成已知错误。
236
+ - **超时 / 重试 / 取消**:外部调用设超时,幂等操作有限重试(指数退避),长任务支持 `AbortController` 取消。
237
+
238
+ 正例:
239
+
240
+ ```typescript
241
+ export class AppError extends Error {
242
+ constructor(message: string, readonly code: string, readonly status: number) {
243
+ super(message);
244
+ this.name = new.target.name;
245
+ }
246
+ }
247
+ export class ValidationError extends AppError {
248
+ constructor(msg: string) { super(msg, 'VALIDATION', 422); }
249
+ }
250
+ export class NotFoundError extends AppError {
251
+ constructor(msg: string) { super(msg, 'NOT_FOUND', 404); }
252
+ }
253
+
254
+ // 带超时/取消的外部调用
255
+ async function fetchQuote(id: string): Promise<Quote> {
256
+ const ctrl = new AbortController();
257
+ const timer = setTimeout(() => ctrl.abort(), 3000);
258
+ try {
259
+ const res = await fetch(`/quotes/${id}`, { signal: ctrl.signal });
260
+ if (!res.ok) throw new AppError('quote fetch failed', 'UPSTREAM', 502);
261
+ return (await res.json()) as Quote;
262
+ } finally {
263
+ clearTimeout(timer);
264
+ }
265
+ }
266
+
267
+ // 统一错误中间件:一处映射,handler 不拼 JSON
268
+ export function errorMiddleware(err: unknown, _req, res, _next) {
269
+ if (err instanceof AppError) {
270
+ return res.status(err.status).json({ code: err.code, message: err.message });
271
+ }
272
+ logger.error(err); // 未知错误记录后回 500,不泄露栈
273
+ return res.status(500).json({ code: 'INTERNAL', message: 'internal error' });
274
+ }
275
+ ```
276
+
277
+ 反例:
278
+
279
+ ```typescript
280
+ // 反例:throw 字符串、空 catch 吞错、未处理 promise、handler 里拼 JSON
281
+ async function pay(id: string) {
282
+ saveAudit(id); // 未 await 的 promise,静默丢失
283
+ try {
284
+ if (!id) throw 'missing id'; // throw 字符串
285
+ await charge(id);
286
+ } catch (e) {
287
+ // 空 catch:错误被吞,调用方以为成功
288
+ }
289
+ return { ok: true }; // 无超时、无取消、状态不真实
290
+ }
291
+ ```
292
+
293
+ ## 6. 数据与校验
294
+
295
+ - **边界处 DTO 校验**:请求入口用 schema 校验(`zod` 或 `class-validator`),把 `unknown` 转成强类型 DTO;校验失败抛 `ValidationError`。
296
+ - **禁裸 `any` 入库**:写库前经 DTO→entity 映射,字段类型/不变量已收紧。
297
+ - **参数化查询防注入**:一律用参数占位符或 ORM 查询构造器,**禁字符串拼接 SQL**。
298
+ - **N+1 意识**:列表关联数据用 join / `include` / DataLoader 批量取,禁循环里逐条查库。
299
+
300
+ 正例:
301
+
302
+ ```typescript
303
+ import { z } from 'zod';
304
+
305
+ const CreateOrderSchema = z.object({
306
+ amountCents: z.number().int().positive(),
307
+ currency: z.enum(['USD', 'EUR']),
308
+ });
309
+ type CreateOrderDto = z.infer<typeof CreateOrderSchema>;
310
+
311
+ function parseCreateOrder(raw: unknown): CreateOrderDto {
312
+ const r = CreateOrderSchema.safeParse(raw);
313
+ if (!r.success) throw new ValidationError(r.error.message);
314
+ return r.data; // unknown → 强类型
315
+ }
316
+
317
+ // 参数化查询 + 批量取避免 N+1
318
+ const orders = await db.query(
319
+ 'SELECT * FROM orders WHERE user_id = $1', // 占位符,非拼接
320
+ [userId],
321
+ );
322
+ const users = await repo.findByIds(orders.map((o) => o.userId)); // 批量,非循环查
323
+ ```
324
+
325
+ 反例:
326
+
327
+ ```typescript
328
+ // 反例:无校验直接用、字符串拼 SQL(注入)、循环里查库(N+1)
329
+ async function create(raw: any) { // any 未校验
330
+ const sql = `INSERT INTO orders(amount) VALUES('${raw.amount}')`; // 注入
331
+ await db.exec(sql);
332
+ for (const o of orders) {
333
+ o.user = await db.query(`SELECT * FROM users WHERE id=${o.userId}`); // N+1 + 注入
334
+ }
335
+ }
336
+ ```
337
+
338
+ ## 7. 评审清单(可勾选)
339
+
340
+ - [ ] 分层清晰:controller 薄(只做 HTTP↔DTO),业务在 service,数据访问只在 repository,domain 无框架/IO 依赖。
341
+ - [ ] 按 feature 分包(`modules/<feature>/{controller,service,repository,dto,entity}`),跨域不深层 import。
342
+ - [ ] 命名一致:文件 kebab-case、类型 PascalCase、常量 UPPER_SNAKE;布尔 is/has、时间 At(UTC)、金额最小单位整数。
343
+ - [ ] 类型严格:`strict` 开启,无 `any`(用 `unknown` 收窄)、无 non-null `!`、导出函数有显式返回类型;DTO 与 entity 分离。
344
+ - [ ] Service 用依赖注入,不 `new` 依赖、不用全局单例;纯逻辑与副作用隔离。
345
+ - [ ] 反屎山达标:文件 ≤400 行、函数 ≤50 行、圈复杂度 ≤10、参数 ≤4(用 options 对象)、嵌套 ≤3(早返回);无 god module、无 utils 黑洞、无 barrel 环。
346
+ - [ ] 异步安全:async/await 一致、无未处理 promise、错误用 `Error` 子类、统一错误中间件、不吞错误、外部调用有超时/重试/取消。
347
+ - [ ] 数据安全:入口 DTO 校验(zod/class-validator)、无裸 any 入库、参数化查询、无 N+1。
348
+
349
+ ---
350
+ **要点回顾**:controller 薄、service 厚、repository 隔离、domain 纯;DTO≠entity;类型 strict 无 any;错误结构化 + 统一中间件;边界校验 + 参数化查询。这套结构让新增一个业务域是"复制一个 module 骨架填实现",而非在几千行的 route 文件里继续叠屎山。
@@ -0,0 +1,310 @@
1
+ ---
2
+ id: vue3-typescript-engineering-standards
3
+ title: Vue 3 + TypeScript 前端工程规范(商业级·组件分层·反屎山)
4
+ domain: frontend
5
+ category: 01-standards
6
+ difficulty: advanced
7
+ tags: [Vue3, TypeScript, Composition-API, Pinia, 组件分层, 命名规范, 反屎山, frontend]
8
+ quality_score: 92
9
+ last_updated: 2026-07-12
10
+ ---
11
+
12
+ # Vue 3 + TypeScript 前端工程规范(商业级·组件分层·反屎山)
13
+
14
+ > 这份规范只讲 **Vue 3 + TypeScript** 的落地细节,是"前端架构与分层标准"在 Vue 技术栈上的具体化。分层模型、三类状态分治、API 层隔离、feature-based 分包这些**框架无关的骨架**不在这里重复;本文只回答:SFC 该怎么切、组件怎么命名、逻辑怎么抽 composable、Pinia 边界画在哪、TS 纪律怎么守、屎山怎么防。默认 `<script setup lang="ts">` + Composition API + `strict` TS。
15
+
16
+ ## 0. 一句话原则
17
+
18
+ **一个 SFC 只做一件事:容器管数据、展示纯渲染;可复用逻辑进 composable,可复用状态进 store;类型显式、props 单向、组件短小。** 超过体量阈值不是"写复杂了",是"该拆了"。
19
+
20
+ ## 1. 组件分层与目录结构
21
+
22
+ 四类组件,职责边界不许含糊:
23
+
24
+ | 层 | 位置 | 职责 | 禁止 |
25
+ |---|---|---|---|
26
+ | 页面 | `features/<x>/views/` | 路由落点、取数、编排、组装区块 | 写复杂展示细节 |
27
+ | 业务组件 | `features/<x>/components/` | 绑定某业务域的可复用块(订单卡、审批栏) | 跨域复用、裸取数 |
28
+ | 通用 UI | `components/ui/` 或 `shared/ui/` | 无业务的纯展示件(Button/Modal/Table) | import 任何 store/api |
29
+ | 布局 | `layouts/` | 页面骨架(侧栏/顶栏/插槽),不含业务 | 取业务数据 |
30
+
31
+ **按业务域分包(feature-based),不按文件类型分包。** 每个 feature 自带全套分层,对外只经 `index.ts` 暴露:
32
+
33
+ ```
34
+ src/features/order/
35
+ ├─ views/ OrderListView.vue OrderDetailView.vue # 页面(容器)
36
+ ├─ components/ OrderCard.vue OrderStatusTag.vue # 业务展示组件
37
+ ├─ composables/ useOrderList.ts useOrderActions.ts # 逻辑
38
+ ├─ api/ order.api.ts # 该域唯一数据出口
39
+ ├─ store/ useOrderStore.ts # 跨组件的域内状态(如需)
40
+ ├─ types/ order.ts # DTO / 视图模型
41
+ └─ index.ts # 对外 barrel:只导出 views 与公开类型
42
+ ```
43
+
44
+ **容器 vs 展示** —— 页面是容器,负责"数据从哪来、事件往哪去";展示组件只吃 props、抛事件。
45
+
46
+ 正例(展示组件:纯 props → UI,零副作用、零取数):
47
+
48
+ ```vue
49
+ <!-- OrderCard.vue:展示组件 -->
50
+ <script setup lang="ts">
51
+ import type { Order } from '../types/order'
52
+ defineProps<{ order: Order }>()
53
+ const emit = defineEmits<{ (e: 'cancel', id: string): void }>()
54
+ </script>
55
+
56
+ <template>
57
+ <article class="order-card">
58
+ <h3>{{ order.title }}</h3>
59
+ <OrderStatusTag :status="order.status" />
60
+ <button @click="emit('cancel', order.id)">取消</button>
61
+ </article>
62
+ </template>
63
+ ```
64
+
65
+ 正例(页面:容器只编排,逻辑委托给 composable):
66
+
67
+ ```vue
68
+ <!-- OrderListView.vue:容器 -->
69
+ <script setup lang="ts">
70
+ import { useOrderList } from '../composables/useOrderList'
71
+ import OrderCard from '../components/OrderCard.vue'
72
+ const { orders, isLoading, error, cancel } = useOrderList()
73
+ </script>
74
+
75
+ <template>
76
+ <StateBoundary :loading="isLoading" :error="error" :empty="!orders.length">
77
+ <OrderCard v-for="o in orders" :key="o.id" :order="o" @cancel="cancel" />
78
+ </StateBoundary>
79
+ </template>
80
+ ```
81
+
82
+ 反例(把展示组件写成"什么都干"的巨石:自己取数、自己算、自己渲染,无法复用):
83
+
84
+ ```vue
85
+ <!-- 反例:OrderCard 里裸 fetch + 业务分支,展示组件被污染 -->
86
+ <script setup lang="ts">
87
+ import axios from 'axios'
88
+ const props = defineProps<{ id: string }>()
89
+ const order = ref<any>(null) // any + 组件内取数
90
+ onMounted(async () => { order.value = (await axios.get('/api/order/' + props.id)).data })
91
+ </script>
92
+ ```
93
+
94
+ ## 2. 命名规范
95
+
96
+ | 对象 | 规则 | 正例 | 反例 |
97
+ |---|---|---|---|
98
+ | 组件名 | PascalCase,**多单词**避免与 HTML 冲突 | `UserCard` `OrderStatusTag` | `Card` `Table`(单词) |
99
+ | 文件名 | 与组件名一致 | `UserCard.vue` | `user-card.vue` 内含 `UserCard` |
100
+ | props 声明 | camelCase | `defineProps<{ maxCount: number }>()` | `max_count` |
101
+ | props 模板传入 | kebab-case | `<UserCard :max-count="10" />` | `:maxCount` |
102
+ | 事件 | kebab / `update:xxx`(配 v-model) | `@row-click` `update:modelValue` | `@rowClick` |
103
+ | composable | `useXxx`,返回响应式 | `useOrderList` | `getOrderData` |
104
+ | store | `useXxxStore` | `useOrderStore` | `orderState` |
105
+ | 布尔字段 | is/has/can/should 前缀 | `isLoading` `hasError` `canEdit` | `loading`(歧义) |
106
+ | 时间字段 | `xxxAt`(时刻) / `xxxMs`(时长) | `createdAt` `timeoutMs` | `time` `date` |
107
+ | ref/reactive | 名词,值语义清晰;DOM ref 加 `El` | `count` `formEl` | `data` `temp` `flag` |
108
+
109
+ 命名冲突要点:模板里 `<Order />` 可能被误判为原生元素,业务组件一律用 PascalCase 多单词名。事件用 `update:modelValue` 才能配合 `v-model`。
110
+
111
+ ## 3. Composition API 纪律
112
+
113
+ **逻辑复用只用 composable,不用 mixin。** mixin 来源不清、命名易撞、类型难追;composable 是显式入参、显式返回、可组合、可单测。
114
+
115
+ 正例(把取数/加载/动作收进一个 composable,页面只解构):
116
+
117
+ ```ts
118
+ // composables/useOrderList.ts
119
+ import { ref, onMounted } from 'vue'
120
+ import { fetchOrders, cancelOrder } from '../api/order.api'
121
+ import type { Order } from '../types/order'
122
+
123
+ export function useOrderList() {
124
+ const orders = ref<Order[]>([])
125
+ const isLoading = ref(false)
126
+ const error = ref<Error | null>(null)
127
+
128
+ async function load() {
129
+ isLoading.value = true
130
+ error.value = null
131
+ try { orders.value = await fetchOrders() }
132
+ catch (e) { error.value = e as Error }
133
+ finally { isLoading.value = false }
134
+ }
135
+ async function cancel(id: string) { await cancelOrder(id); await load() }
136
+
137
+ onMounted(load)
138
+ return { orders, isLoading, error, cancel, reload: load }
139
+ }
140
+ ```
141
+
142
+ **props / emits 必须显式泛型类型**,用 `withDefaults` 给默认值:
143
+
144
+ ```ts
145
+ interface Props { size?: 'sm' | 'md' | 'lg'; disabled?: boolean }
146
+ const props = withDefaults(defineProps<Props>(), { size: 'md', disabled: false })
147
+ const emit = defineEmits<{ (e: 'change', value: string): void }>()
148
+ ```
149
+
150
+ **props 单向,禁止在子组件里直接改 props。** 要改就 `emit` 让父级改,或本地拷贝派生:
151
+
152
+ ```ts
153
+ // 正例:v-model 双向由父级 emit 承接
154
+ const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>()
155
+ const props = defineProps<{ modelValue: string }>()
156
+ function onInput(v: string) { emit('update:modelValue', v) }
157
+ ```
158
+
159
+ ```ts
160
+ // 反例:直接改 props —— 破坏单向流,触发警告,行为不可预测
161
+ const props = defineProps<{ count: number }>()
162
+ function inc() { props.count++ } // 禁止
163
+ ```
164
+
165
+ **巨型 setup 拆分。** 一个 `<script setup>` 里堆了取数 + 表单 + 分页 + 轮询就该按关注点拆成 `useXxx`。判断标准:setup 逻辑超过约 50 行,或出现三个以上互不相干的关注点,立即抽。
166
+
167
+ ## 4. 状态管理边界
168
+
169
+ 三层归属,别把一切塞全局 store:
170
+
171
+ - **组件本地 `ref`/`reactive`**:只服务当前组件的开关、输入、hover。
172
+ - **域内 Pinia store**:跨该 feature 多个组件共享的状态(当前选中、筛选条件、购物车草稿)。
173
+ - **服务端数据**:交给取数缓存层管 loading/error/失效,不要手动搬进 store 长期维护。
174
+
175
+ Pinia 规范:**按域拆 store,异步放 action,getter 纯计算,组件不散落 API 调用**。
176
+
177
+ 正例:
178
+
179
+ ```ts
180
+ // store/useOrderStore.ts
181
+ import { defineStore } from 'pinia'
182
+ import { fetchOrders } from '../api/order.api'
183
+ import type { Order } from '../types/order'
184
+
185
+ export const useOrderStore = defineStore('order', {
186
+ state: () => ({ list: [] as Order[], keyword: '' }),
187
+ getters: {
188
+ // 纯计算,无副作用
189
+ visible: (s): Order[] => s.list.filter(o => o.title.includes(s.keyword)),
190
+ },
191
+ actions: {
192
+ // 异步收在 action,组件不碰 api
193
+ async load() { this.list = await fetchOrders() },
194
+ },
195
+ })
196
+ ```
197
+
198
+ 反例(把 UI 局部态塞全局 + 在组件里散落 fetch + getter 带副作用):
199
+
200
+ ```ts
201
+ // 反例:一个 god store 装下所有页面的 modal 开关、输入框值、请求结果
202
+ export const useAppStore = defineStore('app', {
203
+ state: () => ({ orderModalOpen: false, searchInput: '', orders: [], userProfileTab: 0 }),
204
+ getters: { orders: (s) => { fetch('/api/order'); return s.orders } }, // getter 里取数,禁止
205
+ })
206
+ ```
207
+
208
+ 组件里也**禁止裸调 API**——请求一律经 `api/` 层,再由 composable 或 store action 调用:
209
+
210
+ ```ts
211
+ // 反例:组件 <script setup> 里直接 axios.get('/api/...')
212
+ // 正例:组件 → useOrderList() → order.api.ts → 后端
213
+ ```
214
+
215
+ ## 5. TypeScript 纪律
216
+
217
+ - **`strict: true` 全开**(`strictNullChecks` / `noImplicitAny` 生效)。
218
+ - **禁 `any`**:不确定用 `unknown` 再收窄,通用逻辑用泛型。
219
+ - **API 响应显式建模**:每个接口有 DTO 类型,不让 `any` 从网络边界渗进业务。
220
+ - **禁滥用 `as`**:`as` 是绕过类型检查,只在类型断言确有把握(如 `as const`、DOM 精确类型)时用;用它去压 TS 报错就是埋雷。
221
+ - **前端请求 URL 与后端路由对齐**:路径集中为常量/枚举,配合类型化 client,改一处即全量对齐。
222
+
223
+ 正例:
224
+
225
+ ```ts
226
+ // types/order.ts
227
+ export interface OrderDTO { id: string; title: string; status: 'open' | 'paid' | 'closed'; createdAt: string }
228
+
229
+ // api/order.api.ts —— 路径集中、响应类型化、无 any
230
+ const ORDER_API = { list: '/api/orders', cancel: (id: string) => `/api/orders/${id}/cancel` } as const
231
+
232
+ export async function fetchOrders(): Promise<OrderDTO[]> {
233
+ const res = await http.get<OrderDTO[]>(ORDER_API.list)
234
+ return res.data
235
+ }
236
+ ```
237
+
238
+ 反例:
239
+
240
+ ```ts
241
+ // 反例:any 从网络边界扩散 + as 强压 + URL 硬编码散落
242
+ async function load() {
243
+ const res: any = await axios.get('/api/oreders') // any + 拼错的路径无人报错
244
+ list.value = res.data as Order[] // as 强转掩盖字段不匹配
245
+ }
246
+ ```
247
+
248
+ 收窄 `unknown` 的正确姿势:
249
+
250
+ ```ts
251
+ function isOrder(x: unknown): x is OrderDTO {
252
+ return typeof x === 'object' && x !== null && 'id' in x && 'status' in x
253
+ }
254
+ ```
255
+
256
+ ## 6. 反屎山硬规则(触犯即打回)
257
+
258
+ - **单 SFC ≤ 300–400 行**:超了按区块拆子组件、按关注点抽 composable。
259
+ - **单函数 ≤ 50 行**:分支多就拆纯函数。
260
+ - **同一元素禁止 `v-if` + `v-for` 并用**(优先级歧义 + 每轮都判断):先 `computed` 过滤再 `v-for`。
261
+ - **template 嵌套过深要拆**:三层以上条件/循环嵌套抽子组件。
262
+ - **禁 god component**:又取数又算逻辑又渲染又管弹窗的巨石组件。
263
+ - **禁 `utils/` 黑洞**:和某业务相关的 helper 放进该 feature,别丢进全局 `utils` 大筐。
264
+ - **样式走 design token**:颜色/间距/圆角用 CSS 变量或 token,禁硬编码 hex。
265
+ - **禁 emoji 当图标/占位**:功能图标一律来自声明的图标库(组件形式),emoji 不是图标。
266
+
267
+ 正例(先 computed 过滤,再 v-for,无 v-if 混用):
268
+
269
+ ```vue
270
+ <script setup lang="ts">
271
+ const activeItems = computed(() => items.value.filter(i => i.active))
272
+ </script>
273
+ <template>
274
+ <ItemRow v-for="i in activeItems" :key="i.id" :item="i" />
275
+ </template>
276
+ ```
277
+
278
+ 反例(同元素 v-if + v-for + 硬编码色 + emoji 图标):
279
+
280
+ ```vue
281
+ <template>
282
+ <!-- v-if 与 v-for 同元素:语义歧义、性能差 -->
283
+ <li v-for="i in items" v-if="i.active" :key="i.id" :style="{ color: '#8b5cf6' }">
284
+ {{ i.name }} 收藏 <!-- 此处直接塞 emoji 字符当收藏图标 + 硬编码紫色 -->
285
+ </li>
286
+ </template>
287
+ ```
288
+
289
+ 正例(token 色 + 图标库组件):
290
+
291
+ ```vue
292
+ <template>
293
+ <li :style="{ color: 'var(--color-text-primary)' }">
294
+ {{ i.name }} <StarIcon class="icon" /> <!-- 来自图标库的组件,非 emoji -->
295
+ </li>
296
+ </template>
297
+ ```
298
+
299
+ ## 7. 评审清单(可勾选)
300
+
301
+ - [ ] 组件按容器/展示分离:页面取数编排,展示组件只吃 props、抛事件、零副作用。
302
+ - [ ] 按业务域 feature-based 分包,每个 feature 自带 views/components/composables/api/store/types,对外只经 `index.ts`。
303
+ - [ ] 组件 PascalCase 多单词、文件名一致;props camelCase 声明 / kebab 模板;事件 kebab 或 `update:xxx`;composable `useXxx`、store `useXxxStore`。
304
+ - [ ] 逻辑复用用 composable 而非 mixin;props/emits 显式泛型类型;props 单向,无子组件直改 props。
305
+ - [ ] 无巨型 setup(超 ~50 行或多关注点已拆 composable)。
306
+ - [ ] 状态归属正确:UI 局部态用 ref,域内共享用 Pinia,服务端数据交给缓存层;store 按域拆、异步在 action、getter 纯计算。
307
+ - [ ] 组件内无裸 fetch/axios,请求全部经 `api/` 层。
308
+ - [ ] `strict` 开启;无 `any`(用 unknown/泛型);API 响应有 DTO 类型;无滥用 `as`;URL 集中常量、与后端路由对齐。
309
+ - [ ] 单 SFC ≤ 300–400 行、单函数 ≤ 50 行;无同元素 v-if+v-for;无 god component、无 utils 黑洞。
310
+ - [ ] 颜色/间距走 design token,无硬编码色值;功能图标来自图标库,无 emoji 当图标。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umacloud/knowledge",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "description": "UmaDev curated engineering knowledge corpus (standards, methodologies, expert playbooks, design systems, miniprogram/uniapp guides). Platform-independent data shipped once so npm users get the full KB offline.",
5
5
  "license": "MIT",
6
6
  "repository": { "type": "git", "url": "https://github.com/umacloud/umadev.git" },