698 lines
16 KiB
Markdown
698 lines
16 KiB
Markdown
# 程式碼規範與開發標準
|
||
|
||
## 概述
|
||
建立統一的程式碼撰寫規範和開發流程標準,確保團隊協作效率和代碼品質。
|
||
|
||
**技術堆疊**:
|
||
- **前端**: 原生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 開發團隊 |