iT邦幫忙

2025 iThome 鐵人賽

DAY 0
0
Modern Web

前端工程師的 Modern Web 實踐之道系列 第 9

模組化架構:如何組織大型前端應用的程式碼結構

  • 分享至 

  • xImage
  •  

系列文章: 前端工程師的 Modern Web 實踐之道 - Day 9
預計閱讀時間: 12 分鐘
難度等級: ⭐⭐⭐⭐☆

🎯 今日目標

在前一篇文章中,我們探討了元件化思維的設計哲學。今天我們將從更宏觀的角度來思考——如何組織大型前端應用的程式碼結構,這個架構設計將直接影響專案的長期維護成本和團隊協作效率。

為什麼要關注模組化架構?

  • 複雜度爆炸: 隨著專案規模增長,程式碼管理難度呈指數級增長
  • 團隊協作困難: 不清晰的架構導致多人開發時的衝突和混亂
  • 維護成本高昂: 缺乏良好架構的專案後期修改成本極高

🔍 深度分析:從混亂到有序的架構演進

典型的架構演進歷程

讓我們先看看一個真實專案是如何從簡單變複雜的:

# 階段1:單純的靜態網站 (Day 1-30)
my-project/
├── index.html
├── style.css
├── script.js
└── images/
# 階段2:加入框架和構建工具 (Day 30-180)
my-project/
├── public/
├── src/
│   ├── components/
│   ├── pages/
│   ├── utils/
│   ├── App.js
│   └── index.js
├── package.json
└── webpack.config.js
# 階段3:業務複雜化 (Day 180-365)
my-project/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   ├── Modal/
│   │   ├── Form/
│   │   └── ... (50+ 元件)
│   ├── pages/
│   │   ├── Dashboard/
│   │   ├── UserManagement/
│   │   ├── Reports/
│   │   └── ... (20+ 頁面)
│   ├── services/
│   ├── utils/
│   ├── hooks/
│   ├── contexts/
│   └── constants/
└── ... (其他配置檔案)

問題開始浮現:

  • 🚨 單一目錄下元件過多,尋找困難
  • 🚨 業務邏輯和技術邏輯混雜
  • 🚨 跨團隊開發時經常產生衝突
  • 🚨 新人需要很長時間才能理解專案結構

現代化模組架構的核心原則

成功的大型前端應用都遵循以下核心原則:

1. 關注點分離(Separation of Concerns)

// ❌ 混雜的關注點
class UserDashboard {
  // UI 渲染邏輯
  render() { /* ... */ }

  // 業務邏輯
  calculateUserScore() { /* ... */ }

  // 資料存取邏輯
  fetchUserData() { /* ... */ }

  // 表單驗證邏輯
  validateForm() { /* ... */ }
}

// ✅ 清晰的關注點分離
// 業務邏輯層
class UserService {
  calculateUserScore(user: User): number { /* ... */ }
  validateUserData(data: UserData): ValidationResult { /* ... */ }
}

// 資料存取層
class UserRepository {
  async fetchUser(id: string): Promise<User> { /* ... */ }
  async updateUser(user: User): Promise<void> { /* ... */ }
}

// 展示層
class UserDashboard {
  constructor(
    private userService: UserService,
    private userRepository: UserRepository
  ) {}

  async render() { /* 只負責 UI 渲染 */ }
}

2. 依賴反轉(Dependency Inversion)

// ❌ 高層模組依賴低層模組
class OrderService {
  private httpClient = new HttpClient(); // 直接依賴具體實作

  async createOrder(order: Order): Promise<void> {
    await this.httpClient.post('/orders', order);
  }
}

// ✅ 依賴抽象而非具體實作
interface ApiClient {
  post<T>(url: string, data: any): Promise<T>;
  get<T>(url: string): Promise<T>;
}

class OrderService {
  constructor(private apiClient: ApiClient) {} // 依賴抽象

  async createOrder(order: Order): Promise<void> {
    await this.apiClient.post('/orders', order);
  }
}

// 具體實作可以輕易替換
class HttpApiClient implements ApiClient {
  async post<T>(url: string, data: any): Promise<T> { /* HTTP 實作 */ }
  async get<T>(url: string): Promise<T> { /* HTTP 實作 */ }
}

