dramaling-app/docs/03_development/coding-standards.md

16 KiB
Raw Blame History

程式碼規範與開發標準

概述

建立統一的程式碼撰寫規範和開發流程標準,確保團隊協作效率和代碼品質。

技術堆疊:

  • 前端: 原生Web技術 (HTML5 + CSS3/SCSS + Modern JavaScript ES2022+)
  • 後端: .NET Core API
  • 建置工具: Vite 5.x

最後更新: 2025-09-10

通用開發原則

代碼品質原則

  • 可讀性優先: 代碼應該容易閱讀和理解
  • 一致性: 遵循統一的命名和格式規範
  • 簡潔性: 避免過度複雜的解決方案
  • 可測試性: 代碼結構便於單元測試
  • 可維護性: 考慮未來修改和擴展的便利性

SOLID原則遵循

  • 單一職責: 每個函數/類只負責一個明確的功能
  • 開放封閉: 對擴展開放,對修改封閉
  • 里氏替換: 子類應該能夠替換父類
  • 介面隔離: 不應該依賴不需要的介面
  • 依賴倒置: 依賴抽象而非具體實現

C# (.NET Core) 規範

基本格式規則

EditorConfig 配置

# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true

[*.{json,js,ts,tsx,css,scss,yml,yaml}]
indent_size = 2

.NET 分析器規則

<!-- Directory.Build.props -->
<Project>
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors />
    <WarningsNotAsErrors>CS1591</WarningsNotAsErrors>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>

HTML5 原生前端規範

HTML 結構標準

語義化標記規則

<!-- ✅ 正確:使用語義化標記 -->
<article class="vocabulary-lesson">
  <header class="lesson-header">
    <h2 class="lesson-title">詞彙學習</h2>
    <time datetime="2025-09-10" class="lesson-date">2025年9月10日</time>
  </header>
  
  <section class="word-introduction">
    <h3>單字介紹</h3>
    <div class="vocabulary-word" data-word="confidence">
      <span class="word-text">confidence</span>
      <span class="phonetic">/ˈkɒnfɪdəns/</span>
      <p class="definition">信心;自信心;把握</p>
    </div>
  </section>
  
  <aside class="word-notes" role="complementary">
    <h4>學習筆記</h4>
    <p>重點提醒...</p>
  </aside>
</article>

<!-- ❌ 錯誤濫用div -->
<div class="vocabulary-lesson">
  <div class="lesson-header">
    <div class="lesson-title">詞彙學習</div>
  </div>
</div>

無障礙標準

<!-- ✅ 無障礙支援 -->
<button class="play-audio-btn" 
        aria-label="播放confidence的發音"
        data-word="confidence">
  <span class="icon-sound" aria-hidden="true">🔊</span>
  發音
</button>

<div class="vocabulary-progress" 
     role="progressbar" 
     aria-valuenow="75" 
     aria-valuemin="0" 
     aria-valuemax="100"
     aria-label="學習進度">
  <div class="progress-fill" style="width: 75%"></div>
</div>

<!-- Skip link -->
<a class="skip-link" href="#main-content">跳至主要內容</a>

CSS/SCSS 規範

組織結構

// styles/main.scss - 主要樣式入口
@import 'base/reset';
@import 'base/variables';
@import 'base/typography';
@import 'components/vocabulary-card';
@import 'components/mode-selector';
@import 'pages/vocabulary';
@import 'utilities/helpers';

變數命名和組織

// base/variables.scss
// ✅ 語義化變數命名
$color-primary-teal: #00e5cc;
$color-secondary-purple: #8b5cf6;
$color-success-green: #22c55e;
$color-error-red: #ef4444;
$color-warning-yellow: #f59e0b;

$color-text-primary: #1f2937;
$color-text-secondary: #6b7280;
$color-text-tertiary: #9ca3af;

