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