31 KiB
31 KiB
Drama Ling 原生前端技術架構規劃
📋 架構概述
專案名稱: Drama Ling 語言學習應用 (Web端)
建立日期: 2025-09-10
技術主軸: 原生Web技術 (HTML5 + CSS3 + Modern JavaScript)
對應後端: .NET Core API
部署目標: 響應式Web應用程式
核心設計目標
- 🎯 像素級精確: 完全按照HTML原型設計實現,無框架抽象干擾
- 📱 響應式設計: 桌面優先,兼容平板和手機
- 🚀 輕量高效: 無框架負擔,快速載入和流暢互動
- 🔒 企業級安全: 資料保護、安全認證
- 💎 Claude Code友好: 適合AI輔助開發,易於理解和修改
🛠️ 技術堆疊
核心技術
{
"markup": "HTML5", // 語義化標記,無障礙支援
"styling": "CSS3 + SCSS", // 現代CSS特性 + 預處理器
"scripting": "ES2022+", // 現代JavaScript特性
"bundler": "Vite 5.x", // 快速開發伺服器和打包工具
"typescript": "5.x" // 可選的強型別支援
}
開發工具鏈
{
"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配置
模組化設計
// 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 # 模組入口
🔄 狀態管理架構
簡單狀態管理模式
// 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()
本地存儲策略
// 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組織結構
// 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';
變數系統
// 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;
響應式設計混合器
// 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);
}
}
🚦 路由和導航
簡單路由器實現
// 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'
})
導航組件
// 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客戶端
// 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服務類別
// 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封裝
// 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基礎
// 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 }
詞彙卡片組件範例
// 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 }
🚀 效能優化策略
載入優化
// 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()
快取策略
// 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防護
// 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配置
// 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
}
}
}
}
})
組件測試範例
// 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建構配置
// 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
負責團隊: 前端開發團隊
下次檢查: 開發開始前進行技術實施確認