$spacing-xs: 0.25rem;  // 4px
$spacing-sm: 0.5rem;   // 8px
$spacing-md: 1rem;     // 16px
$spacing-lg: 1.5rem;   // 24px
$spacing-xl: 2rem;     // 32px

$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

// ❌ 避免的命名
$blue: #00e5cc;  // 太泛化
$s: 0.5rem;      // 太簡短

BEM 方法論

// ✅ 使用BEM命名慣例
.vocabulary-card {
  // Block
  padding: $spacing-lg;
  
  &__header {
    // Element
    border-bottom: 1px solid $color-divider;
  }
  
  &__word {
    // Element
    font-size: $font-size-xl;
    font-weight: 700;
  }
  
  &--featured {
    // Modifier
    border: 2px solid $color-primary-teal;
  }
  
  &--learning {
    // Modifier
    background: rgba($color-warning-yellow, 0.1);
  }
}

// 使用方式
// <div class="vocabulary-card vocabulary-card--featured">
//   <div class="vocabulary-card__header">
//     <h3 class="vocabulary-card__word">confidence</h3>
//   </div>
// </div>

Modern JavaScript ES2022+ 規範

模組系統

// ✅ ES6+ 模組語法
// modules/vocabulary/index.js
export class VocabularyModule {
  constructor() {
    this.state = new VocabularyState()
    this.api = new VocabularyAPI()
  }
  
  async init() {
    await this.loadInitialData()
    this.setupEventListeners()
  }
}

// utils/helpers.js
export const debounce = (func, wait) => {
  let timeout
  return (...args) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => func.apply(this, args), wait)
  }
}

export const formatPhonetic = (phonetic) => {
  return phonetic.startsWith('/') ? phonetic : `/${phonetic}/`
}

// main.js
import { VocabularyModule } from './modules/vocabulary/index.js'
import { debounce } from './utils/helpers.js'

現代JavaScript特性

// ✅ 使用現代語法特性
class VocabularyState {
  // 私有屬性 (ES2022)
  #words = new Map()
  #currentWord = null
  
  constructor() {
    this.progress = 0
  }
  
  // Getter/Setter
  get currentWord() {
    return this.#currentWord
  }
  
  set currentWord(word) {
    this.#currentWord = word
    this.notifySubscribers()
  }
  
  // 異步方法
  async loadVocabulary(lessonId) {
    try {
      const response = await fetch(`/api/vocabulary/${lessonId}`)
      const data = await response.json()
      
      // 使用解構賦值
      const { words, metadata } = data
      
      // 使用 Map 和現代陣列方法
      words.forEach(word => {
        this.#words.set(word.id, {
          ...word,
          learned: false,
          attempts: 0
        })
      })
      
      return { success: true, count: words.length }
    } catch (error) {
      console.error('載入詞彙失敗:', error)
      throw new VocabularyError('無法載入詞彙資料')
    }
  }
  
  // 使用可選鏈 (Optional Chaining)
  getWordProgress(wordId) {
    return this.#words.get(wordId)?.attempts ?? 0
  }
  
  // 使用 Nullish Coalescing
  getWordDefinition(wordId, fallback = '無定義') {
    return this.#words.get(wordId)?.definition ?? fallback
  }
}

// 自定義錯誤類
class VocabularyError extends Error {
  constructor(message, code = 'VOCABULARY_ERROR') {
    super(message)
    this.name = 'VocabularyError'
    this.code = code
  }
}

事件處理和DOM操作

// ✅ 現代事件處理
class VocabularyUI {
  constructor() {
    this.elements = {
      wordCard: document.querySelector('.vocabulary-card'),
      playButton: document.querySelector('.play-audio-btn'),
      nextButton: document.querySelector('.next-word-btn')
    }
    
    this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
  }
  