class MockApiClient implements ApiClient {
  async post<T>(url: string, data: any): Promise<T> { /* Mock 實作 */ }
  async get<T>(url: string): Promise<T> { /* Mock 實作 */ }
}

💻 實戰演練:企業級架構設計

1. 功能導向的模組組織

# 現代化大型專案架構 (推薦)
src/
├── shared/                    # 共用模組
│   ├── components/           # 共用元件
│   │   ├── ui/              # 基礎 UI 元件
│   │   │   ├── Button/
│   │   │   ├── Input/
│   │   │   └── Modal/
│   │   └── business/        # 業務共用元件
│   │       ├── UserAvatar/
│   │       └── StatusBadge/
│   ├── services/            # 共用服務
│   │   ├── api/
│   │   ├── auth/
│   │   └── storage/
│   ├── utils/               # 工具函式
│   ├── types/               # 型別定義
│   └── constants/           # 常數定義
├── features/                # 功能模組
│   ├── user-management/     # 使用者管理功能
│   │   ├── components/
│   │   ├── services/
│   │   ├── types/
│   │   ├── utils/
│   │   └── index.ts        # 模組匯出
│   ├── dashboard/          # 儀表板功能
│   ├── reporting/          # 報表功能
│   └── settings/          # 設定功能
├── app/                    # 應用層
│   ├── store/             # 全域狀態管理
│   ├── router/            # 路由設定
│   ├── layouts/           # 版面配置
│   └── providers/         # 供應者元件
└── assets/                # 靜態資源

2. 功能模組的內部結構設計

// features/user-management/index.ts - 模組匯出檔案
export { UserManagementPage } from './components/UserManagementPage';
export { UserService } from './services/UserService';
export { useUsers } from './hooks/useUsers';
export type { User, UserFormData } from './types';

// features/user-management/components/UserManagementPage.tsx
import { UserList } from './UserList';
import { UserForm } from './UserForm';
import { useUsers } from '../hooks/useUsers';

export function UserManagementPage() {
  const { users, createUser, updateUser, deleteUser, loading } = useUsers();

  return (
    <div className="user-management">
      <UserForm onSubmit={createUser} />
      <UserList
        users={users}
        onUpdate={updateUser}
        onDelete={deleteUser}
        loading={loading}
      />
    </div>
  );
}

// features/user-management/services/UserService.ts
import { ApiClient } from '../../../shared/services/api';
import { User, CreateUserRequest } from '../types';

export class UserService {
  constructor(private apiClient: ApiClient) {}

  async getUsers(): Promise<User[]> {
    return this.apiClient.get<User[]>('/users');
  }

  async createUser(userData: CreateUserRequest): Promise<User> {
    return this.apiClient.post<User>('/users', userData);
  }

  async updateUser(id: string, userData: Partial<User>): Promise<User> {
    return this.apiClient.put<User>(`/users/${id}`, userData);
  }

  async deleteUser(id: string): Promise<void> {
    return this.apiClient.delete(`/users/${id}`);
  }
}

// features/user-management/hooks/useUsers.ts
import { useState, useEffect } from 'react';
import { UserService } from '../services/UserService';
import { User, CreateUserRequest } from '../types';

export function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const userService = new UserService(/* 注入依賴 */);

  useEffect(() => {
    loadUsers();
  }, []);

  const loadUsers = async () => {
    try {
      setLoading(true);
      setError(null);
      const users = await userService.getUsers();
      setUsers(users);
    } catch (err) {
      setError(err instanceof Error ? err.message : '載入失敗');
    } finally {
      setLoading(false);
    }
  };

  const createUser = async (userData: CreateUserRequest) => {
    try {
      const newUser = await userService.createUser(userData);
      setUsers(prev => [...prev, newUser]);
      return newUser;
    } catch (err) {
      setError(err instanceof Error ? err.message : '建立失敗');
      throw err;
    }
  };

  return {
    users,
    loading,
    error,
    createUser,
    updateUser: (id: string, data: Partial<User>) =>
      userService.updateUser(id, data).then(loadUsers),
    deleteUser: (id: string) =>
      userService.deleteUser(id).then(loadUsers),
    refreshUsers: loadUsers
  };
}

3. 共用模組的設計策略

// shared/services/api/ApiClient.ts
export interface ApiClient {
  get<T>(url: string, config?: RequestConfig): Promise<T>;
  post<T>(url: string, data?: any, config?: RequestConfig): Promise<T>;
  put<T>(url: string, data?: any, config?: RequestConfig): Promise<T>;
  delete<T>(url: string, config?: RequestConfig): Promise<T>;
}

