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