  setupEventListeners() {
    // 使用委派事件處理
    document.addEventListener('click', this.handleGlobalClick.bind(this))
    
    // 鍵盤快捷鍵
    document.addEventListener('keydown', (event) => {
      const shortcuts = {
        'Space': () => this.playAudio(),
        'Enter': () => this.nextWord(),
        'KeyR': () => this.repeatWord(),
        'Escape': () => this.exitLesson()
      }
      
      const handler = shortcuts[event.code]
      if (handler && !event.target.matches('input, textarea')) {
        event.preventDefault()
        handler()
      }
    })
  }
  
  handleGlobalClick(event) {
    // 使用現代選擇器
    if (event.target.matches('.play-audio-btn')) {
      const wordId = event.target.dataset.word
      this.playWordAudio(wordId)
    }
    
    if (event.target.closest('.vocabulary-card')) {
      const card = event.target.closest('.vocabulary-card')
      this.highlightCard(card)
    }
  }
  
  // 使用 Web Audio API
  async playWordAudio(word) {
    try {
      const audioUrl = `/api/audio/pronunciation/${word}`
      const response = await fetch(audioUrl)
      const audioBuffer = await response.arrayBuffer()
      const audioData = await this.audioContext.decodeAudioData(audioBuffer)
      
      const source = this.audioContext.createBufferSource()
      source.buffer = audioData
      source.connect(this.audioContext.destination)
      source.start()
      
    } catch (error) {
      console.error('音頻播放失敗:', error)
      this.showErrorMessage('無法播放發音')
    }
  }
}

### 命名規範

#### C# 命名慣例
```csharp
// ✅ 類別和方法使用PascalCase
public class UserService
{
    public async Task<UserProfile> GetUserProfileAsync(Guid userId)
    {
        // 方法實現
    }

    public decimal CalculateMonthlyInterestRate(decimal principal, decimal rate)
    {
        return principal * rate / 12;
    }
}

// ✅ 變數和參數使用camelCase
private readonly IUserRepository _userRepository;
private const int MaxRetryAttempts = 3;

public async Task<bool> ValidateUserAsync(string email, string password)
{
    var isValidEmail = IsValidEmailFormat(email);
    var hashedPassword = HashPassword(password);
    
    return isValidEmail && await _userRepository.ValidateCredentialsAsync(email, hashedPassword);
}

// ❌ 避免的命名
private string data; // 太泛化
private int u; // 太簡短
private async Task GetUserProfileDataAsync() {} // 冗餘的Data後綴

Vite 5.x 建置配置

vite.config.js 標準配置

// vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'

export default defineConfig({
  // 開發伺服器配置
  server: {
    port: 3000,
    host: true,
    open: true
  },
  
  // 建置配置
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: true,
    minify: 'terser',
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        vocabulary: resolve(__dirname, 'pages/vocabulary.html'),
        dialogue: resolve(__dirname, 'pages/dialogue.html')
      }
    }
  },
  
  // CSS 配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "./src/styles/base/variables.scss";`
      }
    }
  },
  
  // 解析配置
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@styles': resolve(__dirname, 'src/styles'),
      '@components': resolve(__dirname, 'src/components')
    }
  }
})

專案結構標準

web-frontend/
├── index.html                  # 主頁面
├── vite.config.js             # Vite 配置
├── package.json               # 依賴管理
├── public/
│   ├── favicon.svg            # 網站圖示
│   └── assets/                # 靜態資源
├── src/
│   ├── styles/                # 樣式檔案
│   │   ├── main.scss         # 主樣式入口
│   │   ├── base/             # 基礎樣式
│   │   ├── components/       # 組件樣式
│   │   └── pages/           # 頁面樣式
│   ├── scripts/              # JavaScript 模組
│   │   ├── main.js          # 應用入口
│   │   ├── modules/         # 功能模組
│   │   └── utils/           # 工具函數
│   └── components/           # 可重用組件HTML
└── pages/                    # 各頁面HTML檔案
    ├── vocabulary.html
    ├── dialogue.html
    └── profile.html

程式碼品質工具

ESLint 配置

