异常数据防御性编程
场景
后端接口返回的数据结构不符合预期(字段缺失、类型错误、null/undefined),导致前端页面崩溃。如何编写健壮的前端代码?
方案设计
1. 可选链 + 空值合并
基础防御
// ❌ 不安全
const name = response.data.user.profile.name;
// ✅ 可选链 + 默认值
const name = response.data?.user?.profile?.name ?? '未知用户';
const list = response.data?.items ?? [];
const count = response.data?.total ?? 0;
2. 数据校验层(Zod)
api-validator.ts
import { z } from 'zod';
// 定义 Schema
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
avatar: z.string().url().nullable(),
roles: z.array(z.string()).default([]),
});
type User = z.infer<typeof UserSchema>;
// 接口层统一校验
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const json = await res.json();
return UserSchema.parse(json); // 不合法时抛出 ZodError
}
// 宽松模式:不抛错,返回默认值
function safeParseUser(data: unknown): User | null {
const result = UserSchema.safeParse(data);
return result.success ? result.data : null;
}
3. 安全访问工具函数
safe-get.ts
// 安全获取嵌套属性
function safeGet<T>(fn: () => T, fallback: T): T {
try {
const result = fn();
return result ?? fallback;
} catch {
return fallback;
}
}
// 安全 JSON 解析
function safeJSON<T>(str: string, fallback: T): T {
try {
return JSON.parse(str) ?? fallback;
} catch {
return fallback;
}
}
// 安全数组操作
function safeArray<T>(value: unknown): T[] {
return Array.isArray(value) ? value : [];
}
4. 渲染层防御
SafeRender.tsx
// 列表渲染防御
function UserList({ users }: { users: unknown }) {
const safeUsers = Array.isArray(users) ? users : [];
return (
<ul>
{safeUsers.map((user) => (
<li key={user?.id ?? Math.random()}>
{String(user?.name ?? '未知')}
</li>
))}
</ul>
);
}
防御性编程检查清单
| 数据类型 | 常见问题 | 防御方式 |
|---|---|---|
| 字符串 | null/undefined/非字符串 | String(val ?? '') |
| 数字 | NaN/Infinity/字符串 | Number(val) || 0 |
| 数组 | null/对象/字符串 | Array.isArray(val) ? val : [] |
| 对象 | null/数组 | val && typeof val === 'object' && !Array.isArray(val) |
| 日期 | Invalid Date/时间戳类型 | new Date(val) 后检查 isNaN |
| URL | 格式错误 | try { new URL(val) } catch {} |
常见面试问题
Q1: 前端如何防止后端接口返回异常数据导致页面崩溃?
答案:
分 3 层防御:
- 接口层:使用 Zod/Yup 等库对返回数据做 Schema 校验,不合法时降级处理
- 逻辑层:使用可选链(
?.)和空值合并(??)避免 undefined 访问 - 渲染层:ErrorBoundary 兜底 + 列表/属性 渲染前做类型检查
Q2: TypeScript 的类型检查能替代运行时校验吗?
答案:
不能。TypeScript 类型只在编译时检查,运行时被抹除。后端接口返回的数据是 any/unknown,即使你写了类型注解,运行时依然可能是错的。所以需要 Zod 等运行时校验库配合。