Codebase(單一程式碼庫,多個部署環境) 原則:一個應用程式應有單一程式碼庫,透過不同的部署(deploy)對應不同環境(dev/test/prod)。 解決方案: 使用 GitLab repository 管理專案。 每個環境使用不同 branch 或 tag(如 develop, release, main)。 CI/CD pipeline 進行自動化部署,避免分散程式碼庫。 Dependencies(明確宣告與隔離依賴) 原則:應用程式必須明確管理相依性,避免依賴系統環境。 解決方案: 後端:使用 Maven pom.xml 宣告所有 dependencies,不依賴本地安裝的 jar。 前端:使用 package.json 鎖定依賴版本。 建議使用 Docker 建立一致的 build/runtime 環境。 Config(將設定與程式碼分離) 原則:設定(如 DB 密碼、API key)不應寫死在程式碼中。 解決方案: Spring Boot 使用 application.yml + 外部設定檔 或 環境變數。 GitLab CI/CD 提供 Environment Variables 管理不同環境的設定。 建議搭配 Vault / AWS Secrets Manager / Kubernetes Secrets。 Backing Services(後端服務當作附加資源) 原則:資料庫、快取、MQ、外部 API 都應視為「可替換的資源」。 解決方案: DB(MySQL/DB2/PostgreSQL)、Redis、RabbitMQ 等連線資訊放在設定檔,不耦合程式。 測試環境可用輕量替代(如 testcontainers 啟動 DB/Redis)。 Build, Release, Run(明確分離建置、發佈、執行) 原則:建置(build)、發佈(release)、執行(run)必須分開,避免環境污染。 解決方案: ...

2 min · 292 words · Eric Cheng

Code Review 指引 目錄 前言 1.1 目的 1.2 適用範圍 1.3 Code Review 的價值 Code Review 基本原則 2.1 核心原則 2.2 責任分工 Code Review 流程 3.1 提交 Pull Request (PR) 3.2 指定 Reviewers 3.3 進行程式碼檢查 詳細檢查項目 4.1 程式碼風格與規範 4.2 邏輯正確性檢查 4.3 效能考量檢查 4.4 安全性檢查 4.5 測試覆蓋率檢查 Code Review 工具與自動化 5.1 GitHub Pull Request Review 5.2 GitLab Merge Request Review 5.3 SonarQube 程式碼品質檢查 5.4 ESLint 與 Prettier(前端) 實務操作指南 6.1 Review 意見分類與標準 6.2 常見審查重點清單 6.3 溝通技巧與最佳實務 6.4 Review 會議與討論 常見問題與解決方案 7.1 常見 Review 問題 7.2 效率提升技巧 團隊協作與衝突處理 8.1 Review 意見衝突處理 8.2 跨團隊 Review 協作 8.3 新人培訓與指導 特殊情況處理 9.1 緊急修正流程 9.2 大型重構 Review 9.3 第三方程式碼整合 持續改進與測量 10.1 Review 品質指標 10.2 流程效率分析 10.3 團隊成長追蹤 參考資源與延伸閱讀 11.1 程式碼品質標準 11.2 安全性資源 11.3 工具文件 11.4 最佳實務書籍 附錄 12.1 Review 檢查清單範本 12.2 團隊 Code Review 文化建立 1. 前言 1.1 目的 本指引旨在建立標準化的程式碼審查流程,確保所有程式碼在合併至主分支前都經過充分的檢查與評審,以提升程式碼品質、降低潛在錯誤與技術負債,並促進團隊知識分享與技能提升。 ...

21 min · 4448 words · Eric Cheng

以下是完整的 《JdbcTemplate 安全 SQL 實作指引文件》Markdown 版本,可直接放入你的 Git Repository(例如 /docs/security/jdbctemplate-sql-guideline.md),作為團隊安全規範或 Code Review checklist 使用。 # 🧭 JdbcTemplate 安全 SQL 實作指引文件 **版本:v1.0** **適用範圍:** Spring Boot 專案中使用 `JdbcTemplate` 或 `NamedParameterJdbcTemplate` 的資料存取層 **目的:** 防止 SQL Injection 攻擊與弱掃誤判 --- ## 1️⃣ 目的與原則 SQL Injection(SQL 注入)是 OWASP Top 10 的主要風險之一。 若在 API 中直接拼接 SQL 字串(尤其包含前端輸入),將導致弱掃報告出現 Injection 問題,甚至被惡意利用。 **安全實作原則:** 1. 所有 SQL 查詢必須採用 **參數化查詢(Parameterized Query)**。 2. 不可直接拼接使用者輸入字串到 SQL。 3. 動態欄位或排序需求必須使用 **白名單機制(Whitelist)**。 4. 禁止讓 client 直接傳入完整 SQL。 5. 所有 DB 帳號採用最小權限原則(Least Privilege)。 --- ## 2️⃣ 正確安全作法範例 ### ✅ 查詢範例 ```java String sql = "SELECT id, name, email FROM users WHERE email = ?"; return jdbcTemplate.query(sql, new Object[]{email}, new UserRowMapper()); ✅ 插入範例 String sql = "INSERT INTO users (name, email) VALUES (?, ?)"; jdbcTemplate.update(sql, name, email); ✅ 更新範例 String sql = "UPDATE users SET status = ? WHERE id = ?"; jdbcTemplate.update(sql, status, id); ✅ 刪除範例 String sql = "DELETE FROM users WHERE id = ?"; jdbcTemplate.update(sql, id); ✅ 動態查詢(條件可變) 重點:條件以程式判斷拼接,但參數仍使用 ? 綁定。 ...

3 min · 498 words · Eric Cheng

