跳转到内容

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

TypeScript 项目架构与工程化实践

一个良好的项目架构是团队协作和长期维护的基石。本文将从目录结构、类型系统、模块化设计、错误处理、测试策略等维度,全面讲解大型 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 项目成功的关键,需要根据团队规模和项目特点持续迭代优化。


相关阅读: