# 原生前端開發規範和最佳實踐 ## 📋 總體指導原則 ### 核心原則 1. **語義化優先**: HTML結構語義化,提升可讀性和無障礙性 2. **漸進增強**: 基於內容層、表現層、行為層的分離原則 3. **效能考量**: 避免不必要的DOM操作和記憶體洩漏 4. **可維護性**: 編寫易於理解、修改和擴展的代碼 5. **標準相容**: 遵循Web標準,確保跨瀏覽器相容性 ### 技術選型標準 - **HTML5**: 語義化標記,現代HTML特性 - **CSS3**: 現代CSS特性,SCSS預處理器 - **ES2022+**: 現代JavaScript特性,可選TypeScript - **Web Components**: 自定義元素,封裝重用組件 - **Progressive Enhancement**: 漸進增強的設計理念 ## 🏗️ HTML 開發規範 ### 文檔結構標準 #### 基本文檔模板 ```html 頁面標題 - Drama Ling 跳至主要內容
``` ### 語義化標記規範 #### 正確使用語義化元素 ```html

今日詞彙

2025年9月10日

單字介紹

這個單字的定義是...

學習筆記

重點提醒...

今日詞彙
2025年9月10日
單字介紹
這個單字的定義是...
``` #### 表單設計規範 ```html
登入資訊
密碼需要至少8個字符
``` ### 無障礙設計規範 #### ARIA屬性使用 ```html

Hello

/həˈloʊ/

你好,哈囉

練習完成

恭喜你完成了這個練習!

``` ## 🎨 CSS 開發規範 ### SCSS 組織結構 #### 模組化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. 組件層 - UI組件樣式 @import 'components/buttons'; @import 'components/forms'; @import 'components/cards'; @import 'components/navigation'; @import 'components/modals'; // 4. 頁面層 - 頁面特定樣式 @import 'pages/home'; @import 'pages/vocabulary'; @import 'pages/auth'; // 5. 工具層 - 工具類 @import 'utilities/spacing'; @import 'utilities/colors'; @import 'utilities/typography'; @import 'utilities/responsive'; ``` ### 命名規範 #### BEM方法論 ```scss // ✅ 正確:BEM命名 .vocabulary-card { display: flex; padding: $spacing-md; &__header { display: flex; justify-content: space-between; margin-bottom: $spacing-sm; &--highlighted { background-color: $color-primary-light; } } &__content { flex: 1; } &__word { font-size: $font-size-lg; font-weight: 600; color: $color-primary; &--difficult { color: $color-warning; } } &--disabled { opacity: 0.5; pointer-events: none; } &--loading { position: relative; &::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; border: 2px solid $color-primary; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; } } } // ❌ 錯誤:嵌套過深和命名不清楚 .vocabulary-card { .header { .title { .word { .text { color: blue; // 硬編碼顏色 } } } } } ``` #### CSS自定義屬性(CSS Variables) ```scss // styles/base/_variables.scss :root { // 色彩系統 --color-primary: #1976d2; --color-primary-light: #42a5f5; --color-primary-dark: #1565c0; --color-secondary: #26a69a; --color-accent: #9c27b0; --color-success: #21ba45; --color-warning: #f2c037; --color-error: #c10015; --color-info: #31ccec; // 灰階 --color-grey-50: #fafafa; --color-grey-100: #f5f5f5; --color-grey-200: #eeeeee; --color-grey-300: #e0e0e0; --color-grey-400: #bdbdbd; --color-grey-500: #9e9e9e; // 字體系統 --font-family-primary: 'Inter', 'Noto Sans TC', sans-serif; --font-family-secondary: 'Roboto', 'Microsoft JhengHei', sans-serif; --font-family-mono: 'JetBrains Mono', 'Consolas', monospace; --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 --font-size-2xl: 1.5rem; // 24px --font-size-3xl: 1.875rem; // 30px // 間距系統 --spacing-xs: 0.25rem; // 4px --spacing-sm: 0.5rem; // 8px --spacing-md: 1rem; // 16px --spacing-lg: 1.5rem; // 24px --spacing-xl: 2rem; // 32px --spacing-2xl: 3rem; // 48px --spacing-3xl: 4rem; // 64px // 邊框圓角 --border-radius-sm: 0.25rem; // 4px --border-radius-md: 0.5rem; // 8px --border-radius-lg: 0.75rem; // 12px --border-radius-xl: 1rem; // 16px --border-radius-full: 9999px; // 陰影系統 --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); // 轉場動畫 --transition-fast: 150ms ease; --transition-base: 200ms ease; --transition-slow: 300ms ease; // Z-index層級 --z-dropdown: 1000; --z-sticky: 1020; --z-fixed: 1030; --z-modal: 1040; --z-popover: 1050; --z-tooltip: 1060; --z-toast: 1070; } // 深色模式支援 @media (prefers-color-scheme: dark) { :root { --color-primary: #42a5f5; --color-primary-light: #90caf9; --color-primary-dark: #1976d2; // 重新定義深色模式下的色彩 } } ``` ### 響應式設計規範 #### Mobile-First 方法 ```scss // styles/base/_mixins.scss // 響應式斷點 $breakpoints: ( 'xs': 0, 'sm': 600px, 'md': 1024px, 'lg': 1440px, 'xl': 1920px ); // 響應式混合器 @mixin respond-above($breakpoint) { $bp-value: map-get($breakpoints, $breakpoint); @if $bp-value == 0 { @content; } @else { @media (min-width: $bp-value) { @content; } } } @mixin respond-below($breakpoint) { $bp-value: map-get($breakpoints, $breakpoint); @media (max-width: $bp-value - 1px) { @content; } } @mixin respond-between($lower, $upper) { $lower-bp: map-get($breakpoints, $lower); $upper-bp: map-get($breakpoints, $upper); @media (min-width: $lower-bp) and (max-width: $upper-bp - 1px) { @content; } } // 使用範例 .vocabulary-grid { display: grid; gap: var(--spacing-md); // Mobile First - 預設單欄 grid-template-columns: 1fr; // 平板:雙欄 @include respond-above('sm') { grid-template-columns: repeat(2, 1fr); } // 桌面:三欄 @include respond-above('md') { grid-template-columns: repeat(3, 1fr); gap: var(--spacing-lg); } // 大螢幕:四欄 @include respond-above('lg') { grid-template-columns: repeat(4, 1fr); } } ``` #### CSS Grid 和 Flexbox 最佳實踐 ```scss // Flexbox工具類 .flex { display: flex; &-col { flex-direction: column; } &-row { flex-direction: row; } &-wrap { flex-wrap: wrap; } &-nowrap { flex-wrap: nowrap; } &-justify-start { justify-content: flex-start; } &-justify-center { justify-content: center; } &-justify-end { justify-content: flex-end; } &-justify-between { justify-content: space-between; } &-justify-around { justify-content: space-around; } &-items-start { align-items: flex-start; } &-items-center { align-items: center; } &-items-end { align-items: flex-end; } &-items-stretch { align-items: stretch; } } // CSS Grid工具類 .grid { display: grid; &-cols-1 { grid-template-columns: repeat(1, 1fr); } &-cols-2 { grid-template-columns: repeat(2, 1fr); } &-cols-3 { grid-template-columns: repeat(3, 1fr); } &-cols-4 { grid-template-columns: repeat(4, 1fr); } &-rows-1 { grid-template-rows: repeat(1, 1fr); } &-rows-2 { grid-template-rows: repeat(2, 1fr); } &-rows-3 { grid-template-rows: repeat(3, 1fr); } &-gap-sm { gap: var(--spacing-sm); } &-gap-md { gap: var(--spacing-md); } &-gap-lg { gap: var(--spacing-lg); } } // 實際應用範例 .page-layout { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; grid-template-areas: "header" "main" "footer"; .site-header { grid-area: header; } .main-content { grid-area: main; padding: var(--spacing-lg); } .site-footer { grid-area: footer; } } ``` ## 📜 JavaScript 開發規範 ### 代碼組織和模組化 #### ES6模組系統 ```javascript // ✅ 正確:清晰的導入導出 // utils/api.js export class APIClient { constructor(baseURL) { this.baseURL = baseURL } async get(endpoint, options = {}) { // 實作 } } export const httpClient = new APIClient('/api') // services/vocabularyService.js import { httpClient } from '../utils/api.js' export class VocabularyService { async getWord(id) { return await httpClient.get(`/vocabulary/${id}`) } } // 默認導出 export default VocabularyService // ❌ 錯誤:混亂的導入導出 export { APIClient, httpClient, VocabularyService, someOtherThing } ``` ### 函數和類別設計規範 #### 函數設計原則 ```javascript // ✅ 正確:單一職責、純函數 function calculateWordDifficulty(word, userLevel, attempts) { if (!word || userLevel < 1 || attempts < 0) { throw new Error('Invalid parameters for difficulty calculation') } const baseDifficulty = word.difficulty || 1 const levelAdjustment = Math.max(0, baseDifficulty - userLevel) const attemptBonus = Math.min(0.5, attempts * 0.1) return Math.max(0.1, levelAdjustment - attemptBonus) } // 高階函數範例 function createThrottledFunction(func, delay) { let timeoutId = null let lastArgs = null return function(...args) { lastArgs = args if (timeoutId === null) { timeoutId = setTimeout(() => { func.apply(this, lastArgs) timeoutId = null }, delay) } } } // 使用範例 const throttledSaveProgress = createThrottledFunction(saveProgress, 1000) // ❌ 錯誤:職責不明確、副作用多 function processWord(word) { // 計算難度 const difficulty = word.difficulty - user.level + attempts * 0.1 // 更新UI - 不應該在這個函數中 document.getElementById('difficulty').textContent = difficulty // 發送API請求 - 不應該在這個函數中 fetch('/api/update-difficulty', { method: 'POST', body: JSON.stringify({ wordId: word.id, difficulty }) }) // 記錄日誌 - 副作用 console.log('Processed word:', word.id) return difficulty } ``` #### 類別設計規範 ```javascript // ✅ 正確:清晰的類別設計 class VocabularyCard { #state = {} // 私有屬性 #observers = new Set() // 私有屬性 constructor(element, options = {}) { this.element = element this.options = { ...this.constructor.defaults, ...options } this.#state = this.#initializeState() this.#bindEventListeners() } // 靜態屬性 static defaults = { showPronunciation: true, enableAudio: true, flipOnClick: true } // 公開方法 setState(newState) { const oldState = { ...this.#state } this.#state = { ...this.#state, ...newState } this.#notifyObservers(oldState, this.#state) this.render() } getState() { return { ...this.#state } // 返回副本 } subscribe(observer) { this.#observers.add(observer) return () => this.#observers.delete(observer) // 返回取消訂閱函數 } destroy() { this.#cleanup() this.#observers.clear() } // 私有方法 #initializeState() { return { isFlipped: false, isLoading: false, word: null, error: null } } #bindEventListeners() { this.#handleClick = this.#handleClick.bind(this) this.element.addEventListener('click', this.#handleClick) } #handleClick(event) { if (this.options.flipOnClick && !this.#state.isLoading) { this.setState({ isFlipped: !this.#state.isFlipped }) } } #notifyObservers(oldState, newState) { for (const observer of this.#observers) { try { observer(newState, oldState) } catch (error) { console.error('Observer error:', error) } } } #cleanup() { this.element.removeEventListener('click', this.#handleClick) } render() { // 渲染邏輯 if (this.#state.isLoading) { this.element.classList.add('loading') } else { this.element.classList.remove('loading') } if (this.#state.isFlipped) { this.element.classList.add('flipped') } else { this.element.classList.remove('flipped') } } } ``` ### 異步程式設計規範 #### Promise 和 async/await ```javascript // ✅ 正確:清晰的異步處理 class VocabularyAPI { constructor() { this.baseURL = '/api/vocabulary' this.cache = new Map() } async getWord(id, options = {}) { const { useCache = true, timeout = 5000 } = options // 檢查快取 if (useCache && this.cache.has(id)) { return this.cache.get(id) } try { const response = await this.#fetchWithTimeout( `${this.baseURL}/words/${id}`, { timeout } ) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const word = await response.json() // 驗證數據結構 this.#validateWordData(word) // 更新快取 if (useCache) { this.cache.set(id, word) } return word } catch (error) { console.error(`Failed to fetch word ${id}:`, error) throw new APIError(`Failed to load word: ${error.message}`, { cause: error, wordId: id }) } } async #fetchWithTimeout(url, { timeout, ...options } = {}) { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) try { const response = await fetch(url, { ...options, signal: controller.signal }) return response } finally { clearTimeout(timeoutId) } } #validateWordData(word) { const required = ['id', 'word', 'definition_zh'] const missing = required.filter(field => !(field in word)) if (missing.length > 0) { throw new Error(`Missing required fields: ${missing.join(', ')}`) } } } // 自定義錯誤類型 class APIError extends Error { constructor(message, options = {}) { super(message, options) this.name = 'APIError' this.timestamp = Date.now() } } // ❌ 錯誤:錯誤處理不當 async function getWord(id) { const response = await fetch(`/api/words/${id}`) // 沒有錯誤處理 const word = await response.json() // 沒有檢查回應狀態 return word // 沒有數據驗證 } ``` #### 事件系統設計 ```javascript // 自定義事件發射器 class EventEmitter { constructor() { this.events = new Map() } on(eventName, listener) { if (!this.events.has(eventName)) { this.events.set(eventName, new Set()) } this.events.get(eventName).add(listener) // 返回取消監聽函數 return () => this.off(eventName, listener) } once(eventName, listener) { const onceListener = (...args) => { this.off(eventName, onceListener) listener.apply(this, args) } return this.on(eventName, onceListener) } off(eventName, listener) { const listeners = this.events.get(eventName) if (listeners) { listeners.delete(listener) if (listeners.size === 0) { this.events.delete(eventName) } } } emit(eventName, ...args) { const listeners = this.events.get(eventName) if (listeners) { for (const listener of listeners) { try { listener.apply(this, args) } catch (error) { console.error(`Error in event listener for "${eventName}":`, error) } } } } removeAllListeners(eventName) { if (eventName) { this.events.delete(eventName) } else { this.events.clear() } } } // 使用範例 const learningEvents = new EventEmitter() // 監聽事件 const unsubscribe = learningEvents.on('wordCompleted', (word, score) => { console.log(`Completed word: ${word.text}, Score: ${score}`) updateProgress(word, score) }) // 觸發事件 learningEvents.emit('wordCompleted', currentWord, 85) // 清理 unsubscribe() ``` ### DOM 操作最佳實踐 #### 安全的DOM操作 ```javascript // ✅ 正確:安全的DOM操作 class DOMHelper { static createElement(tag, options = {}) { const element = document.createElement(tag) if (options.className) { element.className = options.className } if (options.attributes) { Object.entries(options.attributes).forEach(([key, value]) => { element.setAttribute(key, value) }) } if (options.textContent) { element.textContent = options.textContent // 安全地設置文字 } if (options.innerHTML) { // 清理HTML內容 element.innerHTML = this.sanitizeHTML(options.innerHTML) } return element } static sanitizeHTML(html) { // 簡單的HTML清理(實際專案中應使用專業庫如DOMPurify) const temp = document.createElement('div') temp.textContent = html return temp.innerHTML } static findElement(selector, context = document) { const element = context.querySelector(selector) if (!element) { throw new Error(`Element not found: ${selector}`) } return element } static findElements(selector, context = document) { return Array.from(context.querySelectorAll(selector)) } static onReady(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback) } else { callback() } } } // 批量DOM更新 class DOMBatcher { constructor() { this.updates = [] this.isScheduled = false } schedule(updateFunction) { this.updates.push(updateFunction) if (!this.isScheduled) { this.isScheduled = true requestAnimationFrame(() => { this.flush() }) } } flush() { const updates = this.updates.splice(0) updates.forEach(update => { try { update() } catch (error) { console.error('DOM update error:', error) } }) this.isScheduled = false } } const domBatcher = new DOMBatcher() // 使用範例 function updateWordCards(words) { words.forEach(word => { domBatcher.schedule(() => { const cardElement = document.getElementById(`word-${word.id}`) if (cardElement) { cardElement.textContent = word.text cardElement.className = `word-card difficulty-${word.difficulty}` } }) }) } // ❌ 錯誤:不安全的DOM操作 function updateCard(word) { const card = document.getElementById('word-card') card.innerHTML = `

