返回首页

开发经验

分类:全栈框架
发布于:
阅读时间:40 分钟
  1. 路由冲突 路由 /keyVList/:id 在某些框架(比如 Hono、Express、Fastify 等)中可能因为路由匹配顺序或通配规则,导致 /keyVList 被误认为是 /:id 的一个参数(比如 id = "keyVList"

这是一个常见的路由冲突问题,根源在于:

  • /:id 是一个动态路径参数路由,它会匹配任意单段路径(比如 /123/abc/keyVList)。
  • 如果你先注册了 /:id,再注册 /keyVList,那么 /keyVList 可能永远无法被命中,因为 /:id 已经先匹配了它。
<!-- 嵌入内容: PixPin_2025-10-27_10-09-05.png -->

// 属性相关接口统一前缀 
.get("/attributes/sale", ...) // 原 /keyVList 
.get("/attributes/:id", ...) // 原 /:id

2.类型复用

你提到的这些术语(tableformquerycreatepatch全量更新组合 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 接口,不要混用!


🔄 四、典型业务流程示例(商品管理)

  1. 进入列表页

    • 调用 queryGET /products?page=1&size=10
    • table 展示结果
  2. 点击“新建”

    • 打开空 form
    • 用户填写(含 组合 select 选规格)
    • 提交 → create(POST /products)
  3. 点击“编辑”

    • 调用 query 获取详情:GET /products/123
    • 回填 form
    • 用户修改后提交:
      • 若改全部 → PUT /products/123
      • 若只改价格 → PATCH /products/123 + { price: 99 }

✅ 五、最佳实践建议

  1. 新建用 POST(create)
  2. 编辑优先用 PATCH(部分更新),除非业务要求全量覆盖
  3. 表单类型 ≠ 查询类型,分开定义
  4. 组合 select 的值 最终应转为简单对象(如 { color: "blue" })再提交
  5. table 的操作列 可直接触发 PATCH(如开关、状态切换)

📌 总结一句话

tableform 是前端 UI 的两种形态,分别对应 query 查询 和 create/update 操作;而 全量更新(PUT)与 部分更新(PATCH)是更新数据的两种策略,应根据业务场景选择;组合 selectform 中处理复杂属性的组件,其值需按后端要求格式化后用于 createupdate

如果你有具体业务场景(比如 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 被多个地方引用

这样设计后:

  • 修改一个字段(如 pricenumber 改为 string),所有相关类型自动更新
  • 表单、API、表格类型一致,减少 bug
  • 新成员接手项目,类型即文档

如果你有具体的实体(比如 SKU、订单、用户),我可以帮你写出完整的类型复用方案!