---
# System prepended metadata

title: API 整合基礎，深入 Axios 應用與特點解析，實作 API 發送請求

---

# API 整合基礎，深入 Axios 應用與特點解析，實作 API 發送請求

## 什麼是 API？

API（Application Programming Interface，應用程式介面）＝ 前端與後端之間的橋樑。
像餐廳服務生：你（前端）下單 → 服務生（API）去廚房（後端）拿資料 → 端回結果。

### 最小可跑範例（index.html + Fetch）
https://jsonplaceholder.typicode.com

```htmlembedded=
<!DOCTYPE html>
<html lang="zh-Hant">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>API Demo</title></head>
<body>
  <script>
    // 取得一筆 Todo
    fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(res => res.json())
      .then(data => console.log('Todo:', data));
  </script>
</body>
</html>
```

> 打開 DevTools 的 Console 可看到 JSON 結果。

<br/>

### 四種主要作法（快速對照）

#### 1. XMLHttpRequest（XHR，老方法）

**特色**：最原始、可用，但寫法冗長。
**適合**：維護舊專案時。

```javascript
const req = new XMLHttpRequest();
req.open('GET', 'https://randomuser.me/api/');
req.onload = function () { console.log(JSON.parse(this.responseText)); };
req.onerror = () => console.log('Network Error');
req.send();
```

---

#### 2. jQuery.ajax（曾經的主流）

**特色**：比 XHR 好寫、但需載入 jQuery。
**適合**：既有 jQuery 專案。

```htmlembedded=
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$.ajax({
  url: 'https://randomuser.me/api/',
  dataType: 'json',
  success: (data) => console.log(data),
  error: (_, __, err) => console.log('錯誤:', err),
});
</script>
```

---

#### 3. Fetch（現代瀏覽器原生）

**特色**：原生、語法簡潔、回傳用 Promise。
**注意**：4xx/5xx 需自行用 `res.ok` 判斷；預設不帶 cookie。

```javascript=
fetch('https://randomuser.me/api/')
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .then(data => console.log(data))
  .catch(err => console.log('錯誤:', err));
```

---

#### 4. Axios（實務好用封裝）

**特色**：自動 JSON、攔截器、超時/取消、並發。
**注意**：需外掛腳本；**無法繞過 CORS**（必須後端允許）。

```htmlembedded=
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.get('https://randomuser.me/api/')
  .then(res => console.log(res.data))
  .catch(err => console.log('錯誤:', err))
  .finally(() => console.log('done'));
</script>
```

---

### 一頁 CRUD（以 Fetch 為例）

```javascript=
const BASE = 'https://jsonplaceholder.typicode.com/posts';

// GET
fetch(`${BASE}?_limit=3`).then(r=>r.json()).then(console.log);

// POST
fetch(BASE, {
  method: 'POST',
  headers: { 'Content-Type':'application/json' },
  body: JSON.stringify({ title:'Hello', body:'content', userId:1 })
}).then(r=>r.json()).then(console.log);

// PUT（全量）
fetch(`${BASE}/1`, {
  method: 'PUT',
  headers: { 'Content-Type':'application/json' },
  body: JSON.stringify({ id:1, title:'New', body:'...', userId:1 })
}).then(r=>r.json()).then(console.log);

// PATCH（局部）
fetch(`${BASE}/1`, {
  method: 'PATCH',
  headers: { 'Content-Type':'application/json' },
  body: JSON.stringify({ title:'Only Title' })
}).then(r=>r.json()).then(console.log);

// DELETE
fetch(`${BASE}/1`, { method:'DELETE' })
  .then(r=>r.json()).then(console.log);
```

[線上範例](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/api-base)

---


### 常見坑位

* **CORS**：需由後端設定允許；前端（含 Axios/Fetch）**不能繞過**。
* **Fetch 錯誤判斷**：`res.ok` 才算成功；4xx/5xx 需自行丟錯。
* **Cookie/認證**：Fetch 預設不帶 cookie，需 `credentials: 'include'`；Axios 可用攔截器掛 Token。
* **JSON 與物件**：`JSON.stringify()` / `JSON.parse()`；`'{}'`（字串）≠ `{}`（物件）。
* **超時/取消**：Fetch 用 `AbortController`；Axios 直接支援 `timeout` 與 `signal`。

