dramaling-app/docs/04_technical/01_architecture/frontend-backend-separation...

16 KiB
Raw Blame History

前後端分離架構策略

文件版本: 1.0
建立日期: 2025-09-10
更新日期: 2025-09-10
負責人: Drama Ling 開發團隊

📋 概述

Drama Ling 採用前後端分離架構後端提供統一的API服務同時支援Web前端和Flutter移動端。這種架構確保了API的一致性並為未來的擴展奠定了基礎。

🏗 整體架構設計

架構圖

graph TB
    subgraph "客戶端層"
        WEB[Web Frontend<br/>Vite + Modern JS<br/>Port: 3000]
        APP[Flutter Mobile App<br/>Dart + Flutter]
    end

    subgraph "API層"
        API[統一API服務<br/>.NET Core Web API<br/>Port: 5000]
        subgraph "API端點"
            WEB_API[/api/v1/web/*<br/>Web專用API]
            MOBILE_API[/api/v1/mobile/*<br/>移動端專用API]
            COMMON_API[/api/v1/common/*<br/>共用API]
        end
    end

    subgraph "資料層"
        DB[(PostgreSQL<br/>主資料庫)]
        CACHE[(Redis<br/>快取)]
        STORAGE[檔案儲存<br/>本地/雲端]
    end

    subgraph "外部服務"
        AI[AI服務<br/>OpenAI GPT-4o-mini]
        PAYMENT[支付服務<br/>Stripe/藍新]
        EMAIL[通知服務<br/>Email/Push]
    end

    WEB --> WEB_API
    APP --> MOBILE_API
    WEB --> COMMON_API
    APP --> COMMON_API

    WEB_API --> API
    MOBILE_API --> API
    COMMON_API --> API

    API --> DB
    API --> CACHE
    API --> STORAGE
    API --> AI
    API --> PAYMENT
    API --> EMAIL

🎯 前端架構設計

Web前端 (現代JavaScript架構)

技術棧

{
  "基礎技術": {
    "HTML": "HTML5 語義化標籤",
    "CSS": "SCSS/Sass + CSS Grid + Flexbox",
    "JavaScript": "ES2022+ + ES6 Modules"
  },
  "開發工具": {
    "構建工具": "Vite 5.x",
    "包管理": "npm",
    "代碼品質": "ESLint + Prettier"
  },
  "架構模式": {
    "組織方式": "模組化架構",
    "狀態管理": "原生JavaScript類別",
    "路由": "SPA + History API",
    "API通信": "Fetch API"
  }
}

目錄結構

apps/web/
├── index.html                 # 主入口頁面
├── src/
│   ├── modules/               # 核心模組
│   │   ├── VocabularyApp.js   # 詞彙學習應用
│   │   ├── VocabularyState.js # 狀態管理
│   │   └── AuthModule.js      # 認證模組
│   ├── components/            # 可重用組件
│   │   ├── BaseComponent.js   # 基礎組件類別
│   │   └── AudioManager.js    # 音頻管理
│   ├── utils/                 # 工具函數
│   │   ├── api.js            # API請求封裝
│   │   ├── storage.js        # 本地儲存
│   │   └── helpers.js        # 輔助函數
│   ├── styles/               # 樣式檔案
│   │   ├── main.scss         # 主樣式
│   │   ├── variables.scss    # SCSS變數
│   │   └── vocabulary.scss   # 功能專用樣式
│   └── main.js               # 應用入口
├── package.json              # 依賴配置
└── vite.config.js           # Vite配置

API整合範例

// src/utils/api.js
class ApiClient {
    constructor() {
        this.baseURL = 'http://localhost:5000/api/v1';
        this.webEndpoint = `${this.baseURL}/web`;
        this.commonEndpoint = `${this.baseURL}/common`;
    }

    async request(url, options = {}) {
        const response = await fetch(url, {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.getToken()}`,
                ...options.headers
            },
            ...options
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        return await response.json();
    }

    // Web專用API
    async getVocabulary(id) {
        return this.request(`${this.webEndpoint}/vocabulary/${id}`);
    }

    async searchVocabulary(query) {
        return this.request(`${this.webEndpoint}/vocabulary/search?q=${query}`);
    }

    // 共用API
    async getUserProfile() {
        return this.request(`${this.commonEndpoint}/user/profile`);
    }

    getToken() {
        return localStorage.getItem('auth_token');
    }
}

export const apiClient = new ApiClient();

Flutter移動端

技術棧

framework: "Flutter 3.16+"
language: "Dart 3.0+"
state_management: "Riverpod"
networking: "Dio + Retrofit"
local_storage: "Hive"
authentication: "JWT Token"
api_integration: "RESTful API Client"

API整合範例

// lib/services/api_service.dart
class ApiService {
  static const String baseUrl = 'http://localhost:5000/api/v1';
  static const String mobileEndpoint = '$baseUrl/mobile';
  static const String commonEndpoint = '$baseUrl/common';

  final Dio _dio = Dio();

  ApiService() {
    _dio.interceptors.add(AuthInterceptor());
  }

  // 移動端專用API
  Future<VocabularyDto> getVocabulary(int id) async {
    final response = await _dio.get('$mobileEndpoint/vocabulary/$id');
    return VocabularyDto.fromJson(response.data);
  }

  // 共用API
  Future<UserProfile> getUserProfile() async {
    final response = await _dio.get('$commonEndpoint/user/profile');
    return UserProfile.fromJson(response.data);
  }
}

🔧 後端API設計

API端點策略

1. Web端專用API (/api/v1/web/*)

[ApiController]
[Route("api/v1/web/[controller]")]
public class VocabularyController : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> GetVocabulary(int id)
    {
        // 返回豐富的資料結構適合Web展示
        var vocabulary = await _service.GetDetailedVocabularyAsync(id);
        return Ok(new WebApiResponse<DetailedVocabularyDto>
        {
            Data = vocabulary,
            Meta = new
            {
                RelatedWords = await _service.GetRelatedWordsAsync(id),
                LearningAnalytics = await _service.GetAnalyticsAsync(id)
            }
        });
    }

    [HttpGet("search")]
    public async Task<IActionResult> Search([FromQuery] SearchRequest request)
    {
        // 支援複雜的搜尋功能
        var result = await _service.SearchAsync(request);
        return Ok(new WebApiResponse<SearchResult>
        {
            Data = result.Items,
            Meta = new
            {
                Pagination = result.Pagination,
                Filters = result.AvailableFilters,
                Aggregations = result.Aggregations
            }
        });
    }
}

2. 移動端專用API (/api/v1/mobile/*)

[ApiController]
[Route("api/v1/mobile/[controller]")]
public class VocabularyController : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> GetVocabulary(int id)
    {
        // 返回精簡的資料結構,適合移動端
        var vocabulary = await _service.GetBasicVocabularyAsync(id);
        return Ok(new MobileApiResponse<BasicVocabularyDto>
        {
            Data = vocabulary,
            Success = true,
            Timestamp = DateTimeOffset.UtcNow
        });
    }

    [HttpPost("sync")]
    public async Task<IActionResult> SyncData([FromBody] SyncRequest request)
    {
        // 增量同步,減少資料傳輸
        var changes = await _service.GetChangesAsync(request.LastSyncTime);
        return Ok(new MobileApiResponse<SyncResult>
        {
            Data = changes,
            Success = true
        });
    }
}

3. 共用API (/api/v1/common/*)

[ApiController]
[Route("api/v1/common/[controller]")]
public class UserController : ControllerBase
{
    [HttpGet("profile")]
    public async Task<IActionResult> GetProfile()
    {
        // 兩端都需要的基本用戶資料
        var profile = await _service.GetUserProfileAsync(User.GetUserId());
        return Ok(new ApiResponse<UserProfileDto>
        {
            Data = profile
        });
    }

    [HttpPost("preferences")]
    public async Task<IActionResult> UpdatePreferences([FromBody] PreferencesDto preferences)
    {
        // 用戶偏好設定,兩端共用
        await _service.UpdatePreferencesAsync(User.GetUserId(), preferences);
        return Ok();
    }
}

認證策略

雙重認證系統

// Program.cs
builder.Services.AddAuthentication()
    .AddJwtBearer("JwtBearer", options => 
    {
        // 移動端JWT認證
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    })
    .AddCookie("Cookie", options =>
    {
        // Web端Cookie認證
        options.LoginPath = "/auth/login";
        options.LogoutPath = "/auth/logout";
        options.AccessDeniedPath = "/auth/forbidden";
        options.ExpireTimeSpan = TimeSpan.FromDays(30);
        options.SlidingExpiration = true;
    });

// 控制器上的認證配置
[Authorize(AuthenticationSchemes = "JwtBearer")]  // 移動端API
[Authorize(AuthenticationSchemes = "Cookie")]     // Web端API

🔄 開發工作流程

前端開發流程

  1. 本地開發環境
# 啟動前端開發伺服器
cd apps/web-native
npm install
npm run dev  # http://localhost:3000

# 啟動後端API服務
cd ../../backend
dotnet run   # http://localhost:5000
  1. API測試和整合
// 開發時的API配置
const API_CONFIG = {
    development: 'http://localhost:5000/api/v1',
    production: 'https://api.dramaling.com/api/v1'
};

// 環境檢測
const apiBase = process.env.NODE_ENV === 'production' 
    ? API_CONFIG.production 
    : API_CONFIG.development;
  1. CORS配置 (後端)
// Program.cs
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder
            .WithOrigins(
                "http://localhost:3000",  // 前端開發環境
                "https://app.dramaling.com"  // 生產環境
            )
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
    });
});

app.UseCors();

部署策略

開發環境

前端:
  - 開發伺服器: Vite dev server (http://localhost:3000)
  - 熱重載: 支援
  - API代理: 配置到後端開發服務

後端:
  - 開發伺服器: dotnet run (http://localhost:5000)
  - 資料庫: 本地PostgreSQL
  - 快取: 本地Redis

生產環境

前端:
  - 構建: npm run build
  - 部署: 靜態檔案 + CDN
  - 域名: https://app.dramaling.com

後端:
  - 容器化: Docker
  - 部署: VPS/雲端服務
  - 域名: https://api.dramaling.com
  - 資料庫: PostgreSQL (雲端)
  - 快取: Redis (雲端)

📊 API設計最佳實踐

1. RESTful設計原則

GET    /api/v1/web/vocabulary         # 獲取詞彙清單
GET    /api/v1/web/vocabulary/{id}    # 獲取單個詞彙
POST   /api/v1/web/vocabulary         # 創建新詞彙
PUT    /api/v1/web/vocabulary/{id}    # 更新詞彙
DELETE /api/v1/web/vocabulary/{id}    # 刪除詞彙

GET    /api/v1/mobile/vocabulary/sync # 移動端專用同步
POST   /api/v1/mobile/learning/progress # 學習進度更新

2. 一致的回應格式

// Web端回應格式
public class WebApiResponse<T>
{
    public T Data { get; set; }
    public object Meta { get; set; }
    public Dictionary<string, string> Links { get; set; }
    public DateTimeOffset Timestamp { get; set; }
}

// 移動端回應格式
public class MobileApiResponse<T>
{
    public T Data { get; set; }
    public string Message { get; set; }
    public bool Success { get; set; }
    public DateTimeOffset Timestamp { get; set; }
}

// 錯誤回應格式
public class ErrorResponse
{
    public string Error { get; set; }
    public string Message { get; set; }
    public int Code { get; set; }
    public object Details { get; set; }
}

3. 版本控制策略

[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/web/[controller]")]
public class VocabularyController : ControllerBase
{
    // v1.0 實現
}

[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/web/[controller]")]
public class VocabularyV2Controller : ControllerBase
{
    // v2.0 實現,保持向後相容
}

🚀 效能優化

前端優化

// API請求快取
class CachedApiClient extends ApiClient {
    constructor() {
        super();
        this.cache = new Map();
        this.cacheTimeout = 5 * 60 * 1000; // 5分鐘
    }

    async cachedRequest(url, options = {}) {
        const cacheKey = `${url}-${JSON.stringify(options)}`;
        const cached = this.cache.get(cacheKey);
        
        if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
            return cached.data;
        }

        const data = await this.request(url, options);
        this.cache.set(cacheKey, {
            data,
            timestamp: Date.now()
        });

        return data;
    }
}

// 懶載入模組
const loadVocabularyModule = () => import('./modules/VocabularyApp.js');
const loadAuthModule = () => import('./modules/AuthModule.js');

後端優化

// 快取策略
[HttpGet("{id}")]
[ResponseCache(Duration = 300)] // 5分鐘快取
public async Task<IActionResult> GetVocabulary(int id)
{
    var cacheKey = $"vocabulary:{id}";
    var cached = await _cache.GetStringAsync(cacheKey);
    
    if (cached != null)
    {
        return Ok(JsonSerializer.Deserialize<VocabularyDto>(cached));
    }

    var vocabulary = await _service.GetVocabularyAsync(id);
    await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(vocabulary),
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
        });

    return Ok(vocabulary);
}

// 分頁查詢優化
[HttpGet]
public async Task<IActionResult> GetVocabularies(
    [FromQuery] int page = 1,
    [FromQuery] int size = 20)
{
    var result = await _service.GetVocabulariesAsync(page, size);
    return Ok(new PagedResponse<VocabularyDto>
    {
        Data = result.Items,
        Page = page,
        Size = size,
        Total = result.Total,
        HasNext = result.HasNext,
        HasPrevious = result.HasPrevious
    });
}

📝 開發指南

API測試

// 測試用例範例
describe('Vocabulary API', () => {
    test('should get vocabulary details', async () => {
        const vocabulary = await apiClient.getVocabulary(1);
        
        expect(vocabulary).toHaveProperty('word');
        expect(vocabulary).toHaveProperty('definition');
        expect(vocabulary).toHaveProperty('phonetic');
    });

    test('should handle API errors gracefully', async () => {
        try {
            await apiClient.getVocabulary(999999);
        } catch (error) {
            expect(error.message).toContain('404');
        }
    });
});

錯誤處理

// 統一錯誤處理
class ApiClient {
    async request(url, options = {}) {
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                const errorData = await response.json().catch(() => ({}));
                throw new ApiError(response.status, errorData.message || 'Request failed');
            }

            return await response.json();
        } catch (error) {
            if (error instanceof ApiError) {
                throw error;
            }
            
            throw new NetworkError('Network request failed', error);
        }
    }
}

class ApiError extends Error {
    constructor(status, message) {
        super(message);
        this.status = status;
        this.name = 'ApiError';
    }
}

class NetworkError extends Error {
    constructor(message, originalError) {
        super(message);
        this.originalError = originalError;
        this.name = 'NetworkError';
    }
}

最後更新: 2025-09-10
版本: 1.0 - 前後端分離架構策略
維護者: Drama Ling 開發團隊