# 第八堂:期末總結與專案整合 ## 開課提醒 1. 記得開錄影 2. 下載作業:<https://github.com/hexschool/js-camp-week8>,`npm install` 3. 根目錄建 `.env`,填 API Path 和 Token(<https://livejs-api.hexschool.io/> 申請) ## 今日上課內容 1. 鳥瞰:五個模組依賴關係 2. 驗證 pattern 深化:多欄位驗證兩種種類 3. Service pattern 深化:validate-then-do + success/error 統一包裝 --- ## 一、鳥瞰專案 ### 先看全圖再動手 > 直接從 api.js 開填 → 寫到一半會迷路。先全圖,再下筆。 ### 五個模組的依賴關係 ![2026-04-20-architecture](https://hackmd.io/_uploads/BJPi2kwabg.png) 檔案結構: ``` js-camp-week8/ ├── config.js ← 設定層(讀 .env) ├── api.js ← API 層(打後端) ├── utils.js ← 工具層(純邏輯) ├── services/ ← 服務層(業務邏輯) │ ├── productService.js │ ├── cartService.js │ └── orderService.js └── app.js ← 入口層(串起來跑) ``` | 層 | 責任 | 可打後端?| 依賴誰 | |---|---|---|---| | config | 讀 .env | ❌ | 不依賴 | | api | 打後端 | ✅ 唯一可以 | config | | utils | 純邏輯 | ❌ | 不依賴 | | services | 組 api + utils | ❌(透過 api)| api + utils | | app | 串 services 跑流程 | ❌ | services + utils | > 依賴是**樹狀不是堆疊** — services 同時呼叫 api 和 utils(兩個平行下層),utils 不碰 api。 ### 為什麼要拆模組 | | 全塞 app.js | 分層 | |---|---|---| | 行數 | 500+ 一大包 | 每檔 50~100 | | 改 API url | 找半天 | 只改 config | | 換驗證規則 | 找半天 | 只改 utils | | 獨立測試 | 要開網路跑真 API | utils 能離線測 | | 多人協作 | 打架 | 一人顧一層 | > 把第七堂 Calculator 物件的概念放大 — 收散函式 → 收檔案。 > 學完這章 → 寫任務一 api.js。第一波:套第七堂 axios + `.env`,打通 13 個 API 測試。 --- ## 二、驗證 pattern 深化 ### 驗證的兩種種類 | 種類 | 使用時機 | 回傳格式 | |---|---|---| | 一次回一個錯 | 單一規則、一錯就停 | `{ isValid, error }` | | 一次回所有錯 | 多欄位、一次列出所有錯 | `{ isValid, errors: [...] }` | > 使用者體驗差很多:一次看完所有錯改一次 OK;一個一個報錯要送 5 次表單。 ### 一次回一個錯:購物車數量 1–99 正整數 ```jsx function validateQuantity(qty) { if (!Number.isInteger(qty)) return { isValid: false, error: '必須是整數' }; if (qty < 1) return { isValid: false, error: '不可小於 1' }; if (qty > 99) return { isValid: false, error: '不可大於 99' }; return { isValid: true }; } validateQuantity(5); // { isValid: true } validateQuantity(0); // { isValid: false, error: '不可小於 1' } validateQuantity(5.5); // { isValid: false, error: '必須是整數' } ``` > `Number.isInteger` 同時擋小數與非數字,一石二鳥。 ### 一次回所有錯:會員註冊五欄位 ```jsx function validateSignup(data) { const errors = []; if (!data.name) errors.push('姓名不可為空'); if (!/^09\d{8}$/.test(data.tel || '')) errors.push('電話格式錯誤'); if (!data.email?.includes('@')) errors.push('Email 格式錯誤'); if (!data.address) errors.push('地址不可為空'); if (!['ATM', 'Credit Card', 'Apple Pay'].includes(data.payment)) { errors.push('付款方式不正確'); } return { isValid: errors.length === 0, errors }; } validateSignup({ name: '', tel: '1234', email: 'abc', address: '', payment: 'Bitcoin' }); // { isValid: false, errors: ['姓名不可為空', '電話格式錯誤', 'Email 格式錯誤', '地址不可為空', '付款方式不正確'] } ``` > `data.email?.includes('@')` 裡的 `?.` 是 optional chaining — data.email 是 undefined 也不會炸。 ### 選擇指南 | 情境 | 用哪個 | |---|---| | 單一輸入(年齡、密碼長度) | 一次回一個錯 | | 多欄位表單(註冊、訂單) | 一次回所有錯 | | 送 API 前的最後防線 | 一次回所有錯 | ### toLocaleString 千分位 ```jsx (1000).toLocaleString('zh-TW'); // '1,000' (1234567).toLocaleString('zh-TW'); // '1,234,567' function formatCurrency(amount) { return `NT$ ${Number(amount).toLocaleString('zh-TW')}`; } formatCurrency(18000); // 'NT$ 18,000' ← Louvre 雙人床架 ``` ### getDaysAgo 的坑:要先 startOf('day') ```jsx // ❌ 直接 diff:同一天但時分不同,會算成 -1 天 dayjs().diff(dayjs.unix(ts), 'day'); // ✅ 先 startOf('day'):對齊到 00:00 再比 const today = dayjs().startOf('day'); const target = dayjs.unix(ts).startOf('day'); const days = today.diff(target, 'day'); if (days === 0) return '今天'; return `${days} 天前`; ``` --- ## 三、Service pattern 深化 ### Service 是什麼 | 職責 | 說明 | |---|---| | 組 api + utils | 拉低層工具 + 後端呼叫,做一件完整業務 | | 統一回傳格式 | 永遠回 `{ success, data }` 或 `{ success, error }` | | 先驗證再執行 | 資料不合格不打 API(validate-then-do) | ### 兩種 Service 形狀 | 形狀 | 何時用 | 結構 | |---|---|---| | 純讀取 | GET 類、篩選類 | `api → filter/map → return` | | validate-then-do | 寫入類(add / update / place) | `validate → call API → early return` | ### 獨立範例:WOWOROOM 床架下單服務 ```jsx // 情境:客人下訂床架的 service(不是作業直接要寫的函式) function validateOrder(order) { const errors = []; if (!order.bedId) errors.push('請選床架'); if (!order.name) errors.push('姓名必填'); if (!order.address) errors.push('地址必填'); return { isValid: errors.length === 0, errors }; } async function submitOrderAPI(order) { const res = await axios.post('/shop/orders', { data: order }); return res.data; } async function orderBed(order) { // 1. 驗證失敗 → early return const v = validateOrder(order); if (!v.isValid) return { success: false, errors: v.errors }; // 2. 打 API const result = await submitOrderAPI(order); // 3. API 失敗 → early return if (!result.status) return { success: false, error: result.message }; // 4. 成功 return { success: true, data: result }; } orderBed({ bedId: '', name: '', address: '' }); // { success: false, errors: ['請選床架', '姓名必填', '地址必填'] } ``` 骨架:**驗證錯 → early return** → **打 API** → **API 錯 → early return** → **成功 return**。 ### 為什麼統一 { success, data/error } | | 不統一 | 統一 | |---|---|---| | 呼叫端判斷 | 有時檢查 `.error`、有時看 null、有時各自發揮 | 永遠 `if (result.success)` | | 錯誤訊息 | 有 throw、return null、自訂格式 | 永遠 `result.error` | | 測試 mock | 每函式寫法不同 | 同一個 pattern 一套搞定 | > productService 全是純讀取(最簡單),cart 和 order 才有寫入版本要驗證包裝。 ### validate-then-do 標準骨架 ```jsx async function serviceAction(input, payload) { // 1. 驗證錯 → early return const v = validateXXX(input); if (!v.isValid) return { success: false, error: v.error }; // 2. 打 API const result = await apiCall(payload); // 3. API 錯 → early return if (!result.status) return { success: false, error: result.message }; // 4. 成功 return { success: true, data: result }; } ``` ### 坑點:errors 陣列 vs error 字串 | 函式 | 驗證型 | 失敗回傳 | |---|---|---| | `addProductToCart` / `updateProduct` | 一次回一個錯(validateCartQuantity)| `{ success: false, error: '...' }` | | `placeOrder` | 一次回所有錯(validateOrderUser)| `{ success: false, errors: [...] }` | > 驗證回 `error`(單數)還是 `errors`(複數)跟著驗證函式走 — 回一個錯用單數 `error`,回所有錯用複數 `errors` 陣列。 --- ## 本週任務 必做: 1. 主線任務:<https://rpg.hexschool.com/#/training/12063182914161572765/board/content/12063182914161572766_12063182914161572788?tid=12063182914167567616> 2. 繳交期限:(老闆填入) 目標:`npm test` 跑出 75/75 全綠。 ## 正課結束,下方為加碼環節 ## AI 實驗室 * [安裝教學](https://drive.google.com/file/d/1JgyF8hBSkcU0p5ZA-OIv440gsRUshkps/view?usp=sharing) * 講義:https://gonsakon.github.io/claude-code-training/#/ ## 下一步 * 完成主線任務,截止 2026/05/10 23:59 * 心得牆 * [Node.js 預習](https://courses.hexschool.com/courses/enrolled/2929773?preview=admin)