// .eslintrc.js
export default {
  env: {
    browser: true,
    es2022: true
  },
  extends: [
    'eslint:recommended'
  ],
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module'
  },
  rules: {
    // 代碼風格
    'indent': ['error', 2],
    'quotes': ['error', 'single'],
    'semi': ['error', 'never'],
    
    // 最佳實踐
    'no-console': 'warn',
    'no-unused-vars': 'error',
    'prefer-const': 'error',
    'no-var': 'error'
  }
}

Prettier 配置

// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 80,
  "bracketSpacing": true,
  "arrowParens": "avoid"
}

Stylelint 配置

// stylelint.config.js
export default {
  extends: [
    'stylelint-config-standard-scss'
  ],
  rules: {
    'selector-class-pattern': '^[a-z][a-z0-9]*(-[a-z0-9]+)*$',
    'scss/at-import-partial-extension': null,
    'property-no-vendor-prefix': null
  }
}

版本控制規範

Git 提交訊息格式

<type>(<scope>): <subject>

<body>

<footer>

類型說明

  • feat: 新功能
  • fix: 修復bug
  • docs: 文檔更新
  • style: 代碼格式調整(不影響功能)
  • refactor: 代碼重構
  • test: 測試相關
  • chore: 建置工具或輔助工具變動

範例

feat(vocabulary): implement flashcard learning mode

- Add vocabulary card component with flip animation
- Integrate spaced repetition algorithm
- Add audio pronunciation support

Closes #123

分支命名規範

feature/vocabulary-learning-system
hotfix/audio-playback-error
bugfix/user-login-validation
docs/update-coding-standards

測試規範

單元測試標準

// tests/utils/helpers.test.js
import { describe, it, expect } from 'vitest'
import { formatPhonetic, debounce } from '../src/utils/helpers.js'

describe('helpers utilities', () => {
  describe('formatPhonetic', () => {
    it('should add slashes if missing', () => {
      expect(formatPhonetic('kɒnfɪdəns')).toBe('/kɒnfɪdəns/')
    })
    
    it('should keep existing slashes', () => {
      expect(formatPhonetic('/kɒnfɪdəns/')).toBe('/kɒnfɪdəns/')
    })
  })
  
  describe('debounce', () => {
    it('should delay function execution', async () => {
      let count = 0
      const debouncedFn = debounce(() => count++, 100)
      
      debouncedFn()
      debouncedFn()
      debouncedFn()
      
      expect(count).toBe(0)
      
      await new Promise(resolve => setTimeout(resolve, 150))
      expect(count).toBe(1)
    })
  })
})

效能最佳化指南

資源載入優化

<!-- 預載入關鍵資源 -->
<link rel="preload" href="/styles/main.css" as="style">
<link rel="preload" href="/scripts/main.js" as="script">
<link rel="preconnect" href="https://api.dramaling.com">

<!-- 懶載入圖片 -->
<img src="vocabulary-image.jpg" 
     loading="lazy" 
     alt="詞彙示意圖">

JavaScript 效能最佳化

// ✅ 使用 IntersectionObserver 進行懶載入
const observeVocabularyCards = () => {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        loadVocabularyData(entry.target.dataset.wordId)
        observer.unobserve(entry.target)
      }
    })
  })
  
  document.querySelectorAll('.vocabulary-card[data-word-id]')
    .forEach(card => observer.observe(card))
}

// ✅ 使用 requestAnimationFrame 進行流暢動畫
const animateProgressBar = (targetProgress) => {
  const progressBar = document.querySelector('.progress-fill')
  const startProgress = parseFloat(progressBar.style.width) || 0
  const startTime = performance.now()
  const duration = 500
  
  const animate = (currentTime) => {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)
    
    const currentProgress = startProgress + (targetProgress - startProgress) * progress
    progressBar.style.width = `${currentProgress}%`
    
    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }
  
  requestAnimationFrame(animate)
}

最後更新: 2025-09-10
版本: 2.0 - 整合原生Web技術規範
維護者: Drama Ling 開發團隊