重構指引(Refactoring Guide) 目錄 前言與目標 什麼是重構? 重構的核心目標 重構原則 基本原則 SOLID 原則在重構中的應用 重構時機 何時應該進行重構? 重構的紅綠燈系統 常見重構手法 提煉函數(Extract Function) 提煉類別(Extract Class) 簡化條件表達式(Simplify Conditional Expressions) 提煉常數(Extract Constants) 移除死程式碼(Remove Dead Code) 重構流程 標準重構流程 重構檢核清單 Java 重構最佳實務 IDE 重構工具使用 Maven 設定重構支援 重構中的測試策略 安全性考量 重構過程中的安全原則 重構中的資安檢核清單 效能考量 重構對效能的影響 效能測試與監控 重構工具與技術 靜態分析工具 自動化重構工具 持續整合中的重構 常見重構陷阱與解決方案 常見錯誤 最佳實務建議 重構案例研究 遺留系統重構 微服務重構 重構檢核清單 重構前檢核 重構中檢核 重構後檢核 團隊協作與重構 Code Review 中的重構 重構溝通策略 重構效果追蹤 短期追蹤 中期追蹤 長期追蹤 結論 前言與目標 什麼是重構? 重構(Refactoring)是指在不改變程式碼外部行為的前提下,對程式碼內部結構進行改善的過程。這是一種持續性的改進活動,旨在提升程式碼品質、可讀性和可維護性。 重構的核心目標 1. 提高可讀性 目標:讓程式碼更容易理解,降低未來維護的難度 效益: 新團隊成員能快速上手 減少程式碼理解時間 降低錯誤修改的風險 評估指標: 程式碼複雜度(Cyclomatic Complexity) 方法長度 類別職責單一性 2. 改善結構與設計 目標:優化架構,使程式更具彈性、可擴充性 效益: 更容易添加新功能 更好的模組化設計 符合 SOLID 設計原則 評估指標: 耦合度(Coupling) 內聚性(Cohesion) 設計模式使用適當性 3. 減少重複(DRY 原則) 目標:把重複的邏輯抽出來,讓程式碼更簡潔 效益: 減少程式碼維護成本 降低一致性問題 提高程式碼重用性 評估指標: 重複程式碼比例 共用元件使用率 4. 提升可測試性 目標:更清晰的結構有助於單元測試與整合測試 效益: 更容易編寫單元測試 提高測試覆蓋率 更好的依賴注入設計 評估指標: 測試覆蓋率 測試案例數量 模擬物件使用便利性 5. 降低技術債(Technical Debt) 目標:清理過時或混亂的程式碼,避免未來出現更多問題 效益: 提升開發效率 減少維護成本 降低系統風險 評估指標: SonarQube 品質評分 程式碼異味數量 安全漏洞數量 6. 促進團隊協作 目標:統一風格與結構,讓不同開發者更容易接手 效益: 提升團隊開發效率 降低知識傳承成本 統一開發標準 評估指標: 程式碼風格一致性 Code Review 效率 團隊生產力 重構原則 基本原則 1. 保持外部行為不變 重構過程中,程式的功能和對外介面不應改變 所有現有的測試案例應該繼續通過 使用者感受不到任何功能上的差異 2. 小步快跑 每次重構應該是小幅度的改動 頻繁進行測試驗證 避免大範圍的同時修改 3. 測試先行 重構前確保有足夠的測試覆蓋 重構過程中持續執行測試 新增測試案例以驗證重構結果 4. 循序漸進 按照優先順序進行重構 先解決最嚴重的程式碼異味 避免過度重構 SOLID 原則在重構中的應用 1. 單一職責原則(Single Responsibility Principle) // 重構前:一個類別負責多個職責 public class UserManager { public void saveUser(User user) { // 驗證使用者資料 if (user.getEmail() == null || !user.getEmail().contains("@")) { throw new IllegalArgumentException("Invalid email"); } // 儲存到資料庫 DatabaseConnection conn = new DatabaseConnection(); conn.save(user); // 發送通知郵件 EmailService emailService = new EmailService(); emailService.sendWelcomeEmail(user.getEmail()); } } // 重構後:職責分離 public class UserValidator { public void validate(User user) { if (user.getEmail() == null || !user.getEmail().contains("@")) { throw new IllegalArgumentException("Invalid email"); } } } public class UserRepository { public void save(User user) { DatabaseConnection conn = new DatabaseConnection(); conn.save(user); } } public class UserNotificationService { public void sendWelcomeNotification(String email) { EmailService emailService = new EmailService(); emailService.sendWelcomeEmail(email); } } public class UserService { private final UserValidator validator; private final UserRepository repository; private final UserNotificationService notificationService; public UserService(UserValidator validator, UserRepository repository, UserNotificationService notificationService) { this.validator = validator; this.repository = repository; this.notificationService = notificationService; } public void createUser(User user) { validator.validate(user); repository.save(user); notificationService.sendWelcomeNotification(user.getEmail()); } } 2. 開放封閉原則(Open/Closed Principle) // 重構前:修改現有程式碼來新增功能 public class DiscountCalculator { public double calculateDiscount(String customerType, double amount) { if ("REGULAR".equals(customerType)) { return amount * 0.05; } else if ("VIP".equals(customerType)) { return amount * 0.10; } else if ("PREMIUM".equals(customerType)) { return amount * 0.15; } return 0; } } // 重構後:使用策略模式,對擴展開放,對修改封閉 public interface DiscountStrategy { double calculateDiscount(double amount); } public class RegularCustomerDiscount implements DiscountStrategy { @Override public double calculateDiscount(double amount) { return amount * 0.05; } } public class VipCustomerDiscount implements DiscountStrategy { @Override public double calculateDiscount(double amount) { return amount * 0.10; } } public class PremiumCustomerDiscount implements DiscountStrategy { @Override public double calculateDiscount(double amount) { return amount * 0.15; } } public class DiscountCalculator { private final Map<String, DiscountStrategy> strategies; public DiscountCalculator() { strategies = Map.of( "REGULAR", new RegularCustomerDiscount(), "VIP", new VipCustomerDiscount(), "PREMIUM", new PremiumCustomerDiscount() ); } public double calculateDiscount(String customerType, double amount) { DiscountStrategy strategy = strategies.get(customerType); return strategy != null ? strategy.calculateDiscount(amount) : 0; } } 重構時機 何時應該進行重構? 1. 程式碼異味(Code Smells)出現時 長方法(Long Method):方法超過 20-30 行 大類別(Large Class):類別職責過多,超過 200-300 行 重複程式碼(Duplicated Code):相同或相似的程式碼片段重複出現 長參數列表(Long Parameter List):方法參數超過 3-4 個 2. 新增功能前 為新功能建立適當的架構基礎 清理相關的程式碼區域 確保新功能不會增加技術債 3. 修復 Bug 時 分析 Bug 產生的根本原因 改善可能導致類似問題的程式結構 增加相關的測試覆蓋 4. Code Review 過程中 發現程式碼可讀性問題 識別潛在的設計問題 統一團隊的程式碼風格 重構的紅綠燈系統 🟢 綠燈:適合重構 有充足的測試覆蓋(>80%) 沒有緊急的產品發布壓力 團隊對重構區域有充分了解 有足夠的時間進行測試驗證 🟡 黃燈:謹慎重構 測試覆蓋率中等(60-80%) 有適度的時間壓力 重構範圍較大 需要多人協作 🔴 紅燈:暫停重構 測試覆蓋率不足(<60%) 有緊急的產品發布 程式碼變動頻繁 缺乏領域知識 常見重構手法 1. 提煉函數(Extract Function) 目的 將重複的程式碼片段提煉成獨立的函數,提高重用性和可讀性。 ...

26 min · 5417 words · Eric Cheng