export interface RequestConfig {
  headers?: Record<string, string>;
  timeout?: number;
  retries?: number;
}

// shared/services/api/HttpApiClient.ts
import { ApiClient, RequestConfig } from './ApiClient';

export class HttpApiClient implements ApiClient {
  constructor(
    private baseURL: string,
    private defaultConfig: Partial<RequestConfig> = {}
  ) {}

  async get<T>(url: string, config?: RequestConfig): Promise<T> {
    return this.request<T>('GET', url, undefined, config);
  }

  async post<T>(url: string, data?: any, config?: RequestConfig): Promise<T> {
    return this.request<T>('POST', url, data, config);
  }

  private async request<T>(
    method: string,
    url: string,
    data?: any,
    config?: RequestConfig
  ): Promise<T> {
    const fullConfig = { ...this.defaultConfig, ...config };

    try {
      const response = await fetch(`${this.baseURL}${url}`, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...fullConfig.headers
        },
        body: data ? JSON.stringify(data) : undefined
      });

      if (!response.ok) {
        throw new ApiError(response.status, await response.text());
      }

      return await response.json();
    } catch (error) {
      if (error instanceof ApiError) throw error;
      throw new ApiError(0, '網路錯誤');
    }
  }
}

export class ApiError extends Error {
  constructor(
    public status: number,
    message: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// shared/components/ui/Button/Button.tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  loading?: boolean;
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: () => void;
}

export function Button({
  variant = 'primary',
  size = 'medium',
  loading = false,
  disabled = false,
  children,
  onClick
}: ButtonProps) {
  const baseClasses = 'btn';
  const variantClasses = {
    primary: 'btn-primary',
    secondary: 'btn-secondary',
    danger: 'btn-danger'
  };
  const sizeClasses = {
    small: 'btn-sm',
    medium: 'btn-md',
    large: 'btn-lg'
  };

  const classes = [
    baseClasses,
    variantClasses[variant],
    sizeClasses[size],
    loading && 'btn-loading',
    disabled && 'btn-disabled'
  ].filter(Boolean).join(' ');

  return (
    <button
      className={classes}
      disabled={disabled || loading}
      onClick={onClick}
    >
      {loading && <Spinner size="small" />}
      {children}
    </button>
  );
}

🎯 進階應用:架構治理與演進策略

1. 依賴管理與循環依賴檢測

// tools/dependency-checker.ts
import { readFileSync, readdirSync, statSync } from 'fs';
import { join } from 'path';

interface ModuleDependency {
  module: string;
  dependencies: string[];
}

class DependencyAnalyzer {
  private modules: Map<string, string[]> = new Map();

  analyzeProject(srcPath: string): void {
    this.scanDirectory(srcPath);
    this.detectCircularDependencies();
    this.validateArchitectureRules();
  }

  private scanDirectory(dirPath: string): void {
    const items = readdirSync(dirPath);

    for (const item of items) {
      const fullPath = join(dirPath, item);
      const stat = statSync(fullPath);

      if (stat.isDirectory()) {
        this.scanDirectory(fullPath);
      } else if (item.endsWith('.ts') || item.endsWith('.tsx')) {
        this.analyzeFile(fullPath);
      }
    }
  }

  private analyzeFile(filePath: string): void {
    const content = readFileSync(filePath, 'utf-8');
    const imports = this.extractImports(content);
    this.modules.set(filePath, imports);
  }

  private extractImports(content: string): string[] {
    const importRegex = /import.*from\s+['"]([^'"]+)['"]/g;
    const imports: string[] = [];
    let match;

    while ((match = importRegex.exec(content)) !== null) {
      if (match[1].startsWith('./') || match[1].startsWith('../')) {
        imports.push(match[1]);
      }
    }

    return imports;
  }

  private detectCircularDependencies(): void {
    // 實作循環依賴檢測邏輯
    const visited = new Set<string>();
    const stack = new Set<string>();

    for (const [module] of this.modules) {
      if (!visited.has(module)) {
        this.dfsCircularCheck(module, visited, stack, []);
      }
    }
  }

