TypeScript模块化开发实战:大型项目如何拆分模块避免重复代码和类型错误
嘿,朋友,你是不是也有过这种经历——在一个TypeScript项目里,文件越堆越多,类型到处乱飞,改了一个地方的类型定义,其他十几个文件跟着报错,改到怀疑人生?别慌,这事儿我见过太多了。今天咱们就掰开揉碎了聊聊,怎么把一个TypeScript大项目拆得明明白白,让代码清爽、类型安全,还能让新手一脚踩进去不摔跟头。
一、先搞懂”模块化”到底是个啥
说白了,模块化就是把一个巨大的程序拆成一块一块的”乐高积木”,每一块只管好自己的事情,别的事不掺和。你搭房子的时候,不会把所有砖头混在一堆吧?得分类放好,不然找一块红砖得翻半天。代码也一样,不拆模块的话,文件可能几百上千行,类型定义散落各处,谁都不知道某个 User 类型到底该包含哪些字段。
我之前带过一个项目,刚开始没拆分,所有类型定义都塞在一个 types.ts 文件里,后来这个文件长到了将近2000行。同事A改了用户类型的某个字段,同事B的支付模块立马报错,结果两个人吵了三天——其实根本原因就是类型散落在各个角落,没人知道谁依赖谁。这就是不拆模块的代价。
二、拆分模块的核心原则
1. 按”领域”而不是按”文件类型”拆
新手最容易犯的错误,就是把项目按文件类型分类:所有类型放一个文件夹,所有工具函数放另一个,所有组件再放一个。看起来整齐,实际上完全违背了”高内聚”的原则。
举个例子,假设你在做一个电商后台系统,里面有”用户管理”、”商品管理”、”订单管理”三个核心模块。如果你按文件类型分类,User 类型的定义可能在 types/user.ts,但 User 相关的API接口、工具函数、常量又散落在其他地方。一旦业务逻辑变了,你得像扫雷一样找遍整个项目。
正确的做法是:按领域(Domain)组织代码。 每个业务模块都有自己的一亩三分地:
src/
features/
user/
types.ts # 用户相关的所有类型
api.ts # 用户相关的接口调用
hooks.ts # 用户相关的自定义Hooks
constants.ts # 用户相关的常量
utils.ts # 用户相关的工具函数
index.ts # 统一对外导出
product/
types.ts
api.ts
...
index.ts
order/
...
这样,当产品同学说”用户模块要加一个会员等级字段”时,你直接进 src/features/user/ 就全搞定,不会误伤商品或订单模块。
2. 类型的”单一来源”原则
这是避免类型错误最关键的一条:任何一个类型定义,在整个项目里只能有一个”权威来源”。
什么叫单一来源?比如 User 类型,它的定义只能放在 src/features/user/types.ts,其他地方如果要引用,必须从这个地方 import。不能在 api.ts 里自己定义一个 User,也不能在某个组件里又写一个一模一样的 User 接口。
我见过最离谱的情况是一个项目的 User 类型定义了7个版本,名字还不一样:UserDTO、UserModel、IUser、UserEntity……改需求的时候根本不知道该改哪个,最后干脆全部改完,累个半死还容易漏。
// ✅ 正确:唯一的权威来源
// src/features/user/types.ts
export interface User {
id: string;
name: string;
email: string;
role: UserRole;
createdAt: Date;
}
export type UserRole = 'admin' | 'seller' | 'buyer';
// ✅ 正确:其他地方统一从这里引用
import { User, UserRole } from '@/features/user/types';
// ❌ 错误:自己在别处重新定义
interface User { // 同名不同结构,迟早出问题
id: number;
username: string;
}
3. 用 barrel file( barrel 文件)做统一出口
每个模块的 index.ts 就是一个 barrel file,它的作用是统一导出,让外部调用者不需要知道模块内部的具体结构,只需要从一个地方 import 就好。
// src/features/user/index.ts
// 统一导出,外部只认这个入口
export { User, UserRole, UserStatus } from './types';
export { fetchUser, createUser, updateUser } from './api';
export { useUser, useUserList } from './hooks';
export { USER_ROLE_MAX, DEFAULT_USER_STATUS } from './constants';
export { formatUserName, validateUserEmail } from './utils';
调用方就这么写:
import { User, fetchUser, useUser } from '@/features/user';
是不是清爽多了?而且以后你内部怎么重构,只要 index.ts 的导出不变,外部代码完全不用改。
三、实际案例:从零搭建一个模块化的TypeScript项目
光说不练假把式,咱们来一个完整的实战。假设我们要做一个”文章管理系统”,功能包括:文章管理、用户管理、标签管理。
第一步:搭建基础目录结构
src/
shared/ # 跨模块共享的基础工具
types/
common.ts # 通用类型(分页、响应体等)
constants.ts
utils/
helpers.ts
validators.ts
features/
article/
types.ts
api.ts
hooks.ts
utils.ts
index.ts
user/
types.ts
api.ts
hooks.ts
utils.ts
index.ts
tag/
types.ts
api.ts
hooks.ts
index.ts
app/
api/
index.ts # API路由注册
pages/
ArticleList.tsx
UserList.tsx
TagList.tsx
main.ts
index.ts # 项目根导出
第二步:定义共享类型(shared/types/common.ts)
// 通用分页请求参数
export interface PaginationParams {
page: number;
pageSize: number;
}
// 通用分页响应结构
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// 通用API响应包装
export interface ApiResponse<T = unknown> {
code: number;
message: string;
data: T;
}
// 通用分页查询结果(用于页面展示)
export type PaginatedResult<T> = {
items: T[];
total: number;
hasMore: boolean;
};
这里特别要强调一点:PaginatedResponse<T> 和 PaginatedResult<T> 要分开定义。 前者是API返回的原始结构,后者是页面组件实际使用的数据结构。把它们混在一起,后面类型转换的时候会疯掉。
第三步:定义各业务模块的类型
// src/features/article/types.ts
import { ApiResponse, PaginatedResponse } from '@/shared/types/common';
// 文章核心类型
export interface Article {
id: string;
title: string;
content: string;
summary: string;
authorId: string;
coverImage?: string;
status: ArticleStatus;
viewCount: number;
likeCount: number;
commentCount: number;
createdAt: string; // ISO 8601 格式字符串,不在这里用 Date 对象
updatedAt: string;
tags: ArticleTag[];
}
// 文章创建/更新时的输入类型(不包含服务端生成的字段)
export type ArticleCreateInput = Omit<Article, 'id' | 'viewCount' | 'likeCount' | 'commentCount' | 'createdAt' | 'updatedAt'>;
export type ArticleUpdateInput = Partial<ArticleCreateInput>;
// 文章状态枚举
export type ArticleStatus = 'draft' | 'published' | 'archived';
// 文章中的标签(精简版,避免循环引用)
export interface ArticleTag {
id: string;
name: string;
slug: string;
}
// API响应类型
export type ArticleListResponse = ApiResponse<PaginatedResponse<Article>>;
export type ArticleDetailResponse = ApiResponse<Article>;
export type ArticleCreateResponse = ApiResponse<Article>;
export type ArticleDeleteResponse = ApiResponse<null>;
看到 ArticleCreateInput 和 ArticleUpdateInput 了吗?用 Omit 和 Partial 来派生,而不是重新写一遍。这样当 Article 加了新字段,创建和更新的输入类型会自动更新,不会出现”新增字段忘记加到创建表单”这种低级错误。
// src/features/user/types.ts
import { ApiResponse } from '@/shared/types/common';
export interface User {
id: string;
username: string;
email: string;
avatar?: string;
role: UserRole;
isActive: boolean;
lastLoginAt?: string;
createdAt: string;
updatedAt: string;
}
export type UserRole = 'super_admin' | 'editor' | 'viewer';
// 用户创建输入(密码需要单独处理,不在 User 类型里)
export interface UserCreateInput {
username: string;
email: string;
password: string;
role: UserRole;
}
// 用户更新输入
export type UserUpdateInput = Partial<Omit<UserCreateInput, 'password'>> & { id: string };
// API响应类型
export type UserListResponse = ApiResponse<{ users: User[]; total: number }>;
export type UserDetailResponse = ApiResponse<User>;
这里注意 UserUpdateInput 里把 id 设为必填,因为更新操作必须知道更新谁。如果用 Partial<User>,id 就变成可选的了,调用方可能会忘记传,导致bug。
第四步:封装API层
// src/features/article/api.ts
import {
Article,
ArticleCreateInput,
ArticleUpdateInput,
ArticleListResponse,
ArticleDetailResponse,
ArticleCreateResponse,
ArticleDeleteResponse,
} from './types';
import { PaginationParams } from '@/shared/types/common';
import { request } from '@/shared/utils/request';
// 获取文章列表
export const fetchArticleList = (params: PaginationParams & { status?: Article['status'] }) => {
return request<ArticleListResponse>('/api/articles', { params });
};
// 获取文章详情
export const fetchArticleDetail = (id: string) => {
return request<ArticleDetailResponse>(`/api/articles/${id}`);
};
// 创建文章
export const createArticle = (input: ArticleCreateInput) => {
return request<ArticleCreateResponse>('/api/articles', {
method: 'POST',
body: input,
});
};
// 更新文章
export const updateArticle = (id: string, input: ArticleUpdateInput) => {
return request<ArticleCreateResponse>(`/api/articles/${id}`, {
method: 'PUT',
body: input,
});
};
// 删除文章
export const deleteArticle = (id: string) => {
return request<ArticleDeleteResponse>(`/api/articles/${id}`, {
method: 'DELETE',
});
};
API层的关键点:类型参数化。request<T> 函数接收泛型参数 T,TypeScript 就能自动推断出返回值的完整类型,不用每次手动标注。
// src/shared/utils/request.ts
// 这是一个封装好的 HTTP 请求工具,返回类型由调用方决定
export async function request<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
}
第五步:封装业务Hooks
// src/features/article/hooks.ts
import { useState, useCallback } from 'react';
import {
fetchArticleList,
fetchArticleDetail,
createArticle,
updateArticle,
deleteArticle,
} from './api';
import { Article, ArticleCreateInput, ArticleUpdateInput } from './types';
// 获取文章列表的Hook
export function useArticleList(params: { page: number; pageSize: number; status?: Article['status'] }) {
const [articles, setArticles] = useState<Article[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const res = await fetchArticleList(params);
setArticles(res.data.data);
setTotal(res.data.total);
} finally {
setLoading(false);
}
}, [params]);
return { articles, total, loading, load };
}
// 创建文章的Hook
export function useCreateArticle() {
const [error, setError] = useState<string | null>(null);
const mutate = useCallback(async (input: ArticleCreateInput) => {
setError(null);
try {
const res = await createArticle(input);
return res.data.data;
} catch (e: unknown) {
const err = e as { message?: string };
setError(err.message ?? '创建失败');
return null;
}
}, []);
return { mutate, error };
}
// 删除文章的Hook
export function useDeleteArticle(onSuccess?: () => void) {
const mutate = useCallback(async (id: string) => {
await deleteArticle(id);
onSuccess?.();
}, [onSuccess]);
return { mutate };
}
这里 useCreateArticle 的返回值里包含了 error 状态,这样调用方可以方便地处理错误提示。注意 error 的类型是 string | null,而不是 Error 对象——在React组件里,我们通常只需要显示错误消息,不需要持有完整的Error对象。
第六步:barrel file 统一导出
// src/features/article/index.ts
export {
Article,
ArticleCreateInput,
ArticleUpdateInput,
ArticleStatus,
ArticleTag,
} from './types';
export {
fetchArticleList,
fetchArticleDetail,
createArticle,
updateArticle,
deleteArticle,
} from './api';
export {
useArticleList,
useCreateArticle,
useDeleteArticle,
} from './hooks';
// src/features/user/index.ts
export {
User,
UserRole,
UserCreateInput,
UserUpdateInput,
} from './types';
export {
fetchUserList,
fetchUserDetail,
createUser,
updateUser,
deleteUser,
} from './api';
export {
useUserList,
useCreateUser,
} from './hooks';
// src/index.ts — 项目根导出
export * from '@/shared/types/common';
export * from '@/features/article';
export * from '@/features/user';
export * from '@/features/tag';
有了根 index.ts,调用方可以这样简洁地导入:
import { Article, useArticleList, fetchArticleList } from '@/';
四、避免重复代码的套路
1. 用泛型和工具类型减少重复
很多新手喜欢每个模块都写一遍”分页响应类型”:
// ❌ 重复!每个模块都自己写一遍
interface ArticleListResponse {
code: number;
data: { items: Article[]; total: number };
}
interface UserListResponse {
code: number;
data: { items: User[]; total: number };
}
interface TagListResponse {
code: number;
data: { items: Tag[]; total: number };
}
正确的做法是用泛型参数化一次,到处复用:
// ✅ 只定义一次,到处复用
interface PaginatedListResponse<T> {
code: number;
data: { items: T[]; total: number };
}
// 各模块直接用
type ArticleListResponse = PaginatedListResponse<Article>;
type UserListResponse = PaginatedListResponse<User>;
type TagListResponse = PaginatedListResponse<Tag>;
2. 提取公共的CRUD模板
如果多个模块都有相似的增删改查逻辑,可以提取通用的工具函数:
// src/shared/utils/api-helpers.ts
import { ApiResponse } from '@/shared/types/common';
// 通用分页参数校验
export function validatePaginationParams(
params: Record<string, unknown>
): { page: number; pageSize: number } {
const page = Math.max(1, parseInt(params.page as string, 10) || 1);
const pageSize = Math.min(100, Math.max(1, parseInt(params.pageSize as string, 10) || 20));
return { page, pageSize };
}
// 通用错误处理
export function handleApiError(error: unknown): string {
if (error instanceof Error) return error.message;
if (error && typeof error === 'object' && 'message' in error) {
return String((error as { message: unknown }).message);
}
return '未知错误';
}
// 通用类型守卫(判断API响应是否成功)
export function isApiResponseSuccess<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { code: 200 } {
return response.code === 200;
}
3. 用 zod 做运行时类型校验
光有TypeScript的类型系统还不够,运行时数据(比如从API返回的JSON)是没有类型信息的。这时候引入 zod 来定义schema,既能做类型推断,又能做运行时校验:
// src/features/article/types.ts
import { z } from 'zod';
// 用 zod 定义 schema
export const ArticleSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(200),
content: z.string().min(1),
summary: z.string().max(500),
authorId: z.string().uuid(),
coverImage: z.string().url().optional().nullable(),
status: z.enum(['draft', 'published', 'archived']),
viewCount: z.number().int().min(0),
likeCount: z.number().int().min(0),
commentCount: z.number().int().min(0),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
tags: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
slug: z.string(),
})),
});
// 自动从 schema 推导出 TypeScript 类型
export type Article = z.infer<typeof ArticleSchema>;
// 创建时的 schema(去掉服务端生成字段)
export const ArticleCreateSchema = ArticleSchema.omit({
id: true,
viewCount: true,
likeCount: true,
commentCount: true,
createdAt: true,
updatedAt: true,
}).extend({
status: z.enum(['draft']), // 创建时只能是草稿
});
export type ArticleCreateInput = z.infer<typeof ArticleCreateSchema>;
这样做的好处是:API返回的数据可以用 ArticleSchema.parse(data) 来校验,校验通过后才进入业务逻辑,类型安全从前端一直延伸到后端。而且 z.infer 自动推导类型,不需要手写接口,彻底杜绝”接口定义了A,实际返回B”的情况。
五、常见坑点和避坑指南
坑1:循环依赖
// article/types.ts
import { User } from '@/features/user/types'; // ← 导入用户类型
// user/types.ts
import { Article } from '@/features/article/types'; // ← 导入文章类型
这种互相引用的情况在大型项目里非常常见,TypeScript编译的时候会直接报错或者类型信息丢失。
解决方法: 把共用的类型提到 shared 层,或者用 import type 延迟加载。
// src/shared/types/user-reference.ts — 只放最小引用
export interface UserReference {
id: string;
username: string;
avatar?: string;
}
// article/types.ts — 只引用精简版
import { UserReference } from '@/shared/types/user-reference';
export interface Article {
id: string;
title: string;
author: UserReference; // 不是完整的 User,只是引用
// ...
}
坑2:过度使用 any
// ❌ 到处用 any,类型安全形同虚设
const response: any = await fetch('/api/articles');
const data = await response.json();
any 是TypeScript的”放弃治疗”模式。用了 any,你就失去了所有的类型检查,IDE不会给你提示,也不会帮你发现错误。
解决方法: 如果暂时不确定类型,先用 unknown,再做类型守卫:
// ✅ 用 unknown + 类型守卫
const response = await fetch('/api/articles');
const raw = await response.json() as unknown;
if (isArticleListResponse(raw)) {
// 这里 TypeScript 知道 raw 是 ArticleListResponse 类型
const articles = raw.data.items;
}
function isArticleListResponse(data: unknown): data is ArticleListResponse {
return (
typeof data === 'object' &&
data !== null &&
'code' in data &&
'data' in data &&
Array.isArray((data as Record<string, unknown>).data?.items)
);
}
坑3:类型别名和接口的混用
// ❌ 同一个概念,一会儿用 interface 一会儿用 type,让人迷惑
interface User { ... }
type Article = { ... };
interface ArticleTag { ... };
type UserRole = 'admin' | 'user';
虽然 interface 和 type 在大多数情况下可以互换,但混用会让代码风格不统一,后期维护的人看得很痛苦。
推荐约定:
- 需要对象类型(可以被 extend/implement)→ 用
interface - 需要联合类型/交叉类型/映射类型 → 用
type - 枚举值 → 用
const enum或as const对象
// ✅ 统一的风格
interface User {
id: string;
name: string;
}
interface Admin extends User {
permission: string[];
}
type UserRole = 'admin' | 'editor' | 'viewer';
type Article = {
id: string;
title: string;
author: User;
};
坑4:忽略 tsconfig.json 的配置
很多新手项目里的 tsconfig.json 配置得很宽松:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"esModuleInterop": false
}
}
这种配置下的项目,TypeScript 基本等于没开。strict: false 关闭了所有严格检查,noImplicitAny: false 允许隐式的 any,esModuleInterop: false 会导致导入commonjs模块时类型有问题。
推荐配置:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInImports": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
strict: true 是关键,它一次性开启了所有严格类型检查,包括 noImplicitAny、strictNullChecks、strictFunctionTypes 等。刚开始可能报错很多,但修完一遍之后,项目就基本告别类型错误了。
六、给新手的渐进式拆分策略
如果你接手的是一个已经写了很多代码的老项目,不要试图一次性全部重构。那样只会引入大量bug,而且工作量巨大。推荐用”逐步演进”的策略:
第一阶段:建立规范
- 在
src/shared/types/建立通用类型 - 新写的代码一律按领域组织
- 旧的散乱代码暂时不动,但不能再往里面加新代码
第二阶段:逐个击破
- 选择一个业务模块(比如最核心的”文章模块”)
- 把这个模块的所有类型、API、Hooks 整理到
features/article/下 - 用 barrel file 统一导出
- 跑一遍测试,确认没破坏现有功能
第三阶段:清理重复
- 全局搜索重复的类型定义(比如搜
interface.*Response) - 把重复的定义统一到 shared 层
- 更新引用
第四阶段:引入 zod
- 先在一个模块试点 zod schema
- 验证效果后,逐步推广到其他模块
- 最后可以把纯 TypeScript 接口删掉,全部用 zod 推导
记住,重构是为了让项目更好维护,不是为了炫技。 每次改动的范围要小,改完要验证,这样即使出了问题也容易回退。
七、一个完整的模块示例:Tag 模块
前面讲了那么多理论,最后给一个完整的 Tag(标签)模块,让你看看所有原则怎么落地:
// src/features/tag/types.ts
import { z } from 'zod';
import { ApiResponse, PaginatedResponse } from '@/shared/types/common';
// 标签核心类型
export const TagSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(50),
slug: z.string().min(1).max(100).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
description: z.string().max(200).optional().nullable(),
articleCount: z.number().int().min(0),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export type Tag = z.infer<typeof TagSchema>;
// 创建标签的输入(不含服务端生成字段)
export const TagCreateSchema = TagSchema.omit({
id: true,
articleCount: true,
createdAt: true,
updatedAt: true,
}).extend({
slug: z.string().min(1), // 创建时 slug 必填
});
export type TagCreateInput = z.infer<typeof TagCreateSchema>;
// 更新标签的输入
export const TagUpdateSchema = TagCreateSchema.partial().extend({
id: z.string().uuid(), // 更新时必须带 id
});
export type TagUpdateInput = z.infer<typeof TagUpdateSchema>;
// API 响应类型
export type TagListResponse = ApiResponse<PaginatedResponse<Tag>>;
export type TagDetailResponse = ApiResponse<Tag>;
export type TagCreateResponse = ApiResponse<Tag>;
export type TagDeleteResponse = ApiResponse<null>;
// src/features/tag/api.ts
import {
Tag,
TagCreateInput,
TagUpdateInput,
TagListResponse,
TagDetailResponse,
TagCreateResponse,
TagDeleteResponse,
} from './types';
import { PaginationParams } from '@/shared/types/common';
import { request } from '@/shared/utils/request';
import { validatePaginationParams } from '@/shared/utils/api-helpers';
export const fetchTagList = (params: PaginationParams) => {
const validated = validatePaginationParams(params);
return request<TagListResponse>('/api/tags', { params: validated });
};
export const fetchTagDetail = (id: string) => {
return request<TagDetailResponse>(`/api/tags/${id}`);
};
export const createTag = (input: TagCreateInput) => {
return request<TagCreateResponse>('/api/tags', {
method: 'POST',
body: input,
});
};
export const updateTag = (id: string, input: TagUpdateInput) => {
return request<TagCreateResponse>(`/api/tags/${id}`, {
method: 'PUT',
body: input,
});
};
export const deleteTag = (id: string) => {
return request<TagDeleteResponse>(`/api/tags/${id}`, {
method: 'DELETE',
});
};
// src/features/tag/hooks.ts
import { useState, useCallback } from 'react';
import { Tag, TagCreateInput, TagUpdateInput } from './types';
import {
fetchTagList,
fetchTagDetail,
createTag,
updateTag,
deleteTag,
} from './api';
import { PaginationParams } from '@/shared/types/common';
export function useTagList(params: PaginationParams) {
const [tags, setTags] = useState<Tag[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetchTagList(params);
setTags(res.data.items);
setTotal(res.data.total);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : '加载失败');
} finally {
setLoading(false);
}
}, [params]);
return { tags, total, loading, error, load };
}
export function useCreateTag() {
const [error, setError] = useState<string | null>(null);
const mutate = useCallback(async (input: TagCreateInput) => {
setError(null);
try {
const res = await createTag(input);
return res.data;
} catch (e: unknown) {
setError(e instanceof Error ? e.message : '创建失败');
return null;
}
}, []);
return { mutate, error };
}
export function useDeleteTag() {
const mutate = useCallback(async (id: string) => {
await deleteTag(id);
}, []);
return { mutate };
}
// src/features/tag/index.ts
export {
Tag,
TagCreateInput,
TagUpdateInput,
TagSchema,
TagCreateSchema,
TagUpdateSchema,
} from './types';
export {
fetchTagList,
fetchTagDetail,
createTag,
updateTag,
deleteTag,
} from './api';
export {
useTagList,
useCreateTag,
useDeleteTag,
} from './hooks';
// src/app/pages/TagList.tsx — 调用方示例
import React, { useEffect } from 'react';
import { useTagList, Tag } from '@/features/tag';
const TagListPage: React.FC = () => {
const { tags, total, loading, error, load } = useTagList({ page: 1, pageSize: 20 });
useEffect(() => {
load();
}, [load]);
if (loading) return <div>加载中...</div>;
if (error) return <div>加载失败: {error}</div>;
return (
<div>
<h2>标签列表(共 {total} 个)</h2>
<ul>
{tags.map((tag: Tag) => (
<li key={tag.id}>
{tag.name}({tag.articleCount} 篇文章)
</li>
))}
</ul>
</div>
);
};
export default TagListPage;
八、最后的建议
模块化开发不是一蹴而就的,它需要你从一开始就养成好习惯。如果项目已经很大很乱了,也别灰心,按上面的策略逐步推进,每个模块都是一个独立的胜利。
记住几个核心要点:
- 一个类型一个家——别让同一个概念有多个定义
- Barrel file 是你的朋友——统一出口,降低耦合
- zod 是你的守门员——运行时校验弥补 TypeScript 的先天不足
- 严格模式不要关——
strict: true是你对抗类型错误的最强武器 - 小步快跑,渐进重构——别想着一天之内改变世界
好了,今天的分享就到这里。如果你在实际拆分模块的过程中遇到了什么具体的问题,随时来问。代码这件事,写多了就熟了,模块化拆分也是同样的道理——第一次可能觉得麻烦,习惯之后你会发现,维护起来简直不要太爽。
