635 lines
16 KiB
Markdown
635 lines
16 KiB
Markdown
# 前後端分離架構策略
|
||
|
||
**文件版本**: 1.0
|
||
**建立日期**: 2025-09-10
|
||
**更新日期**: 2025-09-10
|
||
**負責人**: Drama Ling 開發團隊
|
||
|
||
## 📋 概述
|
||
|
||
Drama Ling 採用**前後端分離架構**,後端提供統一的API服務,同時支援Web前端和Flutter移動端。這種架構確保了API的一致性,並為未來的擴展奠定了基礎。
|
||
|
||
## 🏗 整體架構設計
|
||
|
||
### 架構圖
|
||
|
||
```mermaid
|
||
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架構)
|
||
|
||
#### 技術棧
|
||
```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整合範例
|
||
```javascript
|
||
// 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移動端
|
||
|
||
#### 技術棧
|
||
```yaml
|
||
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整合範例
|
||
```dart
|
||
// 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/*`)
|
||
```csharp
|
||
[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/*`)
|
||
```csharp
|
||
[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/*`)
|
||
```csharp
|
||
[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();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 認證策略
|
||
|
||
#### 雙重認證系統
|
||
```csharp
|
||
// 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. **本地開發環境**
|
||
```bash
|
||
# 啟動前端開發伺服器
|
||
cd apps/web-native
|
||
npm install
|
||
npm run dev # http://localhost:3000
|
||
|
||
# 啟動後端API服務
|
||
cd ../../backend
|
||
dotnet run # http://localhost:5000
|
||
```
|
||
|
||
2. **API測試和整合**
|
||
```javascript
|
||
// 開發時的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;
|
||
```
|
||
|
||
3. **CORS配置 (後端)**
|
||
```csharp
|
||
// Program.cs
|
||
builder.Services.AddCors(options =>
|
||
{
|
||
options.AddDefaultPolicy(builder =>
|
||
{
|
||
builder
|
||
.WithOrigins(
|
||
"http://localhost:3000", // 前端開發環境
|
||
"https://app.dramaling.com" // 生產環境
|
||
)
|
||
.AllowAnyHeader()
|
||
.AllowAnyMethod()
|
||
.AllowCredentials();
|
||
});
|
||
});
|
||
|
||
app.UseCors();
|
||
```
|
||
|
||
### 部署策略
|
||
|
||
#### 開發環境
|
||
```yaml
|
||
前端:
|
||
- 開發伺服器: Vite dev server (http://localhost:3000)
|
||
- 熱重載: 支援
|
||
- API代理: 配置到後端開發服務
|
||
|
||
後端:
|
||
- 開發伺服器: dotnet run (http://localhost:5000)
|
||
- 資料庫: 本地PostgreSQL
|
||
- 快取: 本地Redis
|
||
```
|
||
|
||
#### 生產環境
|
||
```yaml
|
||
前端:
|
||
- 構建: 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. 一致的回應格式
|
||
```csharp
|
||
// 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. 版本控制策略
|
||
```csharp
|
||
[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 實現,保持向後相容
|
||
}
|
||
```
|
||
|
||
## 🚀 效能優化
|
||
|
||
### 前端優化
|
||
```javascript
|
||
// 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');
|
||
```
|
||
|
||
### 後端優化
|
||
```csharp
|
||
// 快取策略
|
||
[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測試
|
||
```javascript
|
||
// 測試用例範例
|
||
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');
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
### 錯誤處理
|
||
```javascript
|
||
// 統一錯誤處理
|
||
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 開發團隊 |