  private validateArchitectureRules(): void {
    // 檢查架構規則,例如:
    // - shared 模組不能依賴 features 模組
    // - features 模組不能互相依賴
    for (const [module, dependencies] of this.modules) {
      if (module.includes('/shared/')) {
        const invalidDeps = dependencies.filter(dep =>
          dep.includes('/features/')
        );
        if (invalidDeps.length > 0) {
          console.error(`架構違規: shared 模組 ${module} 依賴了 features 模組`);
        }
      }
    }
  }
}

2. 自動化程式碼分割策略

// webpack.config.js - 智能程式碼分割
const path = require('path');

module.exports = {
  entry: './src/index.ts',

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 第三方套件
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },

        // 共用模組
        shared: {
          test: /[\\/]src[\\/]shared[\\/]/,
          name: 'shared',
          chunks: 'all',
          priority: 5,
          minChunks: 2
        },

        // 各功能模組
        features: {
          test: /[\\/]src[\\/]features[\\/]/,
          name(module) {
            // 根據功能目錄自動命名
            const match = module.context.match(/features[\\/]([^[\\/]]+)/);
            return match ? `feature-${match[1]}` : 'features';
          },
          chunks: 'all',
          priority: 3,
          minSize: 20000
        }
      }
    }
  },

  // 動態載入路由
  resolve: {
    alias: {
      '@shared': path.resolve(__dirname, 'src/shared'),
      '@features': path.resolve(__dirname, 'src/features'),
      '@app': path.resolve(__dirname, 'src/app')
    }
  }
};

// app/router/routes.ts - 懶載入路由設定
import { lazy } from 'react';

// 功能模組懶載入
const UserManagement = lazy(() =>
  import('@features/user-management').then(m => ({
    default: m.UserManagementPage
  }))
);

const Dashboard = lazy(() =>
  import('@features/dashboard').then(m => ({
    default: m.DashboardPage
  }))
);

export const routes = [
  {
    path: '/users',
    component: UserManagement,
    preload: () => import('@features/user-management')
  },
  {
    path: '/dashboard',
    component: Dashboard,
    preload: () => import('@features/dashboard')
  }
];

// 路由預載入策略
export function preloadRoute(routePath: string) {
  const route = routes.find(r => r.path === routePath);
  if (route?.preload) {
    route.preload();
  }
}

3. 模組邊界測試

// __tests__/architecture.test.ts
import { analyzeModuleBoundaries } from '../tools/module-analyzer';

describe('模組架構測試', () => {
  test('shared 模組不應依賴 features 模組', () => {
    const analysis = analyzeModuleBoundaries('./src');
    const violations = analysis.findViolations([
      'shared -> features 依賴是禁止的'
    ]);

    expect(violations).toHaveLength(0);
  });

  test('features 模組不應互相依賴', () => {
    const analysis = analyzeModuleBoundaries('./src');
    const violations = analysis.findCrossFeatureDependencies();

    expect(violations).toHaveLength(0);
  });

  test('模組匯出介面應保持穩定', () => {
    const currentExports = analyzeModuleBoundaries('./src').getPublicExports();
    const expectedExports = require('../__snapshots__/module-exports.json');

    expect(currentExports).toMatchObject(expectedExports);
  });
});

📋 本日重點回顧

  1. 核心概念: 模組化架構透過關注點分離和依賴管理實現大型應用的可維護性
  2. 關鍵技術: 功能導向組織、依賴注入、介面抽象是現代模組設計的基石
  3. 實踐要點: 建立清晰的模組邊界、自動化依賴檢測、智能程式碼分割策略

🎯 最佳實踐建議

  • 推薦做法: 採用功能導向的目錄結構,而非技術導向
  • 推薦做法: 建立明確的模組匯出界面,隱藏內部實作細節
  • 推薦做法: 使用依賴注入降低模組間耦合度
  • 避免陷阱: 過度抽象導致理解成本增加
  • 避免陷阱: 忽略模組邊界,造成架構腐化

🤔 延伸思考

  1. 如何在微前端架構中應用模組化設計原則?
  2. 當團隊規模擴大時,如何建立有效的架構治理機制?
  3. 如何平衡模組獨立性與程式碼複用的矛盾?

上一篇
元件化思維:從 jQuery 外掛到現代元件的設計哲學
下一篇
狀態管理選擇困難症:從 Redux 到 Zustand 的現代化方案
系列文
前端工程師的 Modern Web 實踐之道13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言