跳轉到內容

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 項目成功的關鍵,需要根據團隊規模和項目特點持續迭代優化。


相關閱讀:

最後更新於: