---
# System prepended metadata

title: 第八堂：期末總結與專案整合
tags: [2026-後端-JS]

---

# 第八堂：期末總結與專案整合

## 開課提醒
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)