<br/>


## 異步操作入門：從事件迴圈到 async/await

### 1. 同步 vs. 異步（先有直覺）

* **同步**：一件做完才做下一件（像排隊結帳）
* **異步**：交付工作後先放著，等完成再通知（像點餐取號碼牌）

```js=
console.log('A');
setTimeout(() => console.log('B (2s later)'), 2000);
console.log('C');
// 輸出順序：A → C →（2秒後）B
```


---

### 2. 事件迴圈（Event Loop）心智模型

瀏覽器執行時大致分三塊：

```
Call Stack   <-- 正在執行中的 JS
   │
   ▼
Web APIs     <-- 計時器、網路 I/O、DOM 事件等待完成
   │
   ▼
Task Queues
  ├─ Macrotask Queue   (setTimeout, setInterval, DOM events, I/O)
  └─ Microtask Queue   (Promise.then, queueMicrotask, MutationObserver)
```

🎯 規則重點：

1. **先清空 Microtask**，再處理下一個 Macrotask。
2. `Promise.then`（microtask）會比 `setTimeout`（macrotask）優先。

**觀察順序小實驗：**

```js=
console.log('1: start');
setTimeout(() => console.log('4: setTimeout'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('2: end');
// 輸出：1 → 2 → 3 → 4
```

---

### 3. 非同步的三個時代

#### 3.1 Callback（容易走向 callback hell）

```js
readA((aErr, a) => {
  if (aErr) return handle(aErr);
  readB(a, (bErr, b) => {
    if (bErr) return handle(bErr);
    readC(b, (cErr, c) => { /* ... */ });
  });
});
```

#### 3.2 Promise（把流程「拉直」，可鏈式、可併發）

```js
readA()
  .then(a => readB(a))
  .then(b => readC(b))
  .then(c => console.log('done', c))
  .catch(handle);
```

#### 3.3 async/await（最可讀）

```js
try {
  const a = await readA();
  const b = await readB(a);
  const c = await readC(b);
  console.log('done', c);
} catch (err) {
  handle(err);
}
```

---

### 4. 網路請求的異步實務

#### 4.1 基本錯誤處理（Fetch 需手動檢查 `res.ok`）

```js
async function getJSON(url, options) {
  const res = await fetch(url, options);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
```

#### 4.2 取消請求（AbortController）

```js
const ac = new AbortController();
getJSON('/api/data', { signal: ac.signal }).catch(console.warn);
// 路由切換/離開頁面時：
ac.abort();
```

#### 4.3 簡易重試（退避機制）

```js
async function getWithRetry(url, tries = 2, delay = 500) {
  try {
    return await getJSON(url);
  } catch (e) {
    if (tries <= 0) throw e;
    await new Promise(r => setTimeout(r, delay));
    return getWithRetry(url, tries - 1, delay * 2); // 指數退避
  }
}
```

#### 4.4 多請求併發 / 收斂

```js
// 同時發出，全部成功才算成功
const all = await Promise.all([getJSON('/a'), getJSON('/b')]);

// 不論成功或失敗都回報
const settled = await Promise.allSettled([getJSON('/a'), getJSON('/b')]);

// 任一成功就回傳
const any = await Promise.any([getJSON('/a'), getJSON('/b')]);
```

---

### 5. 常見地雷 & 解法

* ❌ **`await` 搭配 `forEach`**（不等待）
  ✅ 用 `for...of` 或收集成陣列後 `await Promise.all`
  ```js
  // 正確並行
  const results = await Promise.all(ids.map(id => getJSON(`/item/${id}`)));
  ```
* ❌ **Fetch 不會自動對 4xx/5xx 拋錯**
  ✅ 檢查 `res.ok`，手動丟錯（見 4.1）
* ❌ **混用 `.then()` 與 `await` 造成邏輯分散**
  ✅ 選一種風格，整段一致可讀
* ❌ **忘記取消長時間請求/計時器**
  ✅ 路由卸載或元件 unmount 時 `abort()` / `clearTimeout`
* ❌ **把 Token 長存 localStorage**（風險高）
  ✅ 正式環境偏向 **HttpOnly Cookie**（課堂示範可用 localStorage）

---

### 6. UI 與異步：三態管理

非同步請求通常會有 **Loading / Success / Error** 三態。