${word.text}

${word.definition}

` // XSS風險 card.style.color = 'red' // 直接操作style } ``` ## 🔒 安全性最佳實踐 ### 輸入驗證和清理 ```javascript // 輸入驗證工具類 class InputValidator { static patterns = { email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, username: /^[a-zA-Z0-9_]{3,20}$/, phone: /^\+?[\d\s\-\(\)]+$/ } static validate(input, type, options = {}) { if (typeof input !== 'string') { return { valid: false, error: 'Input must be a string' } } // 長度檢查 const { minLength = 0, maxLength = 1000 } = options if (input.length < minLength || input.length > maxLength) { return { valid: false, error: `Length must be between ${minLength} and ${maxLength}` } } // 模式檢查 if (this.patterns[type] && !this.patterns[type].test(input)) { return { valid: false, error: `Invalid ${type} format` } } return { valid: true } } static sanitizeString(input) { return input .replace(/[<>'"&]/g, (match) => { const escapeMap = { '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' } return escapeMap[match] }) .trim() } static normalizeWhitespace(input) { return input.replace(/\s+/g, ' ').trim() } } // 表單驗證範例 class FormValidator { constructor(form) { this.form = form this.errors = new Map() this.rules = new Map() } addRule(fieldName, validator) { if (!this.rules.has(fieldName)) { this.rules.set(fieldName, []) } this.rules.get(fieldName).push(validator) return this } validate() { this.errors.clear() for (const [fieldName, validators] of this.rules) { const field = this.form.querySelector(`[name="${fieldName}"]`) if (!field) continue const value = field.value for (const validator of validators) { const result = validator(value) if (!result.valid) { this.errors.set(fieldName, result.error) break // 只顯示第一個錯誤 } } } this.displayErrors() return this.errors.size === 0 } displayErrors() { // 清除舊錯誤 this.form.querySelectorAll('.form-error').forEach(el => { el.textContent = '' }) // 顯示新錯誤 for (const [fieldName, error] of this.errors) { const errorElement = this.form.querySelector(`#${fieldName}-error`) if (errorElement) { errorElement.textContent = error } } } } ``` ### XSS 和 CSRF 防護 ```javascript // CSRF Token管理 class CSRFManager { constructor() { this.token = null this.refreshPromise = null } async getToken() { if (!this.token || this.isTokenExpired()) { if (!this.refreshPromise) { this.refreshPromise = this.refreshToken() } await this.refreshPromise this.refreshPromise = null } return this.token } async refreshToken() { try { const response = await fetch('/api/csrf-token', { method: 'GET', credentials: 'same-origin' }) if (!response.ok) { throw new Error('Failed to get CSRF token') } const data = await response.json() this.token = data.token this.tokenExpiry = Date.now() + (data.expiresIn * 1000) } catch (error) { console.error('CSRF token refresh failed:', error) throw error } } isTokenExpired() { return !this.tokenExpiry || Date.now() >= this.tokenExpiry } } // Content Security Policy 協助函數 class CSPHelper { static createNonce() { const array = new Uint8Array(16) crypto.getRandomValues(array) return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('') } static addNonceToScript(scriptContent) { const nonce = this.createNonce() const meta = document.createElement('meta') meta.httpEquiv = 'Content-Security-Policy' meta.content = `script-src 'self' 'nonce-${nonce}'` document.head.appendChild(meta) const script = document.createElement('script') script.nonce = nonce script.textContent = scriptContent return script } } ``` ## 🧪 測試規範 ### 單元測試規範 ```javascript // tests/utils/inputValidator.test.js import { describe, it, expect, beforeEach } from 'vitest' import { InputValidator } from '../../src/utils/inputValidator.js' describe('InputValidator', () => { describe('email validation', () => { it('should accept valid email addresses', () => { const validEmails = [ 'test@example.com', 'user.name@domain.co.uk', 'user+tag@example.org' ] validEmails.forEach(email => { const result = InputValidator.validate(email, 'email') expect(result.valid).toBe(true) }) }) it('should reject invalid email addresses', () => { const invalidEmails = [ 'invalid-email', '@example.com', 'user@', 'user@.com' ] invalidEmails.forEach(email => { const result = InputValidator.validate(email, 'email') expect(result.valid).toBe(false) expect(result.error).toBeDefined() }) }) it('should enforce length limits', () => { const shortEmail = 'a@b.c' const longEmail = 'a'.repeat(100) + '@example.com' const shortResult = InputValidator.validate(shortEmail, 'email', { minLength: 10 }) expect(shortResult.valid).toBe(false) const longResult = InputValidator.validate(longEmail, 'email', { maxLength: 50 }) expect(longResult.valid).toBe(false) }) }) describe('sanitizeString', () => { it('should escape HTML special characters', () => { const input = '' const expected = '<script>alert("xss")</script>' const result = InputValidator.sanitizeString(input) expect(result).toBe(expected) }) it('should trim whitespace', () => { const input = ' hello world ' const expected = 'hello world' const result = InputValidator.sanitizeString(input) expect(result).toBe(expected) }) }) }) ``` ### 整合測試範例 ```javascript // tests/integration/vocabularyCard.test.js import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { VocabularyCard } from '../../src/components/VocabularyCard.js' describe('VocabularyCard Integration Tests', () => { let container let mockAPI beforeEach(() => { // 設置DOM環境 container = document.createElement('div') container.innerHTML = `
` document.body.appendChild(container) // 模擬API mockAPI = { getWordIntroduction: vi.fn() } // 替換全局API global.vocabularyAPI = mockAPI }) afterEach(() => { // 清理DOM document.body.removeChild(container) vi.restoreAllMocks() }) it('should load and display word data on initialization', async () => { const mockWord = { id: 'test-word', word: 'hello', pronunciation: '/həˈloʊ/', definition_zh: '你好', examples: [ { sentence: 'Hello, world!' } ] } mockAPI.getWordIntroduction.mockResolvedValue(mockWord) const cardElement = container.querySelector('#vocabulary-card') const vocabularyCard = new VocabularyCard(cardElement) // 等待異步載入 await new Promise(resolve => setTimeout(resolve, 100)) expect(mockAPI.getWordIntroduction).toHaveBeenCalledWith('test-word') expect(cardElement.querySelector('.word-text')).toHaveTextContent('hello') expect(cardElement.querySelector('.pronunciation')).toHaveTextContent('/həˈloʊ/') }) it('should handle API errors gracefully', async () => { mockAPI.getWordIntroduction.mockRejectedValue(new Error('Network error')) const cardElement = container.querySelector('#vocabulary-card') const vocabularyCard = new VocabularyCard(cardElement) await new Promise(resolve => setTimeout(resolve, 100)) expect(cardElement.querySelector('.error-message')).toBeInTheDocument() }) it('should flip card when clicked', async () => { const mockWord = { id: 'test', word: 'test', definition_zh: 'test' } mockAPI.getWordIntroduction.mockResolvedValue(mockWord) const cardElement = container.querySelector('#vocabulary-card') const vocabularyCard = new VocabularyCard(cardElement) await new Promise(resolve => setTimeout(resolve, 100)) // 模擬點擊 const flipButton = cardElement.querySelector('.flip-btn') flipButton.click() expect(cardElement).toHaveClass('flipped') }) }) ``` ## 📋 代碼審查檢查清單 ### HTML 審查要點 - [ ] 使用語義化HTML5元素 - [ ] 所有圖片都有alt屬性 - [ ] 表單元素都有對應的label - [ ] 適當使用ARIA屬性 - [ ] 頁面有正確的文檔結構(DOCTYPE、lang等) - [ ] 無障礙性考量(鍵盤導航、螢幕閱讀器) ### CSS 審查要點 - [ ] 使用BEM或一致的命名規範 - [ ] 無硬編碼的magic numbers - [ ] 響應式設計實現正確 - [ ] 避免過度具體的選擇器 - [ ] CSS變數使用合理 - [ ] 無未使用的CSS規則 ### JavaScript 審查要點 - [ ] 函數職責單一,名稱清楚 - [ ] 適當的錯誤處理 - [ ] 無記憶體洩漏風險 - [ ] 適當使用async/await - [ ] 輸入驗證和清理 - [ ] 事件監聽器正確清理 ### 效能審查要點 - [ ] 避免不必要的DOM查詢 - [ ] 適當使用事件委託 - [ ] 圖片和資源優化 - [ ] 批量DOM更新 - [ ] 避免阻塞主線程的操作 ### 安全性審查要點 - [ ] 用戶輸入已清理 - [ ] 防XSS措施就位 - [ ] CSRF保護實施 - [ ] 適當的Content Security Policy - [ ] 敏感資料不在客戶端暴露 --- **文檔狀態**: 🟢 完整原生前端開發規範 **最後更新**: 2025-09-10 **適用技術**: HTML5 + CSS3 + Modern JavaScript **維護團隊**: 前端開發團隊