# 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 生命週期)

---
### 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>
```