第10章:综合实战 —— Node 服务与前端框架类型设计
约 4 分钟 · 更新于 2026-09-01
第10章:综合实战 —— Node 服务与前端框架类型设计
本章用一个任务管理系统串起 Node.js、React、Vue 与共享协议。重点不是框架 API 大全,而是类型安全边界:请求进入时验证、领域内部保持可信、响应离开时保持稳定。
一、项目结构
text
task-workspace/
├── packages/
│ ├── contracts/
│ ├── server/
│ ├── react-app/
│ └── vue-app/
└── tsconfig.base.json
contracts 只放稳定协议和校验逻辑,不直接共享数据库模型或框架组件。
二、共享协议
ts
export const taskStatuses = ["todo", "doing", "done"] as const;
export type TaskStatus = (typeof taskStatuses)[number];
export interface TaskDto {
id: string;
title: string;
status: TaskStatus;
createdAt: string;
}
export interface CreateTaskInput {
title: string;
}
日期在 JSON 中传字符串,而不是假装网络会传递 Date 实例。
三、运行时校验
ts
export function parseCreateTaskInput(value: unknown): CreateTaskInput {
if (typeof value !== "object" || value === null) {
throw new Error("请求体必须是对象");
}
const record = value as Record<string, unknown>;
if (typeof record.title !== "string" || record.title.trim() === "") {
throw new Error("title 不能为空");
}
return { title: record.title.trim() };
}
真实项目可用 schema 库减少重复。原则是:外部数据先是 unknown,通过校验后才进入领域模型。
四、Node.js 服务层
ts
interface TaskRepository {
list(): Promise<Task[]>;
save(task: Task): Promise<void>;
}
class TaskService {
constructor(private readonly repository: TaskRepository) {}
async create(input: CreateTaskInput): Promise<Task> {
const task: Task = {
id: crypto.randomUUID(),
title: input.title,
status: "todo",
createdAt: new Date()
};
await this.repository.save(task);
return task;
}
}
领域模型可以使用 Date,但响应 DTO 负责序列化:
ts
function toTaskDto(task: Task): TaskDto {
return {
...task,
createdAt: task.createdAt.toISOString()
};
}
五、HTTP 路由边界
伪代码:
ts
async function createTaskHandler(request: Request): Promise<Response> {
try {
const body: unknown = await request.json();
const input = parseCreateTaskInput(body);
const task = await taskService.create(input);
return Response.json(toTaskDto(task), { status: 201 });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "未知错误";
return Response.json({ code: "BAD_REQUEST", message }, { status: 400 });
}
}
不要把框架的请求体类型直接相信为领域类型。
六、类型安全 API Client
ts
async function requestJson<T>(
url: string,
init: RequestInit,
parse: (value: unknown) => T
): Promise<T> {
const response = await fetch(url, init);
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return parse(body);
}
泛型 T 本身不会验证响应,因此把解析函数作为参数显式注入。
七、React Props 与状态
tsx
interface TaskItemProps {
task: TaskDto;
onStatusChange(taskId: string, status: TaskStatus): void;
}
function TaskItem({ task, onStatusChange }: TaskItemProps) {
return (
<button onClick={() => onStatusChange(task.id, "done")}>
{task.title} - {task.status}
</button>
);
}
异步页面状态使用判别联合:
ts
type TasksState =
| { status: "loading" }
| { status: "success"; tasks: TaskDto[] }
| { status: "error"; message: string };
避免 loading + data? + error? 的矛盾组合。
八、React 事件与 Hook
tsx
const [title, setTitle] = useState("");
function handleSubmit(event: React.FormEvent<HTMLFormElement>): void {
event.preventDefault();
// 提交
}
常见原则:
- Props 是组件公开 API,应明确命名。
- 能推断的 useState 不必重复写泛型。
- 初始值为空且后续有值时,显式联合类型。
- useRef 区分 DOM 引用与可变容器。
- 自定义 Hook 返回对象通常比长元组更可读。
九、Vue 3 Props 与 emits
vue
<script setup lang="ts">
import type { TaskDto, TaskStatus } from "@workspace/contracts";
const props = defineProps<{
task: TaskDto;
}>();
const emit = defineEmits<{
change: [taskId: string, status: TaskStatus];
}>();
</script>
<template>
<button @click="emit('change', props.task.id, 'done')">
{{ props.task.title }}
</button>
</template>
组合式 API 中:
- ref 适合单值和需要整体替换的状态。
- reactive 适合对象状态,但解构时注意响应性。
- Props 不应由子组件直接修改。
- emits 是组件输出契约,也应类型化。
十、前端状态机
提交任务时可能有:
ts
type SubmitState =
| { status: "idle" }
| { status: "submitting" }
| { status: "success"; task: TaskDto }
| { status: "error"; message: string };
这比三个独立状态变量更容易确保 UI 分支完整。复杂业务还可把事件也建模,并集中在 reducer 或状态机中转换。
十一、错误协议
ts
interface ApiErrorDto {
code: "BAD_REQUEST" | "NOT_FOUND" | "INTERNAL_ERROR";
message: string;
requestId?: string;
}
服务端内部错误不应原样泄露给客户端。客户端根据稳定 code 决定交互,message 用于展示或诊断。
十二、测试策略
- contracts:校验器和类型契约测试。
- server:领域服务单测、仓储集成测试、HTTP 边界测试。
- React/Vue:组件交互测试,不只测内部实现。
- 端到端:创建任务、状态更新、错误提示。
- 消费测试:确认前端使用的是发布后的协议包。
十三、常见误区
- 前后端共享数据库实体而非稳定 DTO。
- API Client 只写 <T>,没有运行时解析。
- 日期字段在 JSON 中被标成 Date。
- React/Vue 页面用多个布尔值表达异步状态。
- 组件 Props 使用宽泛对象或 any。
- 服务端把内部异常堆栈直接返回客户端。
- 把框架类型传入领域层,造成基础逻辑依赖框架。
- 所有包互相引用,contracts 逐渐变成杂物箱。
十四、实践练习
- 完成 Task 的运行时解析器和序列化器。
- 为服务层注入内存仓储并测试创建任务。
- 实现带解析函数的 API Client。
- 用判别联合实现 React 或 Vue 的列表加载状态。
- 为错误协议增加 requestId 并贯穿服务端和客户端。
- 将领域模型、DTO 和数据库模型拆开,说明各自职责。
十五、总结
- 类型安全系统的关键是边界,而不是共享最多类型。
- 网络数据先按 unknown 处理,验证后进入可信领域。
- DTO、领域模型和数据库模型可以不同。
- 泛型 API Client 不等于运行时校验。
- React Props、Vue Props/emits 和页面状态都应成为明确契约。
- 框架应依赖领域能力,领域核心不要依赖框架细节。
请继续阅读:第11章:兼容性与推断。
原始资料引用