TypeScript 项目架构与工程化实践 2026 | 大型项目最佳实践

一个良好的项目架构是团队协作和长期维护的基石。本文将从目录结构、类型系统、模块化设计、错误处理、测试策略等维度,全面讲解大型 TypeScript 项目的工程化实践。
一、项目架构设计
1.1 目录结构规范
src/
├── api/ # API 层
│ ├── client.ts # HTTP 客户端封装
│ ├── interceptors/ # 请求/响应拦截器
│ └── services/ # 业务 API 服务
│ ├── auth.ts
│ ├── users.ts
│ └── orders.ts
├── app/ # 应用核心
│ ├── app.ts # 应用入口
│ ├── router/ # 路由配置
│ └── store/ # 状态管理
├── components/ # UI 组件
│ ├── common/ # 通用组件
│ ├── layouts/ # 布局组件
│ └── pages/ # 页面组件
├── config/ # 配置文件
│ ├── env.ts # 环境变量
│ ├── constants.ts # 常量定义
│ └── index.ts # 配置导出
├── domain/ # 领域层(核心业务逻辑)
│ ├── entities/ # 实体定义
│ ├── repositories/ # 仓储接口
│ ├── services/ # 领域服务
│ └── useCases/ # 用例(业务场景)
├── infrastructure/ # 基础设施层
│ ├── database/ # 数据库连接
│ ├── logging/ # 日志系统
│ └── external/ # 外部服务集成
├── shared/ # 共享层
│ ├── types/ # 全局类型定义
│ ├── utils/ # 工具函数
│ ├── hooks/ # 自定义 hooks
│ └── validators/ # 表单验证器
├── presentation/ # 表现层
│ ├── pages/ # 页面
│ ├── components/ # 展示组件
│ └── controllers/ # 控制器
├── types/ # TypeScript 类型定义
│ ├── index.ts # 类型导出
│ └── schema.ts # 数据模式
└── main.ts # 应用入口1.2 架构原则
- 单一职责:每个模块只负责一个功能
- 依赖倒置:高层模块不依赖底层模块,都依赖抽象
- 开闭原则:对扩展开放,对修改关闭
- 接口隔离:客户端不应依赖它不需要的接口
- 里氏替换:子类可以替换父类而不影响功能
二、类型系统设计
2.1 基础类型定义
typescript
// types/index.ts
export type Id = string | number;
export interface BaseEntity {
id: Id;
createdAt: Date;
updatedAt: Date;
}
export interface User extends BaseEntity {
name: string;
email: string;
role: UserRole;
avatar?: string;
}
export type UserRole = 'admin' | 'user' | 'guest';
export interface Pagination<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
error?: ApiError;
}
export interface ApiError {
code: number;
message: string;
details?: string[];
}2.2 联合类型与交叉类型
typescript
// 联合类型
type Status = 'pending' | 'active' | 'disabled';
type PaymentMethod =
| 'credit_card'
| 'debit_card'
| 'paypal'
| 'bank_transfer';
// 交叉类型
type Timestamps = { createdAt: Date; updatedAt: Date };
type SoftDelete = { deletedAt?: Date };
type AuditableEntity<T> = T & Timestamps & SoftDelete;
interface Product {
id: string;
name: string;
price: number;
}
type AuditableProduct = AuditableEntity<Product>;2.3 条件类型与映射类型
typescript
// 条件类型
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Extract<T, U> = T extends U ? T : never;
// 映射类型
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 自定义映射类型
type Nullable<T> = { [P in keyof T]: T[P] | null };
type Optional<T> = { [P in keyof T]+?: T[P] };2.4 类型守卫与类型谓词
typescript
// 类型守卫
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// 联合类型守卫
type Animal = Dog | Cat | Bird;
interface Dog { type: 'dog'; bark(): void; }
interface Cat { type: 'cat'; meow(): void; }
interface Bird { type: 'bird'; fly(): void; }
function speak(animal: Animal): void {
switch (animal.type) {
case 'dog':
animal.bark();
break;
case 'cat':
animal.meow();
break;
case 'bird':
animal.fly();
break;
}
}三、模块化设计
3.1 模块划分原则
typescript
// 不好:一个大文件包含所有逻辑
// utils.ts
export function formatDate(date: Date): string { /* ... */ }
export function validateEmail(email: string): boolean { /* ... */ }
export function generateId(): string { /* ... */ }
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): T { /* ... */ }
// 好:按功能划分模块
// utils/date.ts
export function formatDate(date: Date): string { /* ... */ }
// utils/validation.ts
export function validateEmail(email: string): boolean { /* ... */ }
// utils/id.ts
export function generateId(): string { /* ... */ }
// utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): T { /* ... */ }3.2 循环依赖处理
typescript
// 避免循环依赖的方法
// 方法 1:提取公共接口
// types/user.ts (独立文件)
export interface User { id: string; name: string; }
// services/auth.ts
import type { User } from '../types/user';
// services/profile.ts
import type { User } from '../types/user';
// 方法 2:使用动态导入
// a.ts
export async function useB() {
const { b } = await import('./b');
return b();
}
// b.ts
export async function useA() {
const { a } = await import('./a');
return a();
}
// 方法 3:使用第三方服务
// eventBus.ts
export const eventBus = createEventBus();
// a.ts
eventBus.on('event', handler);
// b.ts
eventBus.emit('event', data);3.3 模块导出策略
typescript
// 统一导出(推荐)
// api/index.ts
export { default as authApi } from './services/auth';
export { default as usersApi } from './services/users';
export { default as ordersApi } from './services/orders';
export { default as apiClient } from './client';
// 使用
import { authApi, usersApi } from './api';
// 命名空间导出
export * as auth from './services/auth';
export * as users from './services/users';
// 使用
import { auth, users } from './api';
auth.login();
users.getList();四、错误处理
4.1 自定义错误类
typescript
// errors/index.ts
export class AppError extends Error {
constructor(
public code: string,
message: string,
public details?: Record<string, any>
) {
super(message);
this.name = 'AppError';
}
}
export class ValidationError extends AppError {
constructor(message: string, public errors: Record<string, string[]>) {
super('VALIDATION_ERROR', message);
this.name = 'ValidationError';
}
}
export class AuthError extends AppError {
constructor(message: string) {
super('AUTH_ERROR', message);
this.name = 'AuthError';
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super('NOT_FOUND', `${resource} not found`);
this.name = 'NotFoundError';
}
}
export class RateLimitError extends AppError {
constructor(public retryAfter: number) {
super('RATE_LIMIT', 'Too many requests');
this.name = 'RateLimitError';
}
}4.2 错误处理中间件
typescript
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError, ValidationError, AuthError, NotFoundError } from '../errors';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error(err);
if (err instanceof ValidationError) {
return res.status(400).json({
success: false,
error: {
code: err.code,
message: err.message,
details: err.errors
}
});
}
if (err instanceof AuthError) {
return res.status(401).json({
success: false,
error: { code: err.code, message: err.message }
});
}
if (err instanceof NotFoundError) {
return res.status(404).json({
success: false,
error: { code: err.code, message: err.message }
});
}
if (err instanceof AppError) {
return res.status(500).json({
success: false,
error: { code: err.code, message: err.message }
});
}
// 未知错误
res.status(500).json({
success: false,
error: { code: 'UNKNOWN_ERROR', message: 'Internal server error' }
});
}4.3 异步错误处理
typescript
// utils/asyncHandler.ts
type AsyncHandler<T = any> = (...args: any[]) => Promise<T>;
export function asyncHandler<T>(handler: AsyncHandler<T>) {
return (...args: any[]) => {
const result = handler(...args);
const next = args[args.length - 1];
if (typeof next === 'function') {
result.catch(next);
}
return result;
};
}
// 使用
import { asyncHandler } from './utils';
app.get('/users', asyncHandler(async (req, res) => {
const users = await usersService.getUsers();
res.json({ success: true, data: users });
}));
// 在 Promise 链中处理
export async function fetchUser(id: string): Promise<User> {
try {
const response = await apiClient.get(`/users/${id}`);
return response.data;
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError('FETCH_ERROR', 'Failed to fetch user');
}
}五、状态管理
5.1 Pinia 模块化
typescript
// store/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { usersApi } from '../api';
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
const isLoggedIn = computed(() => !!user.value);
const isAdmin = computed(() => user.value?.role === 'admin');
async function login(credentials: { email: string; password: string }) {
loading.value = true;
error.value = null;
try {
const response = await usersApi.login(credentials);
user.value = response.data;
localStorage.setItem('token', response.token);
} catch (err) {
error.value = err as Error;
throw err;
} finally {
loading.value = false;
}
}
async function logout() {
user.value = null;
localStorage.removeItem('token');
}
async function fetchProfile() {
loading.value = true;
try {
const response = await usersApi.getProfile();
user.value = response.data;
} finally {
loading.value = false;
}
}
return { user, loading, error, isLoggedIn, isAdmin, login, logout, fetchProfile };
});5.2 状态持久化
typescript
// plugins/persist.ts
import { PiniaPluginContext } from 'pinia';
export function persistPlugin(context: PiniaPluginContext) {
const { store, options } = context;
if (!options.persist) return;
// 从 localStorage 恢复状态
const saved = localStorage.getItem(store.$id);
if (saved) {
try {
store.$patch(JSON.parse(saved));
} catch {
console.error('Failed to restore state for', store.$id);
}
}
// 监听状态变化
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
// 使用
import { createPinia } from 'pinia';
import { persistPlugin } from './plugins/persist';
const pinia = createPinia();
pinia.use(persistPlugin);六、测试策略
6.1 测试金字塔
单元测试(Unit Tests) 最多
↓
集成测试(Integration Tests) 中等
↓
端到端测试(E2E Tests) 最少6.2 单元测试
typescript
// utils/date.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate, parseDate } from './date';
describe('date utils', () => {
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024-01-15');
});
it('should handle invalid date', () => {
expect(() => formatDate(new Date('invalid'))).toThrow();
});
});
describe('parseDate', () => {
it('should parse string to date', () => {
const date = parseDate('2024-01-15');
expect(date.getFullYear()).toBe(2024);
expect(date.getMonth()).toBe(0);
expect(date.getDate()).toBe(15);
});
});
});
// services/auth.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { authService } from './auth';
import { apiClient } from '../client';
vi.mock('../client');
describe('auth service', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should login successfully', async () => {
(apiClient.post as vi.Mock).mockResolvedValue({
data: { id: '1', name: 'Test', email: 'test@example.com' },
token: 'mock-token'
});
const result = await authService.login({ email: 'test@example.com', password: 'password' });
expect(result).toEqual({ id: '1', name: 'Test', email: 'test@example.com' });
expect(apiClient.post).toHaveBeenCalledWith('/auth/login', { email: 'test@example.com', password: 'password' });
});
it('should throw error on login failure', async () => {
(apiClient.post as vi.Mock).mockRejectedValue(new Error('Invalid credentials'));
await expect(authService.login({ email: 'test', password: 'wrong' }))
.rejects
.toThrow('Invalid credentials');
});
});6.3 集成测试
typescript
// api/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { app } from '../app';
import request from 'supertest';
describe('users API', () => {
let server: any;
beforeAll(() => {
server = app.listen(3000);
});
afterAll(() => {
server.close();
});
it('should get users list', async () => {
const response = await request(app).get('/api/users');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
it('should create a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Test User', email: 'test@example.com' });
expect(response.status).toBe(201);
expect(response.body.data.name).toBe('Test User');
});
it('should return 400 for invalid user data', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: '' });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
});6.4 E2E 测试
typescript
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.user-name')).toHaveText('Test User');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'wrong@example.com');
await page.fill('input[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toHaveText('Invalid email or password');
});
});七、CI/CD 工程化
7.1 GitHub Actions 完整配置
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --coverage
- uses: codecov/codecov-action@v4
with: { token: ${{ secrets.CODECOV_TOKEN }} }
build:
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with: { name: build, path: dist }7.2 部署流水线
yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
tags: ['v*']
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/download-artifact@v4
with: { name: build }
- name: Deploy to Staging
run: |
# 使用 SSH 部署到测试环境
scp -r dist/* user@staging.example.com:/var/www/app/
deploy-production:
runs-on: ubuntu-latest
environment: production
needs: deploy-staging
steps:
- uses: actions/download-artifact@v4
with: { name: build }
- name: Deploy to Production
run: |
scp -r dist/* user@production.example.com:/var/www/app/八、性能优化
8.1 代码分割
typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
});8.2 懒加载组件
typescript
// components/LazyChart.vue
import { defineAsyncComponent } from 'vue';
export const LazyChart = defineAsyncComponent(() =>
import('./Chart.vue')
);
export const LazyMap = defineAsyncComponent({
loader: () => import('./Map.vue'),
loadingComponent: () => import('./Loading.vue'),
errorComponent: () => import('./Error.vue'),
delay: 200,
timeout: 3000
});8.3 资源优化
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
chart: ['echarts'],
map: ['leaflet']
}
}
}
},
plugins: [visualizer()]
});九、代码规范
9.1 ESLint 配置
javascript
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'vue'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'prettier'
],
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'vue/multi-word-component-names': 'off'
}
};9.2 Prettier 配置
json
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "lf"
}十、总结
- ✅ 项目架构设计(目录结构、分层架构、架构原则)
- ✅ 类型系统设计(基础类型、联合/交叉类型、条件/映射类型、类型守卫)
- ✅ 模块化设计(模块划分、循环依赖处理、导出策略)
- ✅ 错误处理(自定义错误类、错误中间件、异步错误处理)
- ✅ 状态管理(Pinia 模块化、状态持久化)
- ✅ 测试策略(单元测试、集成测试、E2E 测试)
- ✅ CI/CD 工程化(GitHub Actions、部署流水线)
- ✅ 性能优化(代码分割、懒加载、资源优化)
- ✅ 代码规范(ESLint、Prettier)
一个完善的工程化体系是大型 TypeScript 项目成功的关键,需要根据团队规模和项目特点持续迭代优化。
相关阅读: