界面架构设计:组件树、模块划分与接口契约
界面移植这件事,很多人一上来就开干——复制代码、改样式、调接口。结果呢?改到一半发现结构乱了,模块之间互相牵扯,一个按钮的改动能引发五个页面的连锁报错。我见过太多这样的项目了。
说白了,移植不是体力活,是设计活。你得先想清楚:组件怎么组织?模块怎么切分?接口怎么约定?这三件事定下来,后面的工作就是按图索骥。
组件树设计:从页面到原子的分层
组件树,就是你的界面结构地图。我习惯从两个维度去画它:层级关系和复用粒度。
举个例子,一个后台管理系统的用户列表页,它的组件树可能是这样的:
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 行代码,是不是该拆了?
模块划分原则:高内聚、低耦合
模块划分,说白了就是「把相关的放一起,不相关的隔开」。我常用的原则有三个:
- 业务边界原则——同一个业务功能的所有组件、逻辑、样式放在一个模块里。比如「用户管理」模块,就包含列表、表单、详情、权限控制等。
- 复用频率原则——被多处引用的基础组件(按钮、输入框、弹窗)提升到公共模块。我在一个项目中统计过,
Button组件被引用了 47 次,这种组件必须统一管理。 - 变更隔离原则——容易变化的部分(如第三方 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 图,展示了组件树、模块划分和接口契约三者之间的关系:
实战中的避坑指南
最后,分享几个我踩过的坑:
- 组件树太「平」——所有组件都放在同一层,没有层级关系。结果一个页面的组件数量超过 50 个,找起来像大海捞针。建议按页面 → 区域 → 组件 → 子组件分层。
- 模块划分太「细」——每个功能都单独成模块,结果模块之间互相引用,形成了循环依赖。我曾经在一个项目里看到
moduleA引moduleB,moduleB又引moduleA,编译直接报错。解决办法:用shared模块收容公共依赖。 - 接口契约不「活」——契约文档写完了就扔在 wiki 里,没人维护。前后端改了接口也不更新文档,最后全靠猜。我建议把契约定义放在代码仓库里,用 TypeScript 类型或 OpenAPI 规范管理,改代码就必须改契约。
一个小技巧:在项目根目录建一个
contracts/ 文件夹,里面放所有接口的类型定义。前端和后端都引用同一份定义,谁改谁负责更新。这样移植的时候,接口部分基本不用动。
界面架构设计,说白了就是「先想清楚再动手」。组件树、模块划分、接口契约,这三件事花 30% 的时间去设计,能省下后面 70% 的移植时间。别急着写代码,先画图。