界面架构设计:组件树、模块划分与接口契约

界面移植这件事,很多人一上来就开干——复制代码、改样式、调接口。结果呢?改到一半发现结构乱了,模块之间互相牵扯,一个按钮的改动能引发五个页面的连锁报错。我见过太多这样的项目了。

说白了,移植不是体力活,是设计活。你得先想清楚:组件怎么组织?模块怎么切分?接口怎么约定?这三件事定下来,后面的工作就是按图索骥。

组件树设计:从页面到原子的分层

组件树,就是你的界面结构地图。我习惯从两个维度去画它:层级关系和复用粒度。

举个例子,一个后台管理系统的用户列表页,它的组件树可能是这样的:

UserManagementPage
├── PageHeader
│   ├── Breadcrumb
│   └── ActionBar
│       ├── SearchInput
│       ├── FilterDropdown
│       └── CreateButton
├── DataTable
│   ├── TableHeader
│   ├── TableRow (复用)
│   │   ├── AvatarCell
│   │   ├── NameCell
│   │   ├── StatusBadge
│   │   └── ActionCell
│   │       ├── EditButton
│   │       └── DeleteButton
│   └── Pagination
└── Modal (条件渲染)
    ├── FormGroup
    │   ├── InputField
    │   ├── SelectField
    │   └── DatePicker
    └── ModalFooter

注意看,TableRow 是复用的核心节点。我在项目中遇到过一种情况:团队把每一行都写成一个独立组件,结果列表渲染时性能直接崩了。后来改成 TableRow 统一管理,配合 key 优化,问题就解决了。

我的经验:组件树的深度建议控制在 3-4 层。太深了,props 传递会变成噩梦;太浅了,组件会变得臃肿。你想想看,一个组件超过 200 行代码,是不是该拆了?

模块划分原则:高内聚、低耦合

模块划分,说白了就是「把相关的放一起,不相关的隔开」。我常用的原则有三个:

  1. 业务边界原则——同一个业务功能的所有组件、逻辑、样式放在一个模块里。比如「用户管理」模块,就包含列表、表单、详情、权限控制等。
  2. 复用频率原则——被多处引用的基础组件(按钮、输入框、弹窗)提升到公共模块。我在一个项目中统计过,Button 组件被引用了 47 次,这种组件必须统一管理。
  3. 变更隔离原则——容易变化的部分(如第三方 SDK 封装、业务配置)单独成模块。这样改的时候不会波及全局。

来看一个典型的模块划分结构:

src/
├── modules/
│   ├── user/              # 用户模块
│   │   ├── components/    # 用户相关组件
│   │   ├── hooks/         # 用户相关逻辑
│   │   ├── services/      # 用户相关 API
│   │   └── index.tsx      # 模块入口
│   ├── order/             # 订单模块
│   └── dashboard/         # 仪表盘模块
├── shared/                # 公共模块
│   ├── components/        # 基础组件
│   ├── utils/             # 工具函数
│   ├── hooks/             # 通用 hooks
│   └── styles/            # 全局样式
└── core/                  # 核心模块
    ├── router/            # 路由配置
    ├── store/             # 状态管理
    └── i18n/              # 国际化
避坑提醒:我曾经见过一个项目,把所有的 API 调用都写在一个 api.ts 文件里,结果那个文件超过 3000 行。每次改接口都得全局搜索,改完还不敢提交。模块划分一定要按业务领域来,别偷懒。

接口契约定义:让前后端「说同一种语言」

接口契约,是移植过程中最容易出问题的地方。我习惯在项目启动时就定义好一份 API 契约文档,包含以下内容:

字段 说明 示例
接口路径 RESTful 风格,版本号明确 /api/v1/users
请求方法 GET / POST / PUT / DELETE GET
请求参数 参数名、类型、是否必填、默认值 page: number, required
响应结构 统一包裹格式 { code, data, message }
错误码 业务错误码及含义 1001: 用户不存在

嗯,这里要注意:响应结构必须统一。我遇到过最坑的情况是,同一个项目里,有的接口返回 { data: [...] },有的返回 { list: [...] },还有的直接返回数组。前端每次都得写兼容代码,移植的时候更是灾难。

下面是我常用的 TypeScript 接口定义模板:

// 统一响应类型
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

// 分页请求参数
interface PaginationParams {
  page: number;
  pageSize: number;
  keyword?: string;
}

// 用户列表接口
type GetUserList = (params: PaginationParams) => ApiResponse<{
  list: User[];
  total: number;
}>;

// 用户详情接口
type GetUserDetail = (id: string) => ApiResponse<User>;
核心观点:接口契约不是后端单方面定的,也不是前端单方面要求的。它应该是双方共同协商的结果。我建议在开发前做一次「接口对齐会」,把每个接口的入参、出参、异常情况都过一遍。这一步省不了。

架构设计的可视化:组件树与模块关系图

光用文字描述不够直观。下面这张 SVG 图,展示了组件树、模块划分和接口契约三者之间的关系:

界面架构设计核心关系图 组件树 Page ├── Header │ └── SearchBar ├── Table │ └── Row (复用) └── Modal └── Form 模块划分 modules/ ├── user/ │ ├── components/ │ └── services/ ├── order/ └── shared/ └── components/ 接口契约 GET /api/v1/users Params: { page, size } Response: { code: 0, data: { list, total }, message: "success" } 三者关系: 组件树定义「长什么样」,模块划分定义「放哪里」,接口契约定义「怎么连」。三者缺一不可。 💡 移植前,先画出这三张图。哪怕只是手绘在白板上,也能帮你避免 80% 的后期返工。

实战中的避坑指南

最后,分享几个我踩过的坑:

  • 组件树太「平」——所有组件都放在同一层,没有层级关系。结果一个页面的组件数量超过 50 个,找起来像大海捞针。建议按页面 → 区域 → 组件 → 子组件分层。
  • 模块划分太「细」——每个功能都单独成模块,结果模块之间互相引用,形成了循环依赖。我曾经在一个项目里看到 moduleA 引 moduleB,moduleB 又引 moduleA,编译直接报错。解决办法:用 shared 模块收容公共依赖。
  • 接口契约不「活」——契约文档写完了就扔在 wiki 里,没人维护。前后端改了接口也不更新文档,最后全靠猜。我建议把契约定义放在代码仓库里,用 TypeScript 类型或 OpenAPI 规范管理,改代码就必须改契约。
一个小技巧:在项目根目录建一个 contracts/ 文件夹,里面放所有接口的类型定义。前端和后端都引用同一份定义,谁改谁负责更新。这样移植的时候,接口部分基本不用动。

界面架构设计,说白了就是「先想清楚再动手」。组件树、模块划分、接口契约,这三件事花 30% 的时间去设计,能省下后面 70% 的移植时间。别急着写代码,先画图。