开发经验
- 路由冲突
路由
/keyVList和/:id在某些框架(比如 Hono、Express、Fastify 等)中可能因为路由匹配顺序或通配规则,导致/keyVList被误认为是/:id的一个参数(比如id = "keyVList")。
这是一个常见的路由冲突问题,根源在于:
/:id是一个动态路径参数路由,它会匹配任意单段路径(比如/123、/abc、/keyVList)。- 如果你先注册了
/:id,再注册/keyVList,那么/keyVList可能永远无法被命中,因为/:id已经先匹配了它。
// 属性相关接口统一前缀
.get("/attributes/sale", ...) // 原 /keyVList
.get("/attributes/:id", ...) // 原 /:id
2.类型复用
你提到的这些术语(table、form、query、create、patch、全量更新、组合 select、表单类型、部分更新)其实是 前端与后端交互中围绕“数据操作”和“UI 表现” 的核心概念。下面我们系统性地梳理它们之间的关系,并用一张逻辑图帮你理解。
🧩 一、按角色分类
| 类别 | 概念 | 说明 |
|---|---|---|
| UI 层(前端界面) | table(表格) <br>form(表单) <br>组合 select | 用户看到和操作的界面组件 |
| 数据操作语义(CRUD) | create(创建) <br>全量更新(PUT) <br>部分更新(PATCH) <br>query(查询) | 对数据的增删改查行为 |
| 数据结构/类型 | 表单类型 <br>查询类型 | TypeScript/接口中定义的数据结构 |
🔗 二、核心关系图(文字版)
用户操作
│
├─ 查看列表 → 使用 table 展示 ← query(查询 API)
│
└─ 编辑/新建 → 使用 form 表单 ← 表单类型(TS 接口)
│
├─ 新建 → 提交 create 请求(POST)
│
├─ 修改(全量)→ 提交 PUT(全量更新)
│
└─ 修改(部分)→ 提交 PATCH(部分更新,如只改价格)
│
└─ form 中可能包含 组合 select(如规格选择器)
📚 三、逐项详解与关联
1. Table(表格)
- 用途:展示数据列表(如商品列表、用户列表)
- 数据来源:调用
query接口(如GET /api/products) - 关联:
- 表格每行数据 ≈ 一个“资源对象”
- 点击“编辑”按钮 → 跳转到
form,带入该行数据
2. Form(表单)
-
用途:创建或编辑数据
-
数据结构:由 表单类型(Form Type) 定义(TypeScript interface)
interface ProductForm { name: string; price: number; specs: Record<string, string>; // 如 { color: "blue" } } -
关联操作:
- 新建 → 调用
create(POST) - 编辑 → 调用
全量更新(PUT)或部分更新(PATCH)
- 新建 → 调用
3. Query(查询)
-
HTTP 方法:
GET -
用途:获取数据列表(用于
table)或详情(用于form回显) -
数据结构:由 查询类型(Query Type) 定义
interface ProductQuery { page: number; size: number; keyword?: string; }
4. Create(创建)
- HTTP 方法:
POST - 触发场景:用户在
form中填写完数据,点击“提交” - 数据格式:使用 表单类型 的结构
- 与更新的区别:无 ID,后端生成主键
5. 全量更新 vs 部分更新
| 对比项 | 全量更新(PUT) | 部分更新(PATCH) |
|---|---|---|
| 语义 | 替换整个资源 | 只修改部分字段 |
| 前端场景 | 表单完整提交(所有字段) | 只改某个字段(如“上架/下架”开关) |
| 数据要求 | 必须传所有必填字段 | 只传要改的字段 |
| 安全性 | 容易覆盖他人修改 | 更安全,适合并发 |
| 示例 | 编辑商品详情页提交 | 表格中直接改“价格”或“库存” |
✅ 现代前端更倾向用 PATCH 做部分更新,避免不必要的数据传输和覆盖风险。
6. 组合 Select(组合下拉选择器)
- 用途:在
form中选择复杂属性(如“颜色+尺寸”规格) - 数据来源:通常由
query接口返回规格属性(如你之前问的saleAttrsWithValues) - 输出格式:常生成
specJson: { color: "blue", size: "32G" } - 关联:属于
form的一部分,影响create/update的 payload
7. 表单类型(Form Type) vs 查询类型(Query Type)
| 类型 | 用途 | 字段特点 |
|---|---|---|
| 表单类型 | 用于创建/编辑 | 包含所有可编辑字段,可能有默认值 |
| 查询类型 | 用于搜索/分页 | 包含过滤条件(keyword, status, page 等) |
💡 它们通常是不同的 TypeScript 接口,不要混用!
🔄 四、典型业务流程示例(商品管理)
-
进入列表页
- 调用
query:GET /products?page=1&size=10 - 用
table展示结果
- 调用
-
点击“新建”
- 打开空
form - 用户填写(含
组合 select选规格) - 提交 →
create(POST /products)
- 打开空
-
点击“编辑”
- 调用
query获取详情:GET /products/123 - 回填
form - 用户修改后提交:
- 若改全部 →
PUT /products/123 - 若只改价格 →
PATCH /products/123+{ price: 99 }
- 若改全部 →
- 调用
✅ 五、最佳实践建议
- 新建用 POST(create)
- 编辑优先用 PATCH(部分更新),除非业务要求全量覆盖
- 表单类型 ≠ 查询类型,分开定义
- 组合 select 的值 最终应转为简单对象(如
{ color: "blue" })再提交 - table 的操作列 可直接触发 PATCH(如开关、状态切换)
📌 总结一句话
table和form是前端 UI 的两种形态,分别对应query查询 和create/update操作;而全量更新(PUT)与部分更新(PATCH)是更新数据的两种策略,应根据业务场景选择;组合 select是form中处理复杂属性的组件,其值需按后端要求格式化后用于create或update。
如果你有具体业务场景(比如 SKU 管理、用户编辑),我可以给出更针对性的代码结构建议!
太好了!你关注的是 “类型复用” ——这是现代前端工程(尤其是 TypeScript + Vue/React)中提升可维护性、减少重复、保证前后端一致性的核心思想。
我们围绕你提到的场景(table / form / query / create / patch / 全量更新 / 部分更新 / 组合 select),来系统性地思考 如何设计可复用的类型体系。
🎯 核心目标
用最少的类型定义,覆盖所有场景(查询、创建、编辑、表格展示、部分更新),避免重复、保证一致性、支持类型安全。
🧱 一、基础实体类型(Single Source of Truth)
首先,定义你的领域实体(比如 Product)的完整结构:
// types/product.ts
// 1. 基础属性(后端返回的完整对象)
export interface Product {
id: string;
name: string;
price: number;
stock: number;
status: 'active' | 'inactive';
specJson: Record<string, string>; // { color: "blue", size: "32G" }
createdAt: string;
updatedAt: string;
}
✅ 这是唯一权威的实体定义,所有其他类型都基于它派生。
🔁 二、派生类型:按场景复用
1. 表单类型(Form) → 用于 create / edit
- 去掉只读字段(如
id,createdAt) - 可选:某些字段在新建时非必填
// 从 Product 剔除只读字段
export type ProductForm = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;
// 或更精确:新建时 stock 可为空
export interface ProductCreateForm extends Omit<ProductForm, 'stock'> {
stock?: number;
}
✅ 复用
Product的字段定义,避免重复写name: string; price: number; ...
2. 创建请求体(Create Payload)
通常和 ProductForm 一致,但可进一步约束:
// POST /products 的请求体
export type CreateProductDTO = ProductForm;
3. 全量更新(PUT)请求体
和 CreateProductDTO 几乎一样,但可能要求更严格(如 stock 必填):
// PUT /products/:id
export type UpdateProductDTO = ProductForm; // 或根据业务调整
4. 部分更新(PATCH)请求体
关键:只包含可选字段!
// PATCH /products/:id
export type PatchProductDTO = Partial<ProductForm>;
✅ Partial<T> 是 TypeScript 内置工具类型,自动让所有字段变为可选。
例如:
{ price: 99 }或{ status: 'inactive' }都合法。
5. 查询参数类型(Query Params)
用于 table 的搜索、分页,和实体结构无关,单独定义:
export interface ProductQuery {
keyword?: string;
status?: 'active' | 'inactive';
page: number;
size: number;
}
❌ 不要试图从
Product派生!查询条件 ≠ 实体字段。
6. 表格展示类型(Table Row)
通常直接用 Product,因为 table 展示的是完整数据:
// table 的 dataSource: Product[]
但如果表格只展示部分字段,可定义轻量视图:
export type ProductTableRow = Pick<Product, 'id' | 'name' | 'price' | 'status'>;
7. 组合 Select 的值类型
你提到的 specJson 结构:
// 规格选择器的内部值
export type SpecSelection = Record<string, string>; // { color: "blue", size: "32G" }
// 它已经是 Product 的一部分,无需重复定义
✅ 在 form 中使用
SpecSelection,提交时直接作为specJson字段。
🔄 三、类型关系图(文字版)
┌──────────────┐
│ Product │ ←─ 后端完整实体(权威)
└──────┬───────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌───────────────┐
│ ProductForm │ │ProductTableRow│ │SpecSelection │
└──────┬──────┘ └──────────────┘ └───────────────┘
│
┌───┴────────────┐
▼ ▼
CreateProductDTO UpdateProductDTO
│
▼
PatchProductDTO = Partial<UpdateProductDTO>
🛠️ 四、实战:API 层如何复用
// api/product.ts
import type {
Product,
ProductQuery,
CreateProductDTO,
PatchProductDTO
} from '@/types/product';
// 查询列表
export const getProducts = (params: ProductQuery): Promise<Product[]> => { ... }
// 创建
export const createProduct = (data: CreateProductDTO): Promise<Product> => { ... }
// 部分更新
export const patchProduct = (id: string, data: PatchProductDTO): Promise<Product> => { ... }
✅ 前端调用时自动获得类型提示和校验!
💡 五、高级技巧:用映射类型进一步抽象
如果多个实体(Product / User / Order)都有类似模式,可抽象通用类型:
// 通用实体基类
interface BaseEntity {
id: string;
createdAt: string;
updatedAt: string;
}
// 通用表单类型
type EntityForm<T extends BaseEntity> = Omit<T, keyof BaseEntity>;
// 通用部分更新
type EntityPatch<T> = Partial<EntityForm<T>>;
但初期不建议过度抽象,先保证业务清晰。
✅ 六、总结:类型复用的核心原则
| 原则 | 说明 |
|---|---|
| 单一权威源 | 实体类型(如 Product)只定义一次 |
| 派生而非重复 | 用 Omit / Pick / Partial 派生场景类型 |
| 区分读写模型 | 查询参数 ≠ 实体字段;表单字段 ≠ 表格字段 |
| DTO 明确命名 | CreateXXXDTO / PatchXXXDTO 清晰表达用途 |
| 组合类型复用 | 如 SpecSelection 被多个地方引用 |
这样设计后:
- 修改一个字段(如
price从number改为string),所有相关类型自动更新 - 表单、API、表格类型一致,减少 bug
- 新成员接手项目,类型即文档
如果你有具体的实体(比如 SKU、订单、用户),我可以帮你写出完整的类型复用方案!