```js
state = { loading: false, data: null, error: null };

async function load() {
  state.loading = true; state.error = null;
  try {
    state.data = await getJSON('/api/list');
  } catch (e) {
    state.error = e.message;
  } finally {
    state.loading = false;
  }
}
```


---

### 7) 可直接用在 `index.html` 的小範例

#### 7.1 任務順序觀察（Micro vs. Macro）

```htmlembedded=
<script>
  console.log('A');
  setTimeout(() => console.log('D: timeout(0)'), 0);
  Promise.resolve().then(() => console.log('C: microtask'));
  console.log('B');
</script>
```

> 觀察輸出：A → B → C → D

#### 7.2 請求 + Loading + Error + 取消

```htmlembedded=
<button id="load">載入</button>
<button id="cancel">取消</button>
<pre id="out"></pre>
<script>
  const out = document.getElementById('out');
  const loadBtn = document.getElementById('load');
  const cancelBtn = document.getElementById('cancel');
  let ac;

  async function getJSON(url, options) {
    const res = await fetch(url, options);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }

  loadBtn.onclick = async () => {
    ac?.abort();
    ac = new AbortController();
    out.textContent = '載入中…';
    try {
      const data = await getJSON('https://jsonplaceholder.typicode.com/posts?_limit=5', { signal: ac.signal });
      out.textContent = JSON.stringify(data, null, 2);
    } catch (e) {
      out.textContent = '錯誤：' + e.message;
    }
  };
  cancelBtn.onclick = () => ac?.abort();
</script>
```

#### 7.3 併發 vs. 串行

```htmlembedded=
<script>
  const ids = [1,2,3,4,5];
  // 併發（較快）
  Promise.all(ids.map(id => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(r=>r.json())))
    .then(list => console.log('並行結果', list));

  // 串行（有順序、較慢）
  (async () => {
    const results = [];
    for (const id of ids) {
      const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(r=>r.json());
      results.push(res);
    }
    console.log('串行結果', results);
  })();
</script>
```

---

### 8. 備忘錄（Cheat Sheet）

* **順序**：`microtask`（`Promise.then`）比 `macrotask`（`setTimeout`）早
* **錯誤**：Fetch 對 4xx/5xx 不丟錯 → `if (!res.ok) throw`
* **取消**：`AbortController`（`signal`）
* **併發**：`Promise.all / allSettled / any / race`
* **反模式**：`forEach + await`、混用 `.then()` 與 `await`

<br/>


## 了解 Promise

### 1.Promise 是什麼？為什麼需要？
之前，非同步多用 **callback**，容易形成「**回呼地獄**」（巢狀過深、錯誤難傳遞、流程難讀）。
**Promise** 把「非同步結果」封裝成一個可鏈式的物件，讓流程**拉直**、錯誤**一致處理**，可讀性大幅提升。

**生活比喻**：你在餐廳點餐會拿到一張 **號碼牌（Promise）**。

* 餐點完成 → **成功（fulfilled / resolved）**
* 餐點缺料 → **失敗（rejected）**
* 還在做 → **待定（pending）**

