介绍在 TS 中 DTO 与 VO 的思想碰撞与实践,系统性地梳理从后端接口定义 到前端业务消费的完整链路。
一、 为什么要分 DTO 和 VO?
在 TypeScript 开发中,直接将后端返回的 JSON 数据透传到 UI 界面是研发初期的"捷径",但往往也是后期维护的"噩梦"。为了解决后端字段多变、命名风格不统一(如蛇形命名 vs 驼峰命名)等问题,引入 DTO(数据传输对象) 和 VO(视图对象) 的概念至关重要。
二、 核心思想:职责分离
我们将后端接口数据在系统中的流转拆分为三个关键环节:
| 环节 | 承载载体 | 核心职责 |
|---|---|---|
| 入口层 | DTO (Interface) |
契约。严格对齐接口协议,描述后端发来的原始数据。 |
| 转换层 | Mapper 函数 |
解耦。负责逻辑清洗、字段重命名、类型转换。 |
| 应用层 | VO (Interface) |
纯净。仅包含 UI 层渲染所需的属性,命名符合前端规范。 |
三、 实战演练:Axios 与泛型的完美结合
为了实现自动化的类型推导,我们通过泛型封装通用的响应结构。
1. 定义通用响应壳子
TypeScript
// 统一后端返回的 JSON 格式
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T; // 这里的 T 将被具体的 DTO 替换
}
2. 定义 DTO 与 VO
利用 TS 工具类型(如 Pick)提高定义效率。
TypeScript
// 后端原始 DTO
export interface UserDTO {
id: number;
user_name: string;
avatar_url: string;
created_at: number; // 秒级时间戳
}
// 前端业务 VO
// 挑出需要的字段,并增加/修改业务字段
export type UserVO = Pick<UserDTO, 'id' | 'avatar_url'> & {
displayName: string;
regDate: Date; // 转换为 JS Date 对象
};
3. Axios 请求与 Mapper 转换
将 Axios 的泛型能力与转换函数串联起来:
TypeScript
import axios from 'axios';
axios.interceptors.response.use((response) => {
const res = response.data as ApiResponse;
if (res.code !== 200) {
// 统一弹出后端给的错误提示
showToast(res.message);
return Promise.reject(new Error(res.message));
}
return response;
});
// 1. API 层
const fetchUserApi = (id: string) => {
// 告知 Axios:返回值的 body 是 ApiResponse 结构,其 data 属性是 UserDTO
return axios.get<ApiResponse<UserDTO>>(`/api/user/${id}`);
};
// 2. Mapper 层:负责 DTO -> VO 的脏活累活
const toUserVO = (dto: UserDTO): UserVO => ({
id: dto.id,
avatar_url: dto.avatar_url,
displayName: dto.user_name || '未知用户',
regDate: new Date(dto.created_at * 1000)
});
// 3. Service 层:业务组装
async function getUserDetail(id: string): Promise<UserVO> {
const { data: res } = await fetchUserApi(id);
// res.data 此时被自动推导为 UserDTO
return toUserVO(res.data);
}
// 4. UI 层(Vue/React):直接使用 UserVO
const [user, setUser] = useState<UserVO | null>(null);
useEffect(() => {
getUserDetail('123').then(setUser);
}, []);
总结
这套架构本质上是在前端构建了一道 "类型防火墙":
- 防火墙外:是不可控的后端原始数据(DTO)。
- 防火墙内:是稳定的、符合业务习惯的干净数据(VO)。
- 防火墙中间:是透明的转换逻辑(Mapper)。
通过这种设计,即便后端接口字段发生变更,你只需修改 DTO 定义和 Mapper 函数,UI 层的代码完全不需要改动。