dramaling-app/docs/04_technical/03_frontend/native-frontend-architectur...

1310 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Drama Ling 原生前端技術架構規劃
## 📋 架構概述
**專案名稱**: Drama Ling 語言學習應用 (Web端)
**建立日期**: 2025-09-10
**技術主軸**: 原生Web技術 (HTML5 + CSS3 + Modern JavaScript)
**對應後端**: .NET Core API
**部署目標**: 響應式Web應用程式
### 核心設計目標
- 🎯 **像素級精確**: 完全按照HTML原型設計實現無框架抽象干擾
- 📱 **響應式設計**: 桌面優先,兼容平板和手機
- 🚀 **輕量高效**: 無框架負擔,快速載入和流暢互動
- 🔒 **企業級安全**: 資料保護、安全認證
- 💎 **Claude Code友好**: 適合AI輔助開發易於理解和修改
## 🛠️ 技術堆疊
### 核心技術
```javascript
{
"markup": "HTML5", // 語義化標記,無障礙支援
"styling": "CSS3 + SCSS", // 現代CSS特性 + 預處理器
"scripting": "ES2022+", // 現代JavaScript特性
"bundler": "Vite 5.x", // 快速開發伺服器和打包工具
"typescript": "5.x" // 可選的強型別支援
}
```
### 開發工具鏈
```javascript
{
"dev_tools": {
"bundler": "Vite 5.x", // 快速HMR和打包
"css_preprocessor": "Sass/SCSS", // CSS預處理器
"linter": "ESLint 9.x", // JavaScript代碼檢查
"formatter": "Prettier 3.x", // 代碼格式化
"css_linter": "Stylelint 16.x", // CSS代碼檢查
"testing": "Vitest 1.6.x", // 快速單元測試
"e2e_testing": "Playwright" // 端到端測試
},
"build_optimization": {
"minification": "Terser", // JS壓縮
"css_optimization": "cssnano", // CSS優化
"image_optimization": "vite-plugin-imagemin",
"pwa_support": "@vite/plugin-pwa"
}
}
```
## 🏗️ 專案結構設計
### 整體目錄結構
```
web-frontend/
├── public/
│ ├── icons/ # PWA圖標和favicon
│ ├── assets/ # 靜態資源
│ │ ├── audio/ # 音頻檔案
│ │ ├── images/ # 圖片資源
│ │ └── fonts/ # 字體檔案
│ └── manifest.json # PWA配置
├── src/
│ ├── pages/ # 頁面HTML檔案
│ │ ├── home/ # 首頁相關
│ │ ├── auth/ # 認證相關頁面
│ │ ├── vocabulary/ # 詞彙學習頁面
│ │ ├── dialogue/ # 情境對話頁面
│ │ └── profile/ # 用戶相關頁面
│ ├── components/ # 可重用組件
│ │ ├── ui/ # 基礎UI組件
│ │ ├── layout/ # 佈局組件
│ │ └── business/ # 業務邏輯組件
│ ├── styles/ # 樣式檔案
│ │ ├── base/ # 基礎樣式
│ │ │ ├── reset.scss # CSS重置
│ │ │ ├── typography.scss # 字體樣式
│ │ │ └── variables.scss # SCSS變數
│ │ ├── components/ # 組件樣式
│ │ ├── pages/ # 頁面樣式
│ │ └── utilities/ # 工具類樣式
│ ├── scripts/ # JavaScript模組
│ │ ├── core/ # 核心功能
│ │ │ ├── router.js # 路由管理
│ │ │ ├── state.js # 狀態管理
│ │ │ ├── api.js # API客戶端
│ │ │ └── events.js # 事件系統
│ │ ├── modules/ # 功能模組
│ │ │ ├── auth/ # 認證模組
│ │ │ ├── vocabulary/ # 詞彙學習
│ │ │ ├── audio/ # 音頻處理
│ │ │ └── analytics/ # 數據分析
│ │ ├── utils/ # 工具函數
│ │ └── services/ # 業務服務
│ └── main.js # 應用入口
├── tests/ # 測試檔案
├── docs/ # 專案文檔
└── vite.config.js # Vite配置
```
### 模組化設計
```javascript
// modules/vocabulary/index.js
export class VocabularyModule {
constructor() {
this.state = new VocabularyState()
this.api = new VocabularyAPI()
this.ui = new VocabularyUI()
}
init() {
this.setupEventListeners()
this.loadInitialData()
}
setupEventListeners() {
// 事件監聽設置
}
}
// 每個模組包含完整的功能實現
// modules/vocabulary/
├── components/ # 詞彙相關組件
├── services/ # API服務
├── state/ # 狀態管理
├── utils/ # 工具函數
└── index.js # 模組入口
```
## 🔄 狀態管理架構
### 簡單狀態管理模式
```javascript
// core/state.js
class ApplicationState {
constructor() {
this.stores = {
auth: new AuthState(),
vocabulary: new VocabularyState(),
ui: new UIState(),
audio: new AudioState()
}
this.subscribers = new Map()
}
// 訂閱狀態變化
subscribe(storeName, callback) {
if (!this.subscribers.has(storeName)) {
this.subscribers.set(storeName, [])
}
this.subscribers.get(storeName).push(callback)
}
// 通知訂閱者
notify(storeName, data) {
const callbacks = this.subscribers.get(storeName) || []
callbacks.forEach(callback => callback(data))
}
// 更新狀態
updateStore(storeName, updater) {
const store = this.stores[storeName]
if (store) {
store.update(updater)
this.notify(storeName, store.getState())
}
}
}
// 認證狀態管理
class AuthState {
constructor() {
this.state = {
user: null,
token: localStorage.getItem('auth_token'),
isAuthenticated: false
}
this.checkAuthStatus()
}
update(updater) {
this.state = { ...this.state, ...updater(this.state) }
this.persistState()
}
persistState() {
if (this.state.token) {
localStorage.setItem('auth_token', this.state.token)
} else {
localStorage.removeItem('auth_token')
}
}
getState() {
return { ...this.state }
}
}
// 全域狀態實例
export const appState = new ApplicationState()
```
### 本地存儲策略
```javascript
// utils/storage.js
class StorageManager {
constructor() {
this.prefix = 'dramaling_'
}
// localStorage封裝
setLocal(key, value) {
try {
localStorage.setItem(
`${this.prefix}${key}`,
JSON.stringify(value)
)
} catch (error) {
console.error('LocalStorage write failed:', error)
}
}
getLocal(key, defaultValue = null) {
try {
const item = localStorage.getItem(`${this.prefix}${key}`)
return item ? JSON.parse(item) : defaultValue
} catch (error) {
console.error('LocalStorage read failed:', error)
return defaultValue
}
}
// sessionStorage封裝
setSession(key, value) {
try {
sessionStorage.setItem(
`${this.prefix}${key}`,
JSON.stringify(value)
)
} catch (error) {
console.error('SessionStorage write failed:', error)
}
}
getSession(key, defaultValue = null) {
try {
const item = sessionStorage.getItem(`${this.prefix}${key}`)
return item ? JSON.parse(item) : defaultValue
} catch (error) {
console.error('SessionStorage read failed:', error)
return defaultValue
}
}
}
export const storage = new StorageManager()
```
## 🎨 CSS架構設計
### SCSS組織結構
```scss
// styles/main.scss
// 1. 工具和變數
@import 'base/variables';
@import 'base/functions';
@import 'base/mixins';
// 2. 基礎樣式
@import 'base/reset';
@import 'base/typography';
@import 'base/layout';
// 3. 組件樣式
@import 'components/buttons';
@import 'components/forms';
@import 'components/cards';
@import 'components/modals';
// 4. 頁面樣式
@import 'pages/home';
@import 'pages/vocabulary';
@import 'pages/auth';
// 5. 工具類
@import 'utilities/spacing';
@import 'utilities/colors';
@import 'utilities/responsive';
```
### 變數系統
```scss
// styles/base/_variables.scss
// 色彩系統
$color-primary: #1976d2;
$color-primary-light: lighten($color-primary, 20%);
$color-primary-dark: darken($color-primary, 20%);
$color-secondary: #26a69a;
$color-accent: #9c27b0;
$color-success: #21ba45;
$color-warning: #f2c037;
$color-error: #c10015;
$color-info: #31ccec;
// 字體系統
$font-family-primary: 'Inter', 'Noto Sans TC', sans-serif;
$font-family-secondary: 'Roboto', 'Microsoft JhengHei', sans-serif;
$font-size-xs: 0.75rem; // 12px
$font-size-sm: 0.875rem; // 14px
$font-size-base: 1rem; // 16px
$font-size-lg: 1.125rem; // 18px
$font-size-xl: 1.25rem; // 20px
// 間距系統
$spacing-xs: 0.25rem; // 4px
$spacing-sm: 0.5rem; // 8px
$spacing-md: 1rem; // 16px
$spacing-lg: 1.5rem; // 24px
$spacing-xl: 2rem; // 32px
// 響應式斷點
$breakpoint-xs: 0;
$breakpoint-sm: 600px;
$breakpoint-md: 1024px;
$breakpoint-lg: 1440px;
$breakpoint-xl: 1920px;
```
### 響應式設計混合器
```scss
// styles/base/_mixins.scss
// 響應式斷點混合器
@mixin respond-to($breakpoint) {
@if $breakpoint == xs {
@media (max-width: #{$breakpoint-sm - 1px}) { @content; }
}
@if $breakpoint == sm {
@media (min-width: #{$breakpoint-sm}) and (max-width: #{$breakpoint-md - 1px}) { @content; }
}
@if $breakpoint == md {
@media (min-width: #{$breakpoint-md}) and (max-width: #{$breakpoint-lg - 1px}) { @content; }
}
@if $breakpoint == lg {
@media (min-width: #{$breakpoint-lg}) { @content; }
}
}
// CSS Grid佈局工具
@mixin grid-container($columns: 1, $gap: $spacing-md) {
display: grid;
grid-template-columns: repeat($columns, 1fr);
gap: $gap;
}
// 按鈕樣式基礎
@mixin button-base {
display: inline-flex;
align-items: center;
justify-content: center;
padding: $spacing-sm $spacing-md;
border: none;
border-radius: 4px;
font-family: $font-family-primary;
font-size: $font-size-base;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// 卡片組件樣式
@mixin card-shadow($level: 1) {
@if $level == 1 {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@if $level == 2 {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.08);
}
@if $level == 3 {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
```
## 🚦 路由和導航
### 簡單路由器實現
```javascript
// core/router.js
class SimpleRouter {
constructor() {
this.routes = new Map()
this.currentRoute = null
this.init()
}
init() {
window.addEventListener('popstate', () => {
this.handleRoute()
})
// 處理初始路由
this.handleRoute()
}
// 註冊路由
addRoute(path, handler, options = {}) {
this.routes.set(path, {
handler,
requiresAuth: options.requiresAuth || false,
title: options.title || 'Drama Ling'
})
}
// 導航到指定路由
navigate(path, state = {}) {
history.pushState(state, '', path)
this.handleRoute()
}
// 處理路由變化
handleRoute() {
const path = window.location.pathname
const route = this.findRoute(path)
if (route) {
// 檢查認證要求
if (route.requiresAuth && !this.checkAuth()) {
this.navigate('/login')
return
}
// 設置頁面標題
document.title = route.title
// 執行路由處理器
route.handler(path)
this.currentRoute = path
} else {
this.handle404()
}
}
// 查找匹配的路由
findRoute(path) {
// 精確匹配
if (this.routes.has(path)) {
return this.routes.get(path)
}
// 動態路由匹配 (例如: /vocabulary/:id)
for (const [pattern, route] of this.routes) {
const regex = this.pathToRegex(pattern)
if (regex.test(path)) {
return route
}
}
return null
}
// 路徑轉正則表達式
pathToRegex(path) {
const regexPath = path
.replace(/:\w+/g, '([^/]+)') // :id -> ([^/]+)
.replace(/\*/g, '.*') // * -> .*
return new RegExp(`^${regexPath}$`)
}
// 檢查用戶認證狀態
checkAuth() {
return appState.stores.auth.getState().isAuthenticated
}
// 處理404錯誤
handle404() {
document.title = 'Page Not Found - Drama Ling'
// 顯示404頁面
}
}
// 路由配置
export const router = new SimpleRouter()
// 註冊路由
router.addRoute('/', () => import('../pages/home/index.js'), {
title: 'Drama Ling - AI語言學習'
})
router.addRoute('/login', () => import('../pages/auth/login.js'), {
title: '登入 - Drama Ling'
})
router.addRoute('/vocabulary', () => import('../pages/vocabulary/index.js'), {
requiresAuth: true,
title: '詞彙學習 - Drama Ling'
})
router.addRoute('/vocabulary/:id', (path) => {
const id = path.split('/').pop()
import('../pages/vocabulary/detail.js').then(module => {
module.default.init(id)
})
}, {
requiresAuth: true,
title: '詞彙詳情 - Drama Ling'
})
```
### 導航組件
```javascript
// components/layout/Navigation.js
export class Navigation {
constructor() {
this.element = null
this.init()
}
init() {
this.render()
this.setupEventListeners()
}
render() {
this.element = document.createElement('nav')
this.element.className = 'main-navigation'
this.element.innerHTML = `
<div class="nav-container">
<div class="nav-brand">
<a href="/" data-navigate>Drama Ling</a>
</div>
<ul class="nav-menu">
<li><a href="/vocabulary" data-navigate>詞彙學習</a></li>
<li><a href="/dialogue" data-navigate>情境對話</a></li>
<li><a href="/profile" data-navigate>個人中心</a></li>
</ul>
<div class="nav-actions">
<button id="logout-btn" class="btn btn-outline">登出</button>
</div>
</div>
`
}
setupEventListeners() {
// 導航點擊處理
this.element.addEventListener('click', (e) => {
const link = e.target.closest('[data-navigate]')
if (link) {
e.preventDefault()
const path = link.getAttribute('href')
router.navigate(path)
}
})
// 登出處理
this.element.querySelector('#logout-btn').addEventListener('click', () => {
appState.updateStore('auth', (state) => ({
...state,
user: null,
token: null,
isAuthenticated: false
}))
router.navigate('/login')
})
}
mount(container) {
container.appendChild(this.element)
}
}
```
## 🔌 API服務層架構
### HTTP客戶端
```javascript
// services/httpClient.js
class HttpClient {
constructor() {
this.baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
this.timeout = 10000
}
// 通用請求方法
async request(url, options = {}) {
const config = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
...options.headers
},
timeout: this.timeout,
...options
}
try {
const response = await fetch(`${this.baseURL}${url}`, config)
return await this.handleResponse(response)
} catch (error) {
throw this.handleError(error)
}
}
// 獲取認證標頭
getAuthHeaders() {
const token = appState.stores.auth.getState().token
return token ? { Authorization: `Bearer ${token}` } : {}
}
// 處理回應
async handleResponse(response) {
if (!response.ok) {
if (response.status === 401) {
// 處理認證過期
appState.updateStore('auth', (state) => ({
...state,
user: null,
token: null,
isAuthenticated: false
}))
router.navigate('/login')
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return await response.json()
}
return await response.text()
}
// 錯誤處理
handleError(error) {
console.error('API Error:', error)
return error
}
// 便捷方法
get(url, params = {}) {
const queryString = new URLSearchParams(params).toString()
const fullUrl = queryString ? `${url}?${queryString}` : url
return this.request(fullUrl)
}
post(url, data = {}) {
return this.request(url, {
method: 'POST',
body: JSON.stringify(data)
})
}
put(url, data = {}) {
return this.request(url, {
method: 'PUT',
body: JSON.stringify(data)
})
}
delete(url) {
return this.request(url, {
method: 'DELETE'
})
}
}
export const httpClient = new HttpClient()
```
### API服務類別
```javascript
// services/vocabularyApi.js
class VocabularyAPI {
constructor() {
this.basePath = '/vocabulary'
}
// 獲取詞彙介紹
async getWordIntroduction(wordId) {
return await httpClient.get(`${this.basePath}/words/${wordId}`)
}
// 提交練習結果
async submitPracticeResult(result) {
return await httpClient.post(`${this.basePath}/practice`, result)
}
// 獲取複習計劃
async getReviewSchedule() {
return await httpClient.get(`${this.basePath}/review/schedule`)
}
// 獲取分析數據
async getAnalytics(timeRange) {
return await httpClient.get(`${this.basePath}/analytics`, { timeRange })
}
}
export const vocabularyAPI = new VocabularyAPI()
```
## 🎵 音頻處理整合
### Web Audio API封裝
```javascript
// modules/audio/audioManager.js
class AudioManager {
constructor() {
this.audioContext = null
this.currentSource = null
this.isPlaying = false
}
// 初始化音頻上下文
async initializeAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
// 處理瀏覽器自動播放政策
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume()
}
}
}
// 播放音頻
async playAudio(url, playbackRate = 1.0) {
try {
await this.initializeAudioContext()
// 停止當前播放
this.stopCurrentAudio()
// 載入音頻
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
// 創建音頻源
this.currentSource = this.audioContext.createBufferSource()
this.currentSource.buffer = audioBuffer
this.currentSource.playbackRate.value = playbackRate
this.currentSource.connect(this.audioContext.destination)
// 播放結束處理
this.currentSource.onended = () => {
this.isPlaying = false
this.currentSource = null
}
// 開始播放
this.currentSource.start()
this.isPlaying = true
return this.currentSource
} catch (error) {
console.error('Audio playback failed:', error)
this.isPlaying = false
throw error
}
}
// 停止當前播放
stopCurrentAudio() {
if (this.currentSource && this.isPlaying) {
this.currentSource.stop()
this.currentSource = null
this.isPlaying = false
}
}
// 錄製音頻
async startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const mediaRecorder = new MediaRecorder(stream)
const audioChunks = []
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data)
}
return new Promise((resolve, reject) => {
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' })
resolve(audioBlob)
}
mediaRecorder.onerror = (event) => {
reject(event.error)
}
mediaRecorder.start()
// 返回停止函數
setTimeout(() => {
mediaRecorder.stop()
stream.getTracks().forEach(track => track.stop())
}, 5000) // 5秒自動停止
})
} catch (error) {
console.error('Recording failed:', error)
throw error
}
}
}
export const audioManager = new AudioManager()
```
## 🧩 組件系統
### Web Components基礎
```javascript
// components/ui/BaseComponent.js
class BaseComponent extends HTMLElement {
constructor() {
super()
this.state = {}
this.listeners = new Map()
}
// 組件生命週期
connectedCallback() {
this.render()
this.setupEventListeners()
this.onMounted()
}
disconnectedCallback() {
this.cleanup()
this.onUnmounted()
}
// 狀態管理
setState(newState) {
this.state = { ...this.state, ...newState }
this.render()
}
getState() {
return { ...this.state }
}
// 事件處理
addEventListener(event, handler) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event).push(handler)
super.addEventListener(event, handler)
}
// 清理資源
cleanup() {
for (const [event, handlers] of this.listeners) {
handlers.forEach(handler => {
super.removeEventListener(event, handler)
})
}
this.listeners.clear()
}
// 子類需要實現的方法
render() {
throw new Error('render method must be implemented')
}
onMounted() {
// 組件掛載後的邏輯
}
onUnmounted() {
// 組件卸載前的清理
}
setupEventListeners() {
// 事件監聽設置
}
}
export { BaseComponent }
```
### 詞彙卡片組件範例
```javascript
// components/business/VocabularyCard.js
import { BaseComponent } from '../ui/BaseComponent.js'
class VocabularyCard extends BaseComponent {
constructor() {
super()
this.state = {
word: null,
isFlipped: false,
isLoading: false
}
}
static get observedAttributes() {
return ['word-id', 'show-pronunciation', 'interactive']
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'word-id' && newValue !== oldValue) {
this.loadWord(newValue)
}
}
async loadWord(wordId) {
this.setState({ isLoading: true })
try {
const word = await vocabularyAPI.getWordIntroduction(wordId)
this.setState({ word, isLoading: false })
} catch (error) {
console.error('Failed to load word:', error)
this.setState({ isLoading: false })
}
}
render() {
if (this.state.isLoading) {
this.innerHTML = `
<div class="vocabulary-card loading">
<div class="loading-spinner"></div>
</div>
`
return
}
if (!this.state.word) {
this.innerHTML = `
<div class="vocabulary-card error">
<p>無法載入詞彙資料</p>
</div>
`
return
}
const { word } = this.state
const showPronunciation = this.getAttribute('show-pronunciation') !== 'false'
const interactive = this.getAttribute('interactive') !== 'false'
this.innerHTML = `
<div class="vocabulary-card ${this.state.isFlipped ? 'flipped' : ''}">
<div class="card-front">
<div class="word-header">
<h3 class="word-text">${word.word}</h3>
${showPronunciation ? `<span class="pronunciation">${word.pronunciation}</span>` : ''}
</div>
${interactive ? `
<button class="audio-btn" data-audio-url="${word.audio_url}">
🔊 播放發音
</button>
<button class="flip-btn">
顯示定義
</button>
` : ''}
</div>
<div class="card-back">
<div class="definition">
<h4>定義</h4>
<p>${word.definition_zh}</p>
</div>
<div class="examples">
<h4>例句</h4>
${word.examples.map(example => `
<p class="example">${example.sentence}</p>
`).join('')}
</div>
${interactive ? `
<button class="flip-btn">
返回
</button>
` : ''}
</div>
</div>
`
}
setupEventListeners() {
this.addEventListener('click', (e) => {
if (e.target.matches('.flip-btn')) {
this.setState({ isFlipped: !this.state.isFlipped })
}
if (e.target.matches('.audio-btn')) {
const audioUrl = e.target.getAttribute('data-audio-url')
audioManager.playAudio(audioUrl)
}
})
}
}
// 註冊自定義元素
customElements.define('vocabulary-card', VocabularyCard)
export { VocabularyCard }
```
## 🚀 效能優化策略
### 載入優化
```javascript
// utils/lazyLoader.js
class LazyLoader {
constructor() {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{ threshold: 0.1 }
)
}
// 延遲載入圖片
lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]')
images.forEach(img => this.observer.observe(img))
}
// 延遲載入組件
lazyLoadComponent(element, importFunction) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
importFunction().then(module => {
module.default.init(element)
})
observer.disconnect()
}
})
},
{ threshold: 0.1 }
)
observer.observe(element)
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.getAttribute('data-src')
img.removeAttribute('data-src')
this.observer.unobserve(img)
}
})
}
}
export const lazyLoader = new LazyLoader()
```
### 快取策略
```javascript
// utils/cache.js
class CacheManager {
constructor() {
this.memoryCache = new Map()
this.maxAge = 5 * 60 * 1000 // 5分鐘
}
// 記憶體快取
setMemoryCache(key, data, maxAge = this.maxAge) {
this.memoryCache.set(key, {
data,
timestamp: Date.now(),
maxAge
})
}
getMemoryCache(key) {
const cached = this.memoryCache.get(key)
if (!cached) return null
if (Date.now() - cached.timestamp > cached.maxAge) {
this.memoryCache.delete(key)
return null
}
return cached.data
}
// localStorage快取
setLocalCache(key, data, maxAge = 24 * 60 * 60 * 1000) {
const cacheData = {
data,
timestamp: Date.now(),
maxAge
}
storage.setLocal(`cache_${key}`, cacheData)
}
getLocalCache(key) {
const cached = storage.getLocal(`cache_${key}`)
if (!cached) return null
if (Date.now() - cached.timestamp > cached.maxAge) {
storage.removeLocal(`cache_${key}`)
return null
}
return cached.data
}
}
export const cacheManager = new CacheManager()
```
## 🔒 安全性實作
### XSS防護
```javascript
// utils/security.js
class SecurityUtils {
// HTML清理
sanitizeHTML(html) {
const temp = document.createElement('div')
temp.textContent = html
return temp.innerHTML
}
// 輸入驗證
validateInput(input, type = 'text') {
const patterns = {
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
phone: /^\+?[\d\s\-\(\)]+$/,
username: /^[a-zA-Z0-9_]{3,20}$/,
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/
}
if (patterns[type]) {
return patterns[type].test(input)
}
// 基本長度檢查
return input.length > 0 && input.length < 1000
}
// CSRF Token處理
async getCSRFToken() {
try {
const response = await httpClient.get('/csrf-token')
return response.token
} catch (error) {
console.error('Failed to get CSRF token:', error)
return null
}
}
}
export const security = new SecurityUtils()
```
## 🧪 測試策略
### Vitest配置
```javascript
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'happy-dom',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary', 'html'],
threshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
}
})
```
### 組件測試範例
```javascript
// tests/components/VocabularyCard.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { VocabularyCard } from '../../src/components/business/VocabularyCard.js'
describe('VocabularyCard', () => {
let container
let vocabularyCard
beforeEach(() => {
// 設置DOM容器
container = document.createElement('div')
document.body.appendChild(container)
// 創建組件實例
vocabularyCard = document.createElement('vocabulary-card')
container.appendChild(vocabularyCard)
})
afterEach(() => {
// 清理DOM
document.body.removeChild(container)
})
it('should render word information correctly', () => {
vocabularyCard.setAttribute('word-id', 'test-word')
// 模擬API回應
vi.mock('../../src/services/vocabularyApi.js', () => ({
vocabularyAPI: {
getWordIntroduction: vi.fn().mockResolvedValue({
word: 'hello',
pronunciation: '/həˈloʊ/',
definition_zh: '你好',
examples: [
{ sentence: 'Hello, world!' }
]
})
}
}))
// 等待組件渲染
return new Promise(resolve => {
setTimeout(() => {
expect(vocabularyCard.querySelector('.word-text')).toHaveTextContent('hello')
expect(vocabularyCard.querySelector('.pronunciation')).toHaveTextContent('/həˈloʊ/')
resolve()
}, 100)
})
})
})
```
## 📦 建構和部署
### Vite建構配置
```javascript
// vite.config.js
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: 'Drama Ling',
short_name: 'DramaLing',
description: 'AI-powered language learning app',
theme_color: '#1976d2',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
}
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vite'],
utils: ['./src/utils/index.js']
}
}
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/styles/base/variables.scss";`
}
}
}
})
```
---
**文檔狀態**: 🟢 已完成原生前端技術架構規劃
**最後更新**: 2025-09-10
**負責團隊**: 前端開發團隊
**下次檢查**: 開發開始前進行技術實施確認