（下圖：Promise 生命週期）
![](https://i.imgur.com/ULZf6SW.png)

---

### 2. 三種狀態與生命週期

* **pending**：建立後尚未完成
* **fulfilled**：成功完成 → 走 `.then(onFulfilled)`
* **rejected**：失敗完成 → 走 `.catch(onRejected)`
* **settled**：已完成（不論成功或失敗），可用 `.finally()` 做收尾

> 重點：每次呼叫 `.then/.catch` 都會**回傳一個新的 Promise**（可鏈式）。

---

### 3. 最小可讀範例（含錯誤與收尾）

```js
new Promise((resolve, reject) => {
  console.log('初始化…');
  // 模擬非同步
  setTimeout(() => Math.random() > 0.5 ? resolve('OK') : reject(new Error('Sold out')), 500);
})
.then(value => {
  console.log('成功：', value);
  return '下一步資料'; // 傳給後面 then
})
.then(next => {
  console.log('鏈式接續：', next);
})
.catch(err => {
  console.log('失敗：', err.message);
})
.finally(() => {
  console.log('不管成功失敗都會執行（清理資源/關 loading）');
});
```

### 4. 手工包裝一個非同步（標準寫法）

> 將「回呼式 API / 計時器 / 事件」**Promise 化**，就能統一用 `.then/.catch` 或 `await`。

```js
function timeout(ms) {
  return new Promise((resolve) => {
    const id = setTimeout(() => resolve(ms), ms);
    // 若需要取消，可回傳清理函式（配合外部控制）
  });
}

timeout(300).then(ms => console.log(`等了 ${ms}ms`));
```

---

### 5. 鏈式流程與錯誤傳遞

#### 5.1 回傳值會往下一個 then 傳

```js
Promise.resolve(1)
  .then(x => x + 1)       // 2
  .then(x => Promise.resolve(x * 3)) // 6（回傳 Promise 也可）
  .then(console.log);     // 6
```

#### 5.2 錯誤會沿鏈往下冒泡到最近的 catch

```js
Promise.resolve()
  .then(() => { throw new Error('爆了'); })
  .then(() => console.log('不會到這'))
  .catch(err => console.log('接到錯誤：', err.message))
  .then(() => console.log('錯誤處理後可以繼續'));
```

---

### 6. 一次處理多個非同步（工具箱）

```js
const p1 = Promise.resolve(3);
const p2 = 1337; // 非 Promise 會自動包成成功的 Promise
const p3 = new Promise(r => setTimeout(() => r('foo'), 2000));

// 全部成功才成功；任一失敗就進 catch
Promise.all([p1, p2, p3]).then(values => {
  console.log('all:', values); // [3, 1337, "foo"]
});

// 不論成敗都回報
Promise.allSettled([p1, p3]).then(results => {
  console.log('allSettled:', results);
});

// 任一成功就成功（忽略失敗）
Promise.any([
  Promise.reject('bad'),
  new Promise(r => setTimeout(() => r('first ok'), 500)),
]).then(v => console.log('any:', v));

// 誰先完成就回誰（成功或失敗都可能）
Promise.race([p3, timeout(100)]).then(v => console.log('race:', v));
```

---

### 7. `async/await`：把 Promise 寫得像同步

> `await` 其實就是在**等待一個 Promise** 完成；外層用 `try/catch` 收錯。

```js
function getJSON(url) {
  return fetch(url).then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  });
}

async function main() {
  try {
    const a = await getJSON('https://jsonplaceholder.typicode.com/todos/1');
    const b = await getJSON('https://jsonplaceholder.typicode.com/todos/2');
    console.log(a.title, b.title);
  } catch (e) {
    console.log('錯誤：', e.message);
  } finally {
    console.log('完成');
  }
}
main();
```

> 並行化技巧：需要同時發多個請求時，用 `Promise.all` 再 `await`

```js
const [a, b] = await Promise.all([getJSON(url1), getJSON(url2)]);
```

<br/>

## Axois 基礎學習


### 1. 黑板公式

```js
// 1) 通用格式
axios(config)

// 2) 常用快捷
axios.get(url, config)                 // 查詢用 params
axios.post(url, data, config)          // 送資料用 data（JSON/表單等）
```

#### config 最常用鍵

* `baseURL` 請求共同前綴
* `params`  查詢字串（會變成 `?a=1&b=2`）
* `data`    請求主體（POST/PUT/PATCH 用）
* `headers` 自訂標頭（如 `Content-Type`、`Authorization`）
* `timeout` 逾時毫秒
* `signal`  可取消請求（`AbortController`）
* `withCredentials` 是否夾帶 Cookie

#### 回應物件（記 4 個就好）

* `res.data`    伺服器回的真正資料
* `res.status`  HTTP 狀態碼
* `res.headers` 標頭
* `res.config`  當時的設定

#### 錯誤物件（記 3 個）

* `err.message`   錯誤訊息
* `err.response`  伺服器有回（含 `status/data`）
* `err.request`   請求送出但沒回應（網路/CORS/逾時）

---

### 2. 兩張口訣卡

**A. params vs data**

* `GET` 用 **`params`**（放 URL 後面）
* `POST/PUT/PATCH` 用 **`data`**（放請求主體）

**B. 最小錯誤處理模板**

```js
try {
  const res = await axios.get(url);
  // if (res.status !== 200) ...（通常不必，Axios 4xx/5xx 會丟錯）
} catch (err) {
  // err.response 伺服器有回；err.request 送出沒回
}
```


<br/>

## Axios API 串接學習

### 1. 基礎專案
```htmlembedded=
<!DOCTYPE html>
<html lang="zh-Hant">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Step1 最小 GET</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <!-- 內容放置區 -->
  </body>
</html>
```

---

### 2. 最小 GET（先感受 Axios）

**目標**：會打第一個 API，看到回應資料。
**重點**：`axios.get(url)`、`res.data`。

```htmlembedded=
    <button id="btn">GET /todos/1</button>
    <pre id="out">// 結果顯示</pre>

    <script>
      const $ = (s) => document.querySelector(s);
      $('#btn').onclick = async () => {
        try {
          const res = await axios.get(
            'https://jsonplaceholder.typicode.com/todos/1',
          );
          $('#out').textContent = JSON.stringify(res.data, null, 2);
        } catch (e) {
          $('#out').textContent = e.message;
        }
      };
    </script>
```

---

### 3. 查詢參數與 POST（Body/Headers）
**目標**：理解 `params`、JSON Body 與 `Content-Type`。
```htmlembedded=
    <button id="btnQ">GET /posts?_limit=3</button>
    <button id="btnP">POST /posts</button>
    <pre id="out">// 結果顯示</pre>

    <script>
      const $ = (s) => document.querySelector(s);
      $('#btnQ').onclick = async () => {
        const r = await axios.get(
          'https://jsonplaceholder.typicode.com/posts',
          { params: { _limit: 3 } },
        );
        $('#out').textContent = JSON.stringify(r.data, null, 2);
      };
      $('#btnP').onclick = async () => {
        const r = await axios.post(
          'https://jsonplaceholder.typicode.com/posts',
          { title: 'Hello', body: 'content', userId: 1 },
          { headers: { 'Content-Type': 'application/json' } },
        );
        $('#out').textContent = JSON.stringify(r.data, null, 2);
      };
    </script>
```

---

### 4. 建立 axios instance（可複用設定）


**目標**：用 `axios.create()` 統一 `baseURL/timeout`。

```html=
    <button id="btn">GET /todos?_limit=5（用 instance）</button>
    <pre id="out">// 結果顯示</pre>
    <script>
      const api = axios.create({
        baseURL: 'https://jsonplaceholder.typicode.com',
        timeout: 8000,
      });

      document.querySelector('#btn').onclick = async () => {
        const r = await api.get('/todos', { params: { _limit: 5 } });
        document.querySelector('#out').textContent = JSON.stringify(
          r.data,
          null,
          2,
        );
      };
    </script>
```

---

### 5. 攔截器 + 權限（自動掛 Token、統一錯誤）

**目標**：在請求前自動帶 `Authorization`，在回應時統一錯誤訊息。

```html=
    <button id="login">模擬登入(寫 Token)</button>
    <button id="logout">登出(清 Token)</button>
    <button id="call">GET /users（自動帶權限）</button>
    <pre id="out">// 結果顯示</pre>

    <script>
      const $ = (s) => document.querySelector(s);
      const authed = axios.create({
        baseURL: 'https://jsonplaceholder.typicode.com',
        timeout: 8000,
      });

      // 請求攔截器：帶上 Token
      authed.interceptors.request.use((cfg) => {
        const token = localStorage.getItem('demo_token');
        if (token) cfg.headers.Authorization = `Bearer ${token}`;
        return cfg;
      });
      // 回應攔截器：錯誤同格式
      authed.interceptors.response.use(
        (r) => r,
        (e) =>
          Promise.reject(
            new Error(`API Error ${e.response?.status ?? ''}: ${e.message}`),
          ),
      );

      $('#login').onclick = () => {
        localStorage.setItem('demo_token', 'fake-123');
        $('#out').textContent = '已寫入 Token';
      };
      $('#logout').onclick = () => {
        localStorage.removeItem('demo_token');
        $('#out').textContent = '已清除 Token';
      };
      $('#call').onclick = async () => {
        try {
          const r = await authed.get('/users');
          $('#out').textContent = JSON.stringify(r.data, null, 2);
        } catch (e) {
          $('#out').textContent = e.message;
        }
      };
    </script>
```

---


### 6. 錯誤與逾時（httpstat.us 模擬）

**目標**：會抓 4xx/5xx 錯誤、設定 `timeout`。

```html=
    <button id="btn404">觸發 404</button>
    <button id="btnTO">逾時 2 秒（伺服器睡 5 秒）</button>
    <pre id="out">// 結果顯示</pre>

    <script>
      const $ = (s) => document.querySelector(s);
      $('#btn404').onclick = async () => {
        try {
          await axios.get('https://httpstat.us/404');
        } catch (e) {
          $('#out').textContent = e.message;
        }
      };
      $('#btnTO').onclick = async () => {
        try {
          await axios.get('https://httpstat.us/200?sleep=5000', {
            timeout: 2000,
          });
        } catch (e) {
          $('#out').textContent = e.message;
        }
      };
    </script>
```

---

### 7. 取消請求（AbortController）

**目標**：送出後可中止長時間請求。

```html=
    <button id="start">送出慢速請求</button>
    <button id="cancel">取消</button>
    <pre id="out">// 結果顯示</pre>

    <script>
      let ac;
      const $ = (s) => document.querySelector(s);
      $('#start').onclick = async () => {
        ac?.abort();
        ac = new AbortController();
        $('#out').textContent = '請求中…';
        try {
          const r = await axios.get('https://httpstat.us/200?sleep=5000', {
            signal: ac.signal,
          });
          $('#out').textContent = '完成：' + r.status;
        } catch (e) {
          $('#out').textContent = '已取消或錯誤：' + e.message;
        }
      };
      $('#cancel').onclick = () => ac?.abort();
    </script>
```

---

### 8. 多請求併發（Promise.all）

**目標**：送出後可中止長時間請求。

```html=
    <button id="btn">同時抓 /posts 與 /users</button>
    <pre id="out">// 結果顯示</pre>
    <script>
      const $ = (s) => document.querySelector(s);
      const api = axios.create({
        baseURL: 'https://jsonplaceholder.typicode.com',
      });
      $('#btn').onclick = async () => {
        try {
          const [posts, users] = await Promise.all([
            api.get('/posts', { params: { _limit: 2 } }),
            api.get('/users', { params: { _limit: 2 } }),
          ]);
          $('#out').textContent = JSON.stringify(
            { posts: posts.data, users: users.data },
            null,
            2,
          );
        } catch (e) {
          $('#out').textContent = e.message;
        }
      };
    </script>
```

---

### 建議事項
* **Instance 一定要建**：`create({ baseURL, timeout })`，之後好統一改。
* **攔截器兩件事**：請求掛 Token、回應統一錯誤。
* **三態管理**：Loading / Success / Error；錯誤一定 `try/catch`。
* **取消與逾時**：路由切換要 `abort()`；長請求要設 `timeout`。
* **CORS**：要後端允許；前端（含 Axios/Fetch）無法繞過。

<br/>

## 簡單應用測試

```htmlembedded
<!DOCTYPE html>
<html lang="zh-Hant">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Todo List</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <button id="login">模擬登入(寫 Token)</button>
    <button id="logout">登出(清 Token)</button>
    <button id="call">GET /todo/projects（自動帶權限）</button>
    <pre id="out">// 結果顯示</pre>

    <script>
      const $ = (s) => document.querySelector(s);
      const authed = axios.create({
        baseURL: 'http://localhost:8000/api',
        timeout: 8000,
      });

      // 請求攔截器：帶上 Token
      authed.interceptors.request.use((cfg) => {
        const token = localStorage.getItem('demo_token');
        if (token) cfg.headers.Authorization = `Bearer ${token}`;
        return cfg;
      });

      $('#login').onclick = async () => {
        const response = await authed.post('/auth/jwt/create', {
          username: 'admin',
          password: 'admin',
        });

        localStorage.setItem('demo_token', response.data.access);
        $('#out').textContent = '已寫入 Token';
      };
      $('#logout').onclick = () => {
        localStorage.removeItem('demo_token');
        $('#out').textContent = '已清除 Token';
      };
      $('#call').onclick = async () => {
        try {
          const r = await authed.get('/v1/todo/projects');
          $('#out').textContent = JSON.stringify(r.data, null, 2);
        } catch (e) {
          $('#out').textContent = e.message;
        }
      };
    </script>
  </body>
</html>

```