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

698 lines
16 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.

# 程式碼規範與開發標準
## 概述
建立統一的程式碼撰寫規範和開發流程標準,確保團隊協作效率和代碼品質。
**技術堆疊**:
- **前端**: 原生Web技術 (HTML5 + CSS3/SCSS + Modern JavaScript ES2022+)
- **後端**: .NET Core API
- **建置工具**: Vite 5.x
**最後更新**: 2025-09-10
## 通用開發原則
### 代碼品質原則
- [ ] **可讀性優先**: 代碼應該容易閱讀和理解
- [ ] **一致性**: 遵循統一的命名和格式規範
- [ ] **簡潔性**: 避免過度複雜的解決方案
- [ ] **可測試性**: 代碼結構便於單元測試
- [ ] **可維護性**: 考慮未來修改和擴展的便利性
### SOLID原則遵循
- [ ] **單一職責**: 每個函數/類只負責一個明確的功能
- [ ] **開放封閉**: 對擴展開放,對修改封閉
- [ ] **里氏替換**: 子類應該能夠替換父類
- [ ] **介面隔離**: 不應該依賴不需要的介面
- [ ] **依賴倒置**: 依賴抽象而非具體實現
## C# (.NET Core) 規範
### 基本格式規則
#### EditorConfig 配置
```ini
# .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 分析器規則
```xml
<!-- 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 結構標準
#### 語義化標記規則
```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>
```
#### 無障礙標準
```html
<!-- ✅ 無障礙支援 -->
<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 規範
#### 組織結構
```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';
```
#### 變數命名和組織
```scss
// 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 方法論
```scss
// ✅ 使用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+ 規範
#### 模組系統
```javascript
// ✅ 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特性
```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操作
```javascript
// ✅ 現代事件處理
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 標準配置
```javascript
// 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 配置
```javascript
// .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 配置
```json
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "avoid"
}
```
### Stylelint 配置
```javascript
// 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
```
## 測試規範
### 單元測試標準
```javascript
// 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)
})
})
})
```
## 效能最佳化指南
### 資源載入優化
```html
<!-- 預載入關鍵資源 -->
<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 效能最佳化
```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 開發團隊