UI/UX 開發指引 目錄 文件概述 設計原則 1.1 銀行系統的安全性與一致性要求 1.2 清晰的資訊層次與導航設計 1.3 金融資料顯示與重點突顯規範 1.4 無障礙設計(WCAG 2.1 AA 級) 1.5 多語系支援 UI 元件規範 2.1 樣式系統 (Design System) 2.2 響應式設計規範 2.3 常用元件設計 UX 流程規範 3.1 表單驗證與錯誤回饋 3.2 使用者引導 (Onboarding) 3.3 搜尋、篩選與排序互動設計 3.4 資料載入與狀態設計 設計資產與交付 4.1 設計 Token 系統 4.2 元件庫架構 Vue 3 + Tailwind CSS 最佳實踐 5.1 元件命名與結構規範 5.2 Tailwind CSS 自定義配置 檢測與驗證 6.1 視覺回歸測試 6.2 無障礙檢測 6.3 使用性測試清單 性能優化指引 設計協作流程 維護與版本控制 附錄 文件概述 本指引適用於大型金融共用平台的前端 UI/UX 開發,涵蓋設計原則、UI 元件規範、UX 流程、設計資產交付等完整內容。 專案技術堆疊 前端架構: 微前端 + SPA (Single-Page App— 第一部分完成,接下來我將繼續撰寫第二部分:無障礙設計和多語系支援。 1.4 無障礙設計(WCAG 2.1 AA 級) 色彩對比度要求 正常文字: 對比度至少 4.5:1 大文字 (18pt 以上): 對比度至少 3:1 互動元件: 對比度至少 3:1 /* Tailwind CSS 無障礙色彩配置 */ .text-primary { @apply text-gray-900; /* 對比度 21:1 */ } .text-secondary { @apply text-gray-700; /* 對比度 9.2:1 */ } .bg-interactive { @apply bg-blue-600 hover:bg-blue-700 focus:bg-blue-700; } .bg-interactive:focus { @apply ring-2 ring-blue-500 ring-offset-2; } 鍵盤操作支援 Tab 順序: 邏輯性的焦點移動順序 快捷鍵: 主要功能提供鍵盤快捷鍵 焦點指示: 清晰的焦點視覺回饋 <template> <!-- 無障礙表單範例 --> <form @submit.prevent="handleSubmit" class="space-y-6"> <div> <label for="account-number" class="block text-sm font-medium text-gray-700"> 帳戶號碼 <span class="text-red-500" aria-label="必填欄位">*</span> </label> <input id="account-number" v-model="accountNumber" type="text" required aria-describedby="account-help account-error" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" :aria-invalid="hasError" /> <p id="account-help" class="mt-2 text-sm text-gray-500"> 請輸入 12 位數字帳戶號碼 </p> <p id="account-error" v-if="errorMessage" class="mt-2 text-sm text-red-600" role="alert"> {{ errorMessage }} </p> </div> </form> </template> 螢幕閱讀器支援 語義化 HTML: 使用適當的 HTML 語義標籤 ARIA 標籤: 補充無障礙資訊 替代文字: 圖片提供有意義的 alt 文字 <template> <!-- 無障礙數據表格 --> <table role="table" aria-label="帳戶交易記錄"> <caption class="sr-only"> 最近 10 筆交易記錄,包含日期、摘要、金額和餘額 </caption> <thead> <tr> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 交易日期 </th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 摘要 </th> <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> 金額 </th> </tr> </thead> <tbody> <tr v-for="transaction in transactions" :key="transaction.id"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {{ formatDate(transaction.date) }} </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {{ transaction.description }} </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-right"> <span :class="transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'"> {{ formatCurrency(transaction.amount) }} </span> </td> </tr> </tbody> </table> </template> 1.5 多語系支援 語言切換機制 // i18n 配置 import { createI18n } from 'vue-i18n'; interface Messages { 'zh-TW': Record<string, any>; 'en-US': Record<string, any>; 'ja-JP': Record<string, any>; } const messages: Messages = { 'zh-TW': { common: { confirm: '確認', cancel: '取消', save: '儲存', delete: '刪除', loading: '載入中...' }, account: { balance: '帳戶餘額', transfer: '轉帳', history: '交易記錄' } }, 'en-US': { common: { confirm: 'Confirm', cancel: 'Cancel', save: 'Save', delete: 'Delete', loading: 'Loading...' }, account: { balance: 'Account Balance', transfer: 'Transfer', history: 'Transaction History' } } }; export const i18n = createI18n({ locale: 'zh-TW', fallbackLocale: 'en-US', messages }); 文字方向支援 (RTL/LTR) <template> <div :dir="currentLocale.dir" class="app-container"> <!-- 語言切換器 --> <div class="language-selector"> <select v-model="currentLanguage" @change="changeLanguage" class="block w-32 rounded-md border-gray-300" :aria-label="$t('common.selectLanguage')" > <option value="zh-TW">繁體中文</option> <option value="en-US">English</option> <option value="ar-SA">العربية</option> </select> </div> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; const { locale, t } = useI18n(); const localeConfig = { 'zh-TW': { dir: 'ltr', name: '繁體中文' }, 'en-US': { dir: 'ltr', name: 'English' }, 'ar-SA': { dir: 'rtl', name: 'العربية' } }; const currentLocale = computed(() => localeConfig[locale.value]); </script> <style> /* RTL 支援樣式 */ [dir="rtl"] .text-left { text-align: right; } [dir="rtl"] .text-right { text-align: left; } [dir="rtl"] .ml-4 { margin-left: 0; margin-right: 1rem; } </style> 數字與日期本地化 // 本地化格式化工具 export class LocaleFormatter { private locale: string; constructor(locale: string) { this.locale = locale; } // 金額格式化 formatCurrency(amount: number, currency: string = 'TWD'): string { const currencyMap = { 'zh-TW': 'TWD', 'en-US': 'USD', 'ja-JP': 'JPY', 'ko-KR': 'KRW' }; return new Intl.NumberFormat(this.locale, { style: 'currency', currency: currencyMap[this.locale] || currency, minimumFractionDigits: currency === 'JPY' ? 0 : 2 }).format(amount); } // 日期格式化 formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string { const defaultOptions: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' }; return new Intl.DateTimeFormat(this.locale, options || defaultOptions).format(date); } // 時間格式化 formatTime(date: Date): string { return new Intl.DateTimeFormat(this.locale, { hour: '2-digit', minute: '2-digit', hour12: this.locale === 'en-US' }).format(date); } // 百分比格式化 formatPercentage(value: number): string { return new Intl.NumberFormat(this.locale, { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); } } 進階無障礙設計實作 <template> <!-- 高對比模式切換 --> <div class="accessibility-controls fixed top-4 right-4 z-50"> <button @click="toggleHighContrast" :aria-pressed="isHighContrast" class="p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" :class="isHighContrast ? 'bg-yellow-400 text-black' : 'bg-white text-gray-700 shadow-md'" aria-label="切換高對比模式" > <EyeIcon class="h-5 w-5" /> </button> <!-- 字體大小調整 --> <div class="mt-2 flex flex-col space-y-1"> <button @click="increaseFontSize" class="p-1 text-xs bg-white rounded shadow-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" aria-label="增大字體" > A+ </button> <button @click="decreaseFontSize" class="p-1 text-xs bg-white rounded shadow-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" aria-label="縮小字體" > A- </button> <button @click="resetFontSize" class="p-1 text-xs bg-white rounded shadow-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" aria-label="重設字體大小" > A </button> </div> </div> <!-- 跳至主要內容連結 --> <a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-blue-600 text-white px-4 py-2 rounded-md z-50" > 跳至主要內容 </a> <!-- 焦點陷阱對話框範例 --> <div v-if="showModal" class="fixed inset-0 z-10 overflow-y-auto" role="dialog" aria-modal="true" :aria-labelledby="modalTitleId" @keydown.esc="closeModal" > <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <!-- 背景遮罩 --> <div class="fixed inset-0 transition-opacity" aria-hidden="true" @click="closeModal" > <div class="absolute inset-0 bg-gray-500 opacity-75"></div> </div> <!-- 對話框內容 --> <div ref="modalContent" class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" @keydown="handleModalKeydown" > <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <h3 :id="modalTitleId" class="text-lg leading-6 font-medium text-gray-900 mb-4"> 確認操作 </h3> <p class="text-sm text-gray-500 mb-4"> 您確定要執行此操作嗎?此動作無法復原。 </p> <!-- 焦點陷阱內的可互動元素 --> <div class="flex justify-end space-x-3"> <button ref="cancelButton" @click="closeModal" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" > 取消 </button> <button ref="confirmButton" @click="handleConfirm" class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500" > 確認 </button> </div> </div> </div> </div> </div> </template> <script setup lang="ts"> import { ref, nextTick, onMounted, onUnmounted } from 'vue'; import { EyeIcon } from '@heroicons/vue/24/outline'; // 無障礙狀態管理 const isHighContrast = ref(false); const fontSize = ref(16); const showModal = ref(false); const modalTitleId = ref(`modal-title-${Math.random().toString(36).substr(2, 9)}`); // 高對比模式 const toggleHighContrast = () => { isHighContrast.value = !isHighContrast.value; document.documentElement.classList.toggle('high-contrast', isHighContrast.value); // 儲存使用者偏好 localStorage.setItem('high-contrast', isHighContrast.value.toString()); // 通知螢幕閱讀器 announceToScreenReader( isHighContrast.value ? '已開啟高對比模式' : '已關閉高對比模式' ); }; // 字體大小調整 const increaseFontSize = () => { if (fontSize.value < 24) { fontSize.value += 2; updateFontSize(); } }; const decreaseFontSize = () => { if (fontSize.value > 12) { fontSize.value -= 2; updateFontSize(); } }; const resetFontSize = () => { fontSize.value = 16; updateFontSize(); }; const updateFontSize = () => { document.documentElement.style.fontSize = `${fontSize.value}px`; localStorage.setItem('font-size', fontSize.value.toString()); announceToScreenReader(`字體大小已調整為 ${fontSize.value} 像素`); }; // 螢幕閱讀器公告 const announceToScreenReader = (message: string) => { const announcement = document.createElement('div'); announcement.setAttribute('aria-live', 'polite'); announcement.setAttribute('aria-atomic', 'true'); announcement.className = 'sr-only'; announcement.textContent = message; document.body.appendChild(announcement); setTimeout(() => { document.body.removeChild(announcement); }, 1000); }; // 焦點陷阱管理 const modalContent = ref<HTMLElement>(); const cancelButton = ref<HTMLButtonElement>(); const confirmButton = ref<HTMLButtonElement>(); const focusableElements: HTMLElement[] = []; let previousFocusedElement: HTMLElement | null = null; const openModal = async () => { previousFocusedElement = document.activeElement as HTMLElement; showModal.value = true; await nextTick(); // 收集可聚焦元素 const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; focusableElements.splice(0); focusableElements.push( ...Array.from(modalContent.value?.querySelectorAll(selector) || []) ); // 聚焦第一個元素 if (focusableElements.length > 0) { focusableElements[0].focus(); } }; const closeModal = () => { showModal.value = false; // 恢復之前的焦點 if (previousFocusedElement) { previousFocusedElement.focus(); } }; const handleModalKeydown = (event: KeyboardEvent) => { if (event.key === 'Tab') { const currentIndex = focusableElements.indexOf(event.target as HTMLElement); if (event.shiftKey) { // Shift + Tab (向後) if (currentIndex <= 0) { event.preventDefault(); focusableElements[focusableElements.length - 1].focus(); } } else { // Tab (向前) if (currentIndex >= focusableElements.length - 1) { event.preventDefault(); focusableElements[0].focus(); } } } }; const handleConfirm = () => { // 處理確認邏輯 announceToScreenReader('操作已確認'); closeModal(); }; // 鍵盤快捷鍵 const handleGlobalKeydown = (event: KeyboardEvent) => { // Alt + H: 開啟高對比模式 if (event.altKey && event.key === 'h') { event.preventDefault(); toggleHighContrast(); } // Alt + +: 增大字體 if (event.altKey && event.key === '+') { event.preventDefault(); increaseFontSize(); } // Alt + -: 縮小字體 if (event.altKey && event.key === '-') { event.preventDefault(); decreaseFontSize(); } }; // 初始化無障礙設定 onMounted(() => { // 載入儲存的偏好設定 const savedHighContrast = localStorage.getItem('high-contrast'); if (savedHighContrast === 'true') { isHighContrast.value = true; document.documentElement.classList.add('high-contrast'); } const savedFontSize = localStorage.getItem('font-size'); if (savedFontSize) { fontSize.value = parseInt(savedFontSize); document.documentElement.style.fontSize = `${fontSize.value}px`; } // 監聽全域鍵盤事件 document.addEventListener('keydown', handleGlobalKeydown); // 檢測用戶偏好的配色方案 if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.classList.add('dark-mode'); } // 檢測用戶偏好的動畫設定 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.documentElement.classList.add('reduce-motion'); } }); onUnmounted(() => { document.removeEventListener('keydown', handleGlobalKeydown); }); </script> <style> /* 高對比模式樣式 */ .high-contrast { filter: contrast(150%) saturate(200%); } .high-contrast button { border: 2px solid currentColor !important; } .high-contrast a { text-decoration: underline !important; } /* 減少動畫模式 */ .reduce-motion *, .reduce-motion *::before, .reduce-motion *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } /* 深色模式支援 */ .dark-mode { color-scheme: dark; } /* 螢幕閱讀器專用隱藏 */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .sr-only.focus:focus { position: static; width: auto; height: auto; padding: inherit; margin: inherit; overflow: visible; clip: auto; white-space: normal; } </style> 多語系內容管理策略 // 多語系內容管理 interface TranslationKey { key: string; context?: string; pluralization?: boolean; interpolation?: string[]; } interface TranslationContent { [locale: string]: { [namespace: string]: { [key: string]: string | object; }; }; } // 翻譯內容結構化管理 export const translationStructure: TranslationContent = { 'zh-TW': { common: { // 通用操作 actions: { save: '儲存', cancel: '取消', confirm: '確認', delete: '刪除', edit: '編輯', add: '新增', search: '搜尋', filter: '篩選', sort: '排序', refresh: '重新整理', loading: '載入中...', noData: '無資料', error: '發生錯誤' }, // 表單相關 form: { required: '此欄位為必填', invalid: '格式不正確', tooShort: '長度不足', tooLong: '長度過長', emailInvalid: '請輸入有效的電子郵件地址', phoneInvalid: '請輸入有效的手機號碼', passwordMismatch: '密碼不一致' }, // 時間相關 time: { just_now: '剛剛', minutes_ago: '{count} 分鐘前', hours_ago: '{count} 小時前', days_ago: '{count} 天前', weeks_ago: '{count} 週前', months_ago: '{count} 個月前', years_ago: '{count} 年前' } }, // 金融業務相關 banking: { account: { balance: '帳戶餘額', available: '可用餘額', accountNumber: '帳戶號碼', accountType: '帳戶類型', status: '狀態', openDate: '開戶日期', lastTransaction: '最近交易' }, transaction: { transfer: '轉帳', deposit: '存款', withdrawal: '提款', payment: '付款', refund: '退款', fee: '手續費', amount: '金額', recipient: '收款人', description: '摘要', reference: '參考號碼', date: '交易日期', status: '交易狀態' }, status: { active: '啟用', inactive: '停用', pending: '處理中', completed: '已完成', failed: '失敗', cancelled: '已取消', expired: '已過期' } } }, 'en-US': { common: { actions: { save: 'Save', cancel: 'Cancel', confirm: 'Confirm', delete: 'Delete', edit: 'Edit', add: 'Add', search: 'Search', filter: 'Filter', sort: 'Sort', refresh: 'Refresh', loading: 'Loading...', noData: 'No Data', error: 'Error Occurred' }, form: { required: 'This field is required', invalid: 'Invalid format', tooShort: 'Too short', tooLong: 'Too long', emailInvalid: 'Please enter a valid email address', phoneInvalid: 'Please enter a valid phone number', passwordMismatch: 'Passwords do not match' }, time: { just_now: 'Just now', minutes_ago: '{count} minutes ago', hours_ago: '{count} hours ago', days_ago: '{count} days ago', weeks_ago: '{count} weeks ago', months_ago: '{count} months ago', years_ago: '{count} years ago' } }, banking: { account: { balance: 'Account Balance', available: 'Available Balance', accountNumber: 'Account Number', accountType: 'Account Type', status: 'Status', openDate: 'Open Date', lastTransaction: 'Last Transaction' }, transaction: { transfer: 'Transfer', deposit: 'Deposit', withdrawal: 'Withdrawal', payment: 'Payment', refund: 'Refund', fee: 'Fee', amount: 'Amount', recipient: 'Recipient', description: 'Description', reference: 'Reference Number', date: 'Transaction Date', status: 'Transaction Status' }, status: { active: 'Active', inactive: 'Inactive', pending: 'Pending', completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled', expired: 'Expired' } } } }; // 進階翻譯函數 export class I18nManager { private currentLocale: string = 'zh-TW'; private fallbackLocale: string = 'en-US'; // 設定當前語言 setLocale(locale: string) { this.currentLocale = locale; document.documentElement.setAttribute('lang', locale); // 更新頁面方向 const direction = this.getTextDirection(locale); document.documentElement.setAttribute('dir', direction); // 觸發語言變更事件 window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale, direction } })); } // 取得文字方向 private getTextDirection(locale: string): 'ltr' | 'rtl' { const rtlLocales = ['ar', 'he', 'fa', 'ur']; return rtlLocales.some(rtl => locale.startsWith(rtl)) ? 'rtl' : 'ltr'; } // 翻譯函數 translate(key: string, options?: { interpolation?: Record<string, any>; count?: number; defaultValue?: string; }): string { const keys = key.split('.'); let value = this.getNestedValue(translationStructure[this.currentLocale], keys); // 如果找不到翻譯,嘗試備用語言 if (!value) { value = this.getNestedValue(translationStructure[this.fallbackLocale], keys); } // 如果還是找不到,返回預設值或 key if (!value) { return options?.defaultValue || key; } // 處理插值 if (options?.interpolation) { Object.entries(options.interpolation).forEach(([placeholder, replacement]) => { value = value.replace(new RegExp(`{${placeholder}}`, 'g'), replacement); }); } // 處理複數形式 if (options?.count !== undefined) { value = this.handlePluralization(value, options.count); } return value; } // 取得巢狀物件值 private getNestedValue(obj: any, keys: string[]): string | null { return keys.reduce((current, key) => current?.[key], obj) || null; } // 處理複數形式 private handlePluralization(value: string, count: number): string { // 簡化的複數處理,實際應用中可能需要更複雜的邏輯 if (this.currentLocale === 'en-US') { if (count === 1) { return value.replace(/\{count\}/, count.toString()); } else { // 處理英文複數規則 return value.replace(/\{count\}/, count.toString()); } } return value.replace(/\{count\}/, count.toString()); } // 格式化相對時間 formatRelativeTime(date: Date): string { const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (diffInSeconds < 60) { return this.translate('common.time.just_now'); } else if (diffInSeconds < 3600) { const minutes = Math.floor(diffInSeconds / 60); return this.translate('common.time.minutes_ago', { interpolation: { count: minutes.toString() } }); } else if (diffInSeconds < 86400) { const hours = Math.floor(diffInSeconds / 3600); return this.translate('common.time.hours_ago', { interpolation: { count: hours.toString() } }); } else if (diffInSeconds < 604800) { const days = Math.floor(diffInSeconds / 86400); return this.translate('common.time.days_ago', { interpolation: { count: days.toString() } }); } else if (diffInSeconds < 2419200) { const weeks = Math.floor(diffInSeconds / 604800); return this.translate('common.time.weeks_ago', { interpolation: { count: weeks.toString() } }); } else if (diffInSeconds < 29030400) { const months = Math.floor(diffInSeconds / 2419200); return this.translate('common.time.months_ago', { interpolation: { count: months.toString() } }); } else { const years = Math.floor(diffInSeconds / 29030400); return this.translate('common.time.years_ago', { interpolation: { count: years.toString() } }); } } } // 全域 i18n 實例 export const i18nManager = new I18nManager(); // Vue 組合函數 export function useI18n() { const t = (key: string, options?: any) => i18nManager.translate(key, options); const setLocale = (locale: string) => i18nManager.setLocale(locale); const formatRelativeTime = (date: Date) => i18nManager.formatRelativeTime(date); return { t, setLocale, formatRelativeTime }; } 無障礙設計和多語系支援擴充完成,接下來我將繼續撰寫第三部分:UI 元件規範。 ...

34 min · 7053 words · Eric Cheng

前端開發指引 文件資訊 版本: 1.0.0 建立日期: 2025-08-11 適用專案: 大型金融級 Web 專案 技術棧: Vue 3.x + TypeScript + Tailwind CSS 目錄 專案目錄與檔案結構規範 命名規範 程式撰寫風格與 Lint 設定 元件開發規範 樣式與 Tailwind CSS 規範 API 串接與資料存取規範 狀態管理規範 多語系處理規範 測試規範 安全性考量 效能優化規範 無障礙設計規範 版本控制與分支策略 專案建置與部署流程 程式碼審查規範 常見錯誤處理與 Debug 流程 開發工具與環境設定 團隊協作與溝通規範 1. 專案目錄與檔案結構規範 1.1 標準專案結構 frontend-project/ ├── public/ # 靜態資源 │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src/ # 原始碼 │ ├── api/ # API 相關 │ │ ├── modules/ # 依功能模組分類 │ │ │ ├── auth.ts │ │ │ └── user.ts │ │ ├── interceptors/ # 攔截器 │ │ └── types/ # API 型別定義 │ ├── assets/ # 靜態資源 │ │ ├── images/ │ │ ├── icons/ │ │ └── fonts/ │ ├── components/ # 共用元件 │ │ ├── base/ # 基礎元件 │ │ │ ├── BaseButton.vue │ │ │ ├── BaseInput.vue │ │ │ └── BaseModal.vue │ │ ├── business/ # 業務元件 │ │ └── layout/ # 版面元件 │ │ ├── Header.vue │ │ ├── Sidebar.vue │ │ └── Footer.vue │ ├── composables/ # Vue 3 Composition API │ │ ├── useAuth.ts │ │ ├── useApi.ts │ │ └── useLocalStorage.ts │ ├── constants/ # 常數定義 │ │ ├── api.ts │ │ ├── routes.ts │ │ └── config.ts │ ├── directives/ # 自定義指令 │ ├── i18n/ # 多語系 │ │ ├── locales/ │ │ │ ├── zh-TW.json │ │ │ ├── en-US.json │ │ │ └── ja-JP.json │ │ └── index.ts │ ├── layouts/ # 版面配置 │ │ ├── DefaultLayout.vue │ │ ├── AuthLayout.vue │ │ └── EmptyLayout.vue │ ├── middleware/ # 中間件 │ │ ├── auth.ts │ │ └── permission.ts │ ├── pages/ # 頁面元件 │ │ ├── auth/ │ │ │ ├── Login.vue │ │ │ └── Register.vue │ │ ├── dashboard/ │ │ └── user/ │ ├── plugins/ # 插件 │ │ ├── axios.ts │ │ ├── i18n.ts │ │ └── router.ts │ ├── router/ # 路由設定 │ │ ├── modules/ # 路由模組 │ │ │ ├── auth.ts │ │ │ └── dashboard.ts │ │ └── index.ts │ ├── stores/ # Pinia 狀態管理 │ │ ├── modules/ │ │ │ ├── auth.ts │ │ │ └── user.ts │ │ └── index.ts │ ├── styles/ # 樣式檔案 │ │ ├── globals.css │ │ ├── variables.css │ │ └── components/ │ ├── types/ # TypeScript 型別定義 │ │ ├── api.ts │ │ ├── auth.ts │ │ └── global.ts │ ├── utils/ # 工具函式 │ │ ├── format.ts │ │ ├── validation.ts │ │ └── storage.ts │ ├── App.vue # 根元件 │ └── main.ts # 應用程式進入點 ├── tests/ # 測試檔案 │ ├── unit/ # 單元測試 │ ├── e2e/ # E2E 測試 │ └── __mocks__/ # Mock 檔案 ├── .env # 環境變數 ├── .env.development ├── .env.production ├── .eslintrc.js # ESLint 設定 ├── .prettierrc # Prettier 設定 ├── tailwind.config.js # Tailwind CSS 設定 ├── vite.config.ts # Vite 設定 ├── tsconfig.json # TypeScript 設定 └── package.json # 套件管理 1.2 檔案命名原則 檔案類型對應命名方式 Vue 元件: PascalCase (如 UserProfile.vue) TypeScript 檔案: camelCase (如 userService.ts) CSS/SCSS 檔案: kebab-case (如 user-profile.scss) 測試檔案: 與被測檔案同名 + .test 或 .spec (如 UserProfile.test.ts) 型別定義檔案: camelCase + .d.ts (如 userTypes.d.ts) 特殊檔案命名 頁面元件: PascalCase,通常以頁面功能命名 (如 UserManagement.vue) Layout 元件: PascalCase + Layout 後綴 (如 DashboardLayout.vue) Store 檔案: camelCase,以業務領域命名 (如 userStore.ts) API 檔案: camelCase,以 API 服務命名 (如 userApi.ts) 2. 命名規範 2.1 檔案與資料夾命名 資料夾命名 使用 kebab-case (小寫字母 + 連字號) 名稱應簡潔且具描述性 ✅ 正確範例 user-management/ auth-service/ api-client/ ❌ 錯誤範例 UserManagement/ authService/ API_Client/ 檔案命名 Vue 元件檔案: PascalCase JavaScript/TypeScript 檔案: camelCase 樣式檔案: kebab-case 設定檔案: kebab-case 或 camelCase (依慣例) // ✅ 正確範例 UserProfile.vue userService.ts user-profile.scss vite.config.ts // ❌ 錯誤範例 userprofile.vue UserService.ts user_profile.scss vite_config.ts 2.2 變數與函式命名 JavaScript/TypeScript 變數 使用 camelCase 常數使用 UPPER_SNAKE_CASE 私有變數以 _ 開頭 布林值變數使用 is、has、can、should 等前綴 // ✅ 正確範例 const userName = 'John Doe'; const API_BASE_URL = 'https://api.example.com'; const _privateVariable = 'private'; const isLoggedIn = true; const hasPermission = false; const canEdit = true; const shouldUpdate = false; // ❌ 錯誤範例 const user_name = 'John Doe'; const apiBaseUrl = 'https://api.example.com'; // 常數應使用大寫 const privateVariable = 'private'; // 私有變數缺少前綴 const loggedIn = true; // 布林值缺少前綴 函式命名 使用 camelCase 動詞開頭,描述函式的動作 事件處理器使用 handle 前綴 取得資料使用 get、fetch 前綴 設定資料使用 set、update 前綴 // ✅ 正確範例 function getUserData() { } function handleButtonClick() { } function validateEmail() { } function formatCurrency() { } function updateUserProfile() { } function fetchUserList() { } // ❌ 錯誤範例 function userData() { } // 缺少動詞 function buttonClick() { } // 事件處理器缺少 handle 前綴 function email() { } // 不明確的命名 function currency() { } // 不明確的命名 2.3 Vue 元件命名 元件名稱 使用 PascalCase 多個單字組合,避免單一單字 基礎元件使用 Base 前綴 業務元件使用具體的業務領域命名 <!-- ✅ 正確範例 --> <script setup lang="ts"> // 元件檔案: UserProfile.vue </script> <script setup lang="ts"> // 元件檔案: BaseButton.vue </script> <script setup lang="ts"> // 元件檔案: PaymentForm.vue </script> <!-- ❌ 錯誤範例 --> <script setup lang="ts"> // 元件檔案: user.vue - 命名太簡短 </script> <script setup lang="ts"> // 元件檔案: button.vue - 應使用 BaseButton </script> Props 命名 定義時使用 camelCase HTML 模板中使用 kebab-case <script setup lang="ts"> // ✅ 正確範例 interface Props { userName: string; isVisible: boolean; maxLength?: number; } const props = withDefaults(defineProps<Props>(), { maxLength: 100 }); </script> <template> <!-- HTML 模板中使用 kebab-case --> <UserProfile :user-name="currentUser" :is-visible="showProfile" :max-length="200" /> </template> Event 命名 使用 kebab-case 動詞開頭,描述事件的動作 <script setup lang="ts"> // ✅ 正確範例 const emit = defineEmits<{ 'update:modelValue': [value: string]; 'user-created': [user: User]; 'form-submitted': [data: FormData]; 'item-selected': [item: Item]; }>(); // 觸發事件 emit('user-created', newUser); emit('form-submitted', formData); </script> 3. 程式撰寫風格與 Lint 設定 3.1 ESLint 設定 eslint.config.js 範例 import { defineConfig } from 'eslint-define-config'; import vue from 'eslint-plugin-vue'; import typescript from '@typescript-eslint/eslint-plugin'; import typescriptParser from '@typescript-eslint/parser'; import prettier from 'eslint-plugin-prettier'; export default defineConfig([ { files: ['**/*.{js,ts,vue}'], languageOptions: { parser: typescriptParser, parserOptions: { ecmaVersion: 2022, sourceType: 'module', extraFileExtensions: ['.vue'] } }, plugins: { vue, '@typescript-eslint': typescript, prettier }, rules: { // Vue 規則 'vue/multi-word-component-names': 'error', 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 'vue/no-unused-vars': 'error', 'vue/no-multiple-template-root': 'off', // Vue 3 支援多個根元素 'vue/script-setup-uses-vars': 'error', // TypeScript 規則 '@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-non-null-assertion': 'warn', // 通用規則 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 'prefer-const': 'error', 'no-var': 'error', 'object-shorthand': 'error', 'prefer-template': 'error', // Prettier 規則 'prettier/prettier': 'error' } } ]); 3.2 Prettier 設定 .prettierrc 範例 { "semi": true, "trailingComma": "es5", "singleQuote": true, "printWidth": 80, "tabWidth": 2, "useTabs": false, "bracketSpacing": true, "bracketSameLine": false, "arrowParens": "avoid", "endOfLine": "lf", "vueIndentScriptAndStyle": true, "htmlWhitespaceSensitivity": "css" } 3.3 TypeScript 撰寫規範 型別定義規範 // ✅ 正確範例 - 使用 interface 定義物件型別 interface User { id: number; name: string; email: string; isActive: boolean; createdAt: Date; updatedAt?: Date; // 選擇性屬性 } // ✅ 正確範例 - 使用 type 定義聯合型別 type Status = 'pending' | 'approved' | 'rejected'; type Theme = 'light' | 'dark' | 'auto'; // ✅ 正確範例 - 泛型使用 interface ApiResponse<T> { data: T; message: string; success: boolean; code: number; } // ✅ 正確範例 - 函式型別定義 type EventHandler<T = Event> = (event: T) => void; type AsyncFunction<T> = () => Promise<T>; 函式撰寫規範 // ✅ 正確範例 - 明確的參數和回傳型別 async function fetchUserData(userId: number): Promise<User | null> { try { const response = await api.get<ApiResponse<User>>(`/users/${userId}`); return response.data.data; } catch (error) { console.error('Failed to fetch user data:', error); return null; } } // ✅ 正確範例 - 箭頭函式與型別推斷 const formatCurrency = (amount: number, currency = 'TWD'): string => { return new Intl.NumberFormat('zh-TW', { style: 'currency', currency, }).format(amount); }; // ✅ 正確範例 - 高階函式 const createValidator = <T>( validator: (value: T) => boolean, errorMessage: string ) => { return (value: T): ValidationResult => ({ isValid: validator(value), message: validator(value) ? '' : errorMessage, }); }; 3.4 Vue 3 Composition API 撰寫規範 <script setup> 結構規範 <script setup lang="ts"> // 1. 導入 Vue 相關 API import { ref, computed, watch, onMounted } from 'vue'; // 2. 導入 Composables import { useAuth } from '@/composables/useAuth'; import { useApi } from '@/composables/useApi'; // 3. 導入其他模組 import { formatDate } from '@/utils/format'; import type { User } from '@/types/user'; // 4. 定義 Props 介面 interface Props { userId: number; isEditable?: boolean; } // 5. 定義 Emits 介面 interface Emits { 'user-updated': [user: User]; 'error': [error: string]; } // 6. 宣告 Props 和 Emits const props = withDefaults(defineProps<Props>(), { isEditable: false, }); const emit = defineEmits<Emits>(); // 7. 響應式資料 const user = ref<User | null>(null); const loading = ref(false); const error = ref<string>(''); // 8. 計算屬性 const displayName = computed(() => { return user.value ? `${user.value.firstName} ${user.value.lastName}` : ''; }); const canEdit = computed(() => { return props.isEditable && user.value?.isActive; }); // 9. 監聽器 watch( () => props.userId, async (newUserId) => { if (newUserId) { await loadUser(); } }, { immediate: true } ); // 10. 方法 const loadUser = async (): Promise<void> => { loading.value = true; error.value = ''; try { const userData = await fetchUserData(props.userId); user.value = userData; } catch (err) { error.value = '載入使用者資料失敗'; emit('error', error.value); } finally { loading.value = false; } }; const updateUser = async (userData: Partial<User>): Promise<void> => { if (!user.value) return; try { const updatedUser = await updateUserData(user.value.id, userData); user.value = updatedUser; emit('user-updated', updatedUser); } catch (err) { error.value = '更新使用者資料失敗'; emit('error', error.value); } }; // 11. 生命週期鉤子 onMounted(() => { console.log('Component mounted'); }); // 12. 暴露給父元件的方法/屬性 (如需要) defineExpose({ loadUser, updateUser, }); </script> 3.5 程式碼品質規範 註解撰寫規範 /** * 使用者資料服務類別 * * 提供使用者相關的 API 操作方法,包含: * - 取得使用者資料 * - 更新使用者資料 * - 刪除使用者 * * @example * ```typescript * const userService = new UserService(); * const user = await userService.getUser(123); * ``` */ export class UserService { /** * 根據 ID 取得使用者資料 * * @param userId - 使用者 ID * @returns 使用者資料,如果找不到則回傳 null * @throws {ApiError} 當 API 請求失敗時拋出錯誤 * * @example * ```typescript * const user = await userService.getUser(123); * if (user) { * console.log(user.name); * } * ``` */ async getUser(userId: number): Promise<User | null> { // TODO: 實作快取機制 // FIXME: 處理網路錯誤重試邏輯 try { const response = await this.api.get(`/users/${userId}`); return response.data; } catch (error) { // 記錄錯誤但不拋出,讓呼叫方決定如何處理 console.error(`Failed to fetch user ${userId}:`, error); return null; } } } 錯誤處理規範 // ✅ 正確範例 - 統一的錯誤處理 export class ApiError extends Error { constructor( message: string, public status: number, public code?: string ) { super(message); this.name = 'ApiError'; } } // ✅ 正確範例 - 錯誤邊界處理 const handleApiError = (error: unknown): never => { if (error instanceof ApiError) { switch (error.status) { case 401: // 重新導向到登入頁面 router.push('/login'); break; case 403: // 顯示權限不足訊息 showErrorMessage('您沒有權限執行此操作'); break; case 500: // 顯示伺服器錯誤訊息 showErrorMessage('伺服器發生錯誤,請稍後再試'); break; default: showErrorMessage(error.message); } } else { // 未知錯誤 console.error('Unexpected error:', error); showErrorMessage('發生未知錯誤'); } throw error; }; 4. 元件開發規範 4.1 Vue 單檔元件 (SFC) 結構 標準 SFC 結構順序 <!-- 1. 模板區域 --> <template> <div class="user-profile"> <!-- 內容 --> </div> </template> <!-- 2. 邏輯區域 --> <script setup lang="ts"> // 邏輯代碼 </script> <!-- 3. 樣式區域 --> <style scoped lang="scss"> // 樣式代碼 </style> 完整元件範例 <template> <div class="user-card" :class="cardClasses"> <!-- 頭像區域 --> <div class="user-card__avatar"> <img :src="user.avatar || defaultAvatar" :alt="`${user.name} 的頭像`" class="user-card__avatar-img" @error="handleImageError" /> <div v-if="showStatus" class="user-card__status" :class="statusClass"> {{ statusText }} </div> </div> <!-- 使用者資訊 --> <div class="user-card__content"> <h3 class="user-card__name">{{ user.name }}</h3> <p class="user-card__email">{{ user.email }}</p> <!-- 標籤 --> <div v-if="user.tags?.length" class="user-card__tags"> <span v-for="tag in user.tags" :key="tag" class="user-card__tag" > {{ tag }} </span> </div> <!-- 動作按鈕 --> <div class="user-card__actions"> <BaseButton variant="primary" size="small" :disabled="loading" @click="handleEdit" > 編輯 </BaseButton> <BaseButton variant="secondary" size="small" :disabled="loading" @click="handleView" > 查看 </BaseButton> </div> </div> <!-- 載入狀態 --> <div v-if="loading" class="user-card__loading"> <LoadingSpinner size="small" /> </div> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import BaseButton from '@/components/base/BaseButton.vue'; import LoadingSpinner from '@/components/base/LoadingSpinner.vue'; import { useI18n } from 'vue-i18n'; import type { User } from '@/types/user'; // Props 定義 interface Props { user: User; variant?: 'default' | 'compact' | 'detailed'; showStatus?: boolean; interactive?: boolean; } // Emits 定義 interface Emits { edit: [user: User]; view: [user: User]; 'avatar-error': [user: User]; } const props = withDefaults(defineProps<Props>(), { variant: 'default', showStatus: true, interactive: true, }); const emit = defineEmits<Emits>(); // Composables const { t } = useI18n(); // 響應式資料 const loading = ref(false); const defaultAvatar = '/images/default-avatar.png'; // 計算屬性 const cardClasses = computed(() => ({ [`user-card--${props.variant}`]: true, 'user-card--interactive': props.interactive, 'user-card--loading': loading.value, })); const statusClass = computed(() => ({ 'user-card__status--online': props.user.isOnline, 'user-card__status--offline': !props.user.isOnline, })); const statusText = computed(() => { return props.user.isOnline ? t('common.online') : t('common.offline'); }); // 方法 const handleEdit = (): void => { if (!props.interactive || loading.value) return; emit('edit', props.user); }; const handleView = (): void => { if (!props.interactive || loading.value) return; emit('view', props.user); }; const handleImageError = (): void => { emit('avatar-error', props.user); }; </script> <style scoped lang="scss"> .user-card { @apply bg-white rounded-lg shadow-md p-4 transition-all duration-200; &--interactive { @apply hover:shadow-lg cursor-pointer; } &--loading { @apply opacity-50 pointer-events-none; } &__avatar { @apply relative flex-shrink-0; } &__avatar-img { @apply w-12 h-12 rounded-full object-cover; } &__status { @apply absolute -bottom-1 -right-1 px-2 py-1 text-xs rounded-full text-white; &--online { @apply bg-green-500; } &--offline { @apply bg-gray-400; } } &__content { @apply flex-1 ml-4; } &__name { @apply text-lg font-semibold text-gray-900 mb-1; } &__email { @apply text-sm text-gray-600 mb-2; } &__tags { @apply flex flex-wrap gap-1 mb-3; } &__tag { @apply px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded; } &__actions { @apply flex gap-2; } &__loading { @apply absolute inset-0 flex items-center justify-center bg-white bg-opacity-75; } // 變體樣式 &--compact { @apply p-2; .user-card__avatar-img { @apply w-8 h-8; } .user-card__name { @apply text-base; } } &--detailed { @apply p-6; .user-card__avatar-img { @apply w-16 h-16; } } } </style> 4.2 Props 設計規範 Props 型別定義 // ✅ 正確範例 - 完整的 Props 介面 interface ButtonProps { // 必要屬性 label: string; // 選擇性屬性with default values variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; size?: 'small' | 'medium' | 'large'; disabled?: boolean; loading?: boolean; // 複雜型別 icon?: { name: string; position: 'left' | 'right'; }; // 函式型別 onClick?: (event: MouseEvent) => void; } const props = withDefaults(defineProps<ButtonProps>(), { variant: 'primary', size: 'medium', disabled: false, loading: false, }); Props 驗證 // ✅ 正確範例 - 執行時驗證 interface FormInputProps { modelValue: string; type?: 'text' | 'email' | 'password' | 'number'; placeholder?: string; required?: boolean; maxLength?: number; pattern?: string; validator?: (value: string) => boolean | string; } const props = withDefaults(defineProps<FormInputProps>(), { type: 'text', required: false, }); // 自定義驗證邏輯 const isValid = computed(() => { if (props.required && !props.modelValue) { return false; } if (props.maxLength && props.modelValue.length > props.maxLength) { return false; } if (props.pattern && !new RegExp(props.pattern).test(props.modelValue)) { return false; } if (props.validator) { const result = props.validator(props.modelValue); return result === true; } return true; }); 4.3 Emits 事件規範 事件定義與觸發 // ✅ 正確範例 - 型別安全的事件定義 interface FormEmits { // v-model 雙向綁定 'update:modelValue': [value: string]; // 表單事件 submit: [data: FormData]; cancel: []; // 驗證事件 'validation-error': [errors: ValidationError[]]; 'validation-success': []; // 使用者互動事件 'field-focus': [fieldName: string]; 'field-blur': [fieldName: string, value: string]; } const emit = defineEmits<FormEmits>(); // 事件觸發範例 const handleSubmit = (formData: FormData): void => { // 驗證表單 const errors = validateForm(formData); if (errors.length > 0) { emit('validation-error', errors); return; } emit('validation-success'); emit('submit', formData); }; const handleCancel = (): void => { emit('cancel'); }; // v-model 實作 const updateValue = (newValue: string): void => { emit('update:modelValue', newValue); }; 4.4 Slots 使用規範 具名插槽設計 <template> <div class="card"> <!-- 標題插槽 --> <header v-if="$slots.header" class="card__header"> <slot name="header" :title="title" :subtitle="subtitle" /> </header> <!-- 預設內容插槽 --> <main class="card__content"> <slot :data="data" :loading="loading" /> </main> <!-- 動作按鈕插槽 --> <footer v-if="$slots.actions" class="card__actions"> <slot name="actions" :save="handleSave" :cancel="handleCancel" :canSave="canSave" /> </footer> <!-- 條件式插槽 --> <div v-if="$slots.sidebar" class="card__sidebar"> <slot name="sidebar" /> </div> </div> </template> <script setup lang="ts"> interface Props { title?: string; subtitle?: string; data: any; loading?: boolean; } const props = defineProps<Props>(); // 計算屬性 const canSave = computed(() => { return !props.loading && isDataValid(props.data); }); // 提供給插槽的方法 const handleSave = (): void => { // 儲存邏輯 }; const handleCancel = (): void => { // 取消邏輯 }; </script> 插槽使用範例 <template> <Card :data="userData" :loading="loading"> <!-- 標題插槽 --> <template #header="{ title, subtitle }"> <h2>{{ title || '使用者資料' }}</h2> <p v-if="subtitle">{{ subtitle }}</p> </template> <!-- 預設內容插槽 --> <template #default="{ data, loading }"> <div v-if="!loading"> <UserForm :user="data" @update="handleUserUpdate" /> </div> <LoadingSpinner v-else /> </template> <!-- 動作插槽 --> <template #actions="{ save, cancel, canSave }"> <BaseButton variant="primary" :disabled="!canSave" @click="save" > 儲存 </BaseButton> <BaseButton variant="secondary" @click="cancel" > 取消 </BaseButton> </template> </Card> </template> 4.5 元件組合與複用 高階元件 (HOC) 模式 <!-- withLoading.vue - 高階元件 --> <template> <div class="with-loading"> <div v-if="loading" class="loading-overlay"> <LoadingSpinner :size="loadingSize" /> <p v-if="loadingText">{{ loadingText }}</p> </div> <div :class="{ 'is-loading': loading }"> <slot :loading="loading" /> </div> </div> </template> <script setup lang="ts"> interface Props { loading: boolean; loadingText?: string; loadingSize?: 'small' | 'medium' | 'large'; } withDefaults(defineProps<Props>(), { loadingSize: 'medium', }); </script> 組合式元件使用 <template> <WithLoading :loading="isLoading" loading-text="載入使用者資料中..."> <UserProfile :user="userData" @edit="handleEdit" @delete="handleDelete" /> </WithLoading> </template> <script setup lang="ts"> import WithLoading from '@/components/hoc/WithLoading.vue'; import UserProfile from '@/components/UserProfile.vue'; const isLoading = ref(false); const userData = ref(null); const handleEdit = (user: User): void => { // 編輯邏輯 }; const handleDelete = (user: User): void => { // 刪除邏輯 }; </script> 5. 樣式與 Tailwind CSS 規範 5.1 Tailwind CSS 設定 tailwind.config.js 範例 /** @type {import('tailwindcss').Config} */ export default { content: [ './index.html', './src/**/*.{vue,js,ts,jsx,tsx}', ], theme: { extend: { // 顏色系統 colors: { primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', // 主要品牌色 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554', }, secondary: { 50: '#f8fafc', 500: '#64748b', 900: '#0f172a', }, success: { 50: '#f0fdf4', 500: '#22c55e', 900: '#14532d', }, warning: { 50: '#fffbeb', 500: '#f59e0b', 900: '#78350f', }, danger: { 50: '#fef2f2', 500: '#ef4444', 900: '#7f1d1d', }, }, // 字型設定 fontFamily: { sans: [ 'Noto Sans TC', 'Microsoft JhengHei', 'PingFang TC', 'Helvetica Neue', 'Arial', 'sans-serif', ], }, // 響應式斷點 screens: { 'xs': '475px', 'sm': '640px', 'md': '768px', 'lg': '1024px', 'xl': '1280px', '2xl': '1536px', }, }, }, plugins: [ require('@tailwindcss/forms'), require('@tailwindcss/typography'), ], }; 5.2 RWD 響應式設計原則 Mobile-First 設計策略 <template> <!-- 響應式網格系統 --> <div class="container mx-auto px-4"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div v-for="item in items" :key="item.id" class="card"> <!-- 響應式圖片 --> <img :src="item.image" :alt="item.title" class="w-full h-32 sm:h-40 lg:h-48 object-cover" /> <!-- 響應式文字 --> <div class="p-4"> <h3 class="text-lg sm:text-xl lg:text-2xl font-semibold"> {{ item.title }} </h3> <p class="text-sm sm:text-base text-gray-600"> {{ item.description }} </p> </div> </div> </div> </div> <!-- 響應式導航 --> <nav class="bg-white shadow"> <!-- 桌面版導航 --> <div class="hidden lg:flex items-center space-x-8 px-6 py-4"> <a v-for="link in navLinks" :key="link.path" :href="link.path"> {{ link.title }} </a> </div> <!-- 行動版選單 --> <div class="lg:hidden"> <button @click="toggleMobileMenu" class="p-4"> <MenuIcon class="w-6 h-6" /> </button> </div> </nav> </template> 5.3 顏色與主題設計 CSS 變數主題系統 :root { /* 品牌色 */ --color-primary: #3b82f6; --color-secondary: #64748b; /* 語意化顏色 */ --color-success: #22c55e; --color-warning: #f59e0b; --color-danger: #ef4444; /* 背景色 */ --bg-primary: #ffffff; --bg-secondary: #f8fafc; /* 文字色 */ --text-primary: #111827; --text-secondary: #4b5563; } /* 暗色主題 */ [data-theme="dark"] { --bg-primary: #111827; --bg-secondary: #1f2937; --text-primary: #f9fafb; --text-secondary: #d1d5db; } 5.4 共用樣式元件 基礎元件樣式 @layer components { /* 按鈕樣式 */ .btn { @apply inline-flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2; } .btn-primary { @apply btn bg-primary-500 text-white hover:bg-primary-600; } .btn-secondary { @apply btn bg-gray-200 text-gray-900 hover:bg-gray-300; } /* 表單樣式 */ .form-input { @apply w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-1 focus:ring-primary-500 focus:border-primary-500; } /* 卡片樣式 */ .card { @apply bg-white rounded-lg shadow-md p-4; } } 總結 本前端開發指引提供了完整的開發規範,涵蓋了專案結構、命名規範、程式撰寫風格、元件開發和樣式設計等核心面向。請開發團隊嚴格遵循這些規範,以確保程式碼品質和專案的可維護性。 ...

81 min · 17161 words · Eric Cheng

角色: 你是一個資深的系統架師,也是AP 性能調較高手 環境: OS : AIX, ap server:IBM WebSphere Liberty, datbase: db2, 使用sprint boot 開發, 壓測使用Jmeter 任務: 壓測一小時, 同時50人, 95% request 必需在3秒內完成, 請列出壓測應注意那些參數設定與調校 chatGPT 回答 好的 ✅ 以下我會用「資深系統架構師 & AP 性能調校高手」的角度,幫你整理 JMeter 壓測設定、Liberty 調校、DB2 調校、AIX/系統層級參數 四大面向,確保你的一小時壓測(50人同時、95% request < 3 秒)能達標。 1. 壓測工具 (JMeter) 層 測試場景設計 Ramp-Up 時間:避免一次 50 人瞬間打進系統,建議 30–60 秒平滑增加,觀察系統穩態。 Think Time:模擬使用者真實行為,避免「機械式打爆」;可設定 0.5–2 秒隨機等待。 持續時間模式:壓測 60 分鐘,避免只測瞬間峰值。 監控數據: 95% 响應時間 Throughput (TPS) Error Rate DB2 Connection Pool 狀況 JVM Heap/GC 次數 JMeter 參數建議 非 GUI 模式執行(jmeter -n -t test.jmx -l result.jtl) ...

8 min · 1672 words · Eric Cheng

Gemini 回答 好的,作為一個資深的系統架構師與 AP 性能調校專家,我會為您提供一份在這次壓力測試中,從頭到尾、由外到內需要關注的參數設定與調校檢查清單。 我們的目標非常明確:在 50 個併發用戶(Concurrent Users)持續加壓一小時的條件下,確保 95% 的請求(95th Percentile)回應時間在 3 秒以內。 這是一個系統性的工程,我們需要從壓力來源、作業系統、應用伺服器、一直到資料庫,逐層分析與調校。 核心調校哲學 建立基準 (Establish Baseline): 在進行任何調校之前,先用預設設定跑一次縮短版的壓力測試(例如 10-15 分鐘)。這個數據是你的黃金標準,所有的後續調校都必須跟這個基準線做比較,才能判斷是改善還是惡化。 一次只改一個參數 (Change One Thing at a Time): 這是最重要的原則。如果你一次改了三個參數,效能變好了,你不會知道是哪個參數起了作用。反之,如果效能變差,你也不知道是誰的錯。 監控是你的眼睛 (Monitoring is Key): 如果沒有監控,調校就如同矇著眼睛開車。你必須能夠即時看到 CPU、記憶體、網路、I/O、GC、執行緒等關鍵指標的變化。 第一層:壓力來源 (JMeter) 確保壓力測試工具本身不是瓶頸。 參數/項目 建議與說明 啟動模式 絕對要使用命令列模式 (CLI Mode) 進行正式壓測,不要使用 GUI 模式。GUI 模式會消耗大量客戶端資源,影響測試準確性。指令範例:jmeter -n -t your_test_plan.jmx -l results.jtl JVM Heap Size 修改 jmeter.bat 或 jmeter.sh,調整 JMeter 的 JVM 堆疊大小。如果請求或回應的資料量大,預設的 1G 可能不夠。建議至少設為 HEAP="-Xms2g -Xmx2g" 或更高,避免 JMeter 本身 OutOfMemory。 監聽器 (Listeners) 在正式壓測時,禁用或刪除所有 GUI 上的監聽器,如 “View Results Tree”、“Summary Report” 等。它們非常消耗資源。測試結束後,再用 results.jtl 檔來產生報告。 思考時間 (Think Time) 在請求之間加入合理的思考時間 (e.g., Uniform Random Timer),模擬真實用戶行為。完全沒有思考時間的壓測會對伺服器造成不切實際的瞬間衝擊,可能無法反映真實場景的效能瓶頸。 分散式壓測 如果單台 JMeter 機器無法產生足夠的壓力(CPU/網路滿載),應考慮使用主從式 (Master-Slave) 的分散式架構來發動壓測。 第二層:作業系統 (AIX) AIX 有其獨特的監控與調校工具,是穩定運行的基礎。 ...

7 min · 1478 words · Eric Cheng

安全程式碼指引 目錄 文件目的 通用安全開發原則 2.1 最小權限原則 2.2 輸入驗證 2.3 錯誤處理 2.4 加密使用原則 2.5 安全標頭設定 2.6 Session 與 Token 管理 程式語言安全指引 3.1 Java / Spring Boot 3.2 Python 3.3 JavaScript / TypeScript / Vue3 3.4 資料庫安全 OWASP Top 10 對應對策 4.1 A01:2021 – Broken Access Control (存取控制失效) 4.2 A02:2021 – Cryptographic Failures (加密失效) 4.3 A03:2021 – Injection (注入攻擊) 4.4 A04:2021 – Insecure Design (不安全設計) 4.5 A05:2021 – Security Misconfiguration (安全設定錯誤) 4.6 A06:2021 – Vulnerable Components (易受攻擊元件) 4.7 A07:2021 – Identification and Authentication Failures (識別與驗證失效) 4.8 A08:2021 – Software and Data Integrity Failures (軟體與資料完整性失效) 4.9 A09:2021 – Security Logging & Monitoring Failures (安全日誌與監控失效) 4.10 A10:2021 – Server-Side Request Forgery (SSRF) API 安全設計 5.1 RESTful API 安全 5.2 GraphQL 安全 5.3 API 版本控制與向後相容 5.4 API 速率限制與節流 容器化與雲端安全 6.1 Docker 安全 6.2 Kubernetes 安全 6.3 雲端服務安全 CI/CD 安全 7.1 代碼儲存庫安全 7.2 建構流程安全 7.3 部署安全 7.4 供應鏈安全 安全測試與驗證 8.1 靜態應用程式安全測試 (SAST) 8.2 動態應用程式安全測試 (DAST) 8.3 互動式應用程式安全測試 (IAST) 8.4 滲透測試 資料保護與隱私 9.1 個人資料保護 9.2 資料分類與標記 9.3 資料遮罩與匿名化 9.4 資料備份與復原 事件回應與復原 10.1 安全事件識別 10.2 事件回應流程 10.3 取證與證據保全 10.4 災難復原計畫 日常開發檢查清單 (Checklist) 11.1 每日開發檢查項目 11.2 Pull Request 檢查項目 11.3 部署前檢查項目 常見錯誤與反例 12.1 密碼處理錯誤 12.2 SQL 查詢錯誤 12.3 檔案上傳錯誤 12.4 前端 XSS 錯誤 12.5 權限控制錯誤 12.6 API 設計錯誤 12.7 配置錯誤 合規性與法規要求 13.1 GDPR 合規 13.2 個資法合規 13.3 PCI DSS 合規 13.4 SOX 合規 延伸資源 14.1 官方安全指引 14.2 程式語言特定資源 14.3 安全工具 14.4 學習資源 14.5 公司內部資源 1. 文件目的 安全程式碼的撰寫是每位開發者的基本職責。良好的安全設計不僅能保護公司與用戶資料,避免資安事件造成商譽損失與法律責任,也有助於符合法規(如 GDPR、個資法)及客戶合約要求。安全程式碼能降低維運成本、減少漏洞修補時間,讓業務更穩健發展。 ...

24 min · 5108 words · Eric Cheng

後端開發指引 目錄 開發原則 專案結構與命名規範 API 設計規範 資料庫存取與 ORM 規範 安全性規範 效能與擴展性指引 測試與品質保證 部署與維運指引 日誌管理與監控 資料驗證與清理 國際化與本地化 文件生成與 API 規範 第三方整合規範 程式碼審查與品質控制 依賴與配置管理 備份與災難恢復 1. 開發原則 1.1 架構模式 Clean Architecture 實作原則 依賴反轉原則:內層不依賴外層,外層依賴內層 單一職責原則:每個類別/模組只負責一個職責 開放封閉原則:對擴展開放,對修改封閉 介面隔離原則:使用者不應依賴不需要的介面 架構分層結構: ┌─────────────────────────────────────┐ │ Presentation Layer │ ← Controllers, DTOs ├─────────────────────────────────────┤ │ Application Layer │ ← Use Cases, Services ├─────────────────────────────────────┤ │ Domain Layer │ ← Entities, Repositories ├─────────────────────────────────────┤ │ Infrastructure Layer │ ← Database, External APIs └─────────────────────────────────────┘ 分層設計規範 Presentation Layer(表現層) ...

31 min · 6451 words · Eric Cheng