本篇將帶你了解為什麼需要非同步,以及如何用 Promise 與 async/await 優雅地撰寫非同步流程。接著我們用 Fetch 實作 AJAX 請求,並補充瀏覽器端儲存(Cookie、LocalStorage、SessionStorage)與 Session/Token 的觀念對比。
非同步程式設計基礎 瀏覽器在執行 JavaScript 時,僅有單一主執行緒,會按照程式碼的順序逐行執行,這種方式稱為「同步作業」。當遇到需要較長時間才能完成的任務時,若全部採用同步執行,會導致整個流程被阻塞,影響使用者體驗。為了解決這個問題,JavaScript 採用「非同步作業」:將耗時的任務交由瀏覽器或背景處理,等任務完成後再將結果放回事件佇列,等待主執行緒有空時再處理,讓網頁能持續回應其他操作。
所謂「非同步(Asynchronous)」就是讓瀏覽器在等待這些耗時任務(例如:網路請求、檔案讀取、計時器)完成的同時,主執行緒仍能繼續處理其他工作、維持畫面互動與回應;反之,「同步(Synchronous)」會讓執行緒被卡住,導致畫面凍結、操作停滯,造成不佳的使用者體驗。
blocking-vs-async.js function blockingSleep (ms ) { const end = Date .now () + ms; while (Date .now () < end) {} } console .log ('同步開始' );blockingSleep (2000 ); console .log ('同步結束' );console .log ('非同步開始' );setTimeout (() => { console .log ('2 秒後執行(非同步),期間不會卡住 UI/其他程式碼' ); }, 2000 ); console .log ('非同步結束(先印出)' );
上述範例用來說明「同步」與「非同步」的差異。setTimeout
是一種非同步(async)機制,當呼叫時,瀏覽器會將該任務登記起來,等到指定時間後再執行,而主程式流程則不會被阻塞,能繼續往下執行。
同步阻塞會造成後續程式碼與畫面更新延遲,影響使用者體驗。
非同步計時器(如 setTimeout)則能將耗時任務延後執行,讓主流程不中斷,提升網頁的流暢度與回應性。
事件迴圈(Event Loop)概念 事件迴圈(Event Loop)是 JavaScript 執行非同步任務的核心機制,負責監控主執行緒(呼叫堆疊)是否空閒,並依序將「待處理工作」從任務佇列安排進來執行。這些任務主要分為兩大類:
宏任務(Macro Task) :如 setTimeout, setInterval, DOM Event Callback 等,屬於較大型、會影響主流程的任務。
微任務(Micro Task) :如 Promise.then, queueMicrotask,通常用於更細緻、需優先處理的非同步流程。
小技巧:事件迴圈運作順序 每次主執行緒(呼叫堆疊)清空後,事件迴圈會先檢查微任務佇列,將所有微任務依序執行完畢,才會處理下一個宏任務。這也是為什麼 Promise.then 會比 setTimeout 優先執行的原因。
graph TD
A["呼叫堆疊<br/>Call Stack"] -->|清空時| B["事件迴圈<br/>Event Loop"]
B --> D["微任務佇列<br/>Micro Task Queue"]
D --> A
B --> C["宏任務佇列<br/>Macro Task Queue"]
C --> A
巢狀地獄(callback hell) 在實際的網頁開發中,我們經常需要串接多個 API 來完成一個完整的資料流程。例如:先取得使用者資訊,再用使用者 ID 去取得該使用者的相簿列表,最後用相簿 ID 去取得相片資料。每個 API 請求都需要等待伺服器回應,這個等待時間可能從幾毫秒到幾秒不等。
當資料需要不斷抽取至下一個 API 時,就會造成「一個等一個」的串接現象。早期開發者使用「回呼函式(callback)」來處理這種非同步流程,但隨著 API 串接層數增加,程式碼會形成深層的巢狀結構,這就是所謂的「巢狀地獄(callback hell)」。
模擬 API 串接現象 在練習中,我們使用 setTimeout
來模擬真實 API 的等待時間。這讓我們可以在沒有實際後端服務的情況下,體驗非同步程式設計的各種情況。
callback-hell.js function getUserAPI (id, callback, onError ) { console .warn (`level 1: 開始取得使用者 ${id} 的資料。..` ); setTimeout (() => { if (false ) { console .log (`level 1: 使用者 ${id} 資料取得失敗` ); onError (new Error (`level 1: 找不到使用者 ID: ${id} ` )); return ; } console .log (`level 1: 成功,返回使用者 ${id} 資料` ); callback ({ id : id, name : 'Loki' , email : 'loki@example.com' }); }, 2000 ); } function getProfileAPI (userId, callback, onError ) { console .warn (`level 2: 開始取得使用者 ${userId} 的個人資料。..` ); setTimeout (() => { if (false ) { console .log (`level 2: 使用者 ${userId} 個人資料取得失敗` ); onError (new Error (`level 2: 個人資料載入失敗,使用者可能已停權` )); return ; } console .log (`level 2: 成功,返回使用者 ${userId} 個人資料` ); callback ({ userId : userId, bio : '前端開發者' , avatar : 'avatar.jpg' , posts : 15 }); }, 2000 ); } function getPostsAPI (profileName, callback, onError ) { console .warn (`level 3: 開始取得 ${profileName} 的文章列表。..` ); setTimeout (() => { if (false ) { console .log (`level 3: ${profileName} 的文章列表取得失敗` ); onError (new Error (`level 3: 文章列表載入失敗,伺服器忙碌中` )); return ; } console .log (`level 3: 成功,返回${profileName} 的文章列表` ); callback ([ { id : 1 , title : 'JavaScript 基礎' , content : '...' }, { id : 2 , title : 'Promise 教學' , content : '...' }, { id : 3 , title : 'async/await 實戰' , content : '...' } ]); }, 2000 ); } console .clear ();getUserAPI (1 , function (user ) { console .log ('level 1 Done: 取得使用者:' , user.name ); getProfileAPI (user.id , function (profile ) { console .log ('level 2 Done: 取得個人資料:' , profile.bio ); getPostsAPI (profile.userId , function (posts ) { console .log ('level 3 Done: 取得文章列表:' , posts.length , '篇' ); }, function (error ) { console .error ('❌ 取得文章失敗:' , error.message ); console .log ('💡 建議:重新整理頁面或稍後再試' ); }); }, function (error ) { console .error ('❌ 取得個人資料失敗:' , error.message ); console .log ('💡 建議:檢查使用者權限或聯絡管理員' ); }); }, function (error ) { console .error ('❌ 取得使用者失敗:' , error.message ); console .log ('💡 建議:確認使用者 ID 是否正確' ); }); console .log ('=== 主程式繼續執行(非阻塞)===' );
跟著做:體驗巢狀地獄與錯誤處理 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
主程式立即印出「主程式繼續執行」
依序等待每個 API 完成(每個約 2 秒)
總共需要約 6 秒才能完成整個流程
程式碼向右縮排越來越深,難以閱讀
測試錯誤處理 :將任一函數中的 if (false)
改為 if (true)
,就能看到錯誤處理的執行流程。
這種寫法的問題:
可讀性差 :程式碼向右縮排越來越深,難以閱讀
錯誤處理分散 :每個回呼都需要單獨處理錯誤
維護困難 :新增或修改流程時容易出錯
除錯複雜 :當某個步驟出錯時,難以快速定位問題
Promise Promise 是 JavaScript 處理「非同步流程」的核心機制。它本質上是一個物件,代表「未來才會取得的結果」,狀態分為 pending(進行中)、fulfilled(已完成)、rejected(已失敗)。Promise 能將原本層層巢狀的回呼(callback hell)攤平成直覺的鏈式流程,並且將錯誤集中在 .catch()
處理,大幅提升程式碼的可讀性與維護性。
常見的 Promise 應用場景包括:計時器延遲、動畫控制、圖片載入、網路請求(如 AJAX、fetch、XHR)等所有需要等待結果的非同步任務。透過 Promise,非同步程式碼變得更容易撰寫、追蹤與除錯。
Promise 狀態流程 Promise 有三個狀態,且狀態轉換是單向的:一旦從 pending 轉換到 fulfilled 或 rejected,就無法再改變。
graph TD
A["new Promise()<br/>pending(進行中)"] --> B{執行結果}
B -->|成功| C["fulfilled(已完成)<br/>resolve(value)"]
B -->|失敗| D["rejected(已失敗)<br/>reject(error)"]
C --> E[".then() 執行"]
D --> F[".catch() 執行"]
E --> G[".finally() 執行"]
F --> G
G --> H["Promise 結束"]
style A fill:#e1f5fe
style C fill:#c8e6c9
style D fill:#ffcdd2
style E fill:#fff3e0
style F fill:#fff3e0
style G fill:#f3e5f5
Promise 狀態說明
pending :初始狀態,Promise 正在執行中
fulfilled :成功完成,會觸發 .then()
方法
rejected :執行失敗,會觸發 .catch()
方法
不可逆 :一旦狀態改變,就無法再回到 pending 或改變狀態
基本用法 Promise 是 JavaScript 內建的「建構函式(Constructor)」物件。當你使用 new Promise()
建立一個 Promise 實例時,它會自帶三個常用方法:
then :接收「成功」的結果,並可串接後續動作。每個 then 回傳的值會傳給下一個 then。
catch :集中處理「失敗」或「錯誤」的情況。只要鏈式流程中有錯誤,會自動跳到最近的 catch。
finally :無論成功或失敗,最後都會執行的收尾動作。
這三個方法讓我們能用「鏈式」方式描述非同步流程的每個步驟,讓程式碼結構清晰、錯誤處理集中,維護起來更容易。
小技巧:Promise 的三大方法
then
、catch
、finally
都是 Promise 物件的方法,可以依需求串接多個。
這種設計讓非同步流程像「流程圖」一樣直觀易懂。
promise-basic.js function wait (ms ) { return new Promise ((resolve, reject ) => { setTimeout (() => { if (Math .random () < 0.33 ) { reject (new Error ('隨機失敗!' )); } else { resolve ('等待成功' ); } }, ms); }); } console .log ('=== Promise 基本用法測試 ===' );wait (500 ) .then ((result ) => { console .log ('✅ 等待完成!' , result); return 'OK' ; }) .then ((result ) => { console .log ('✅ 收到結果:' , result); }) .catch ((err ) => console .error ('❌ 錯誤:' , err.message )) .finally (() => console .log (' 流程結束' ));
跟著做:體驗 Promise 錯誤處理 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
約 2/3 機率成功:執行 .then()
鏈
約 1/3 機率失敗:跳過 .then()
,執行 .catch()
無論成功失敗都會執行 .finally()
多執行幾次 來體驗不同的結果!
快捷方法 介紹「Promise 的快捷方法」。這些方法能讓你更快速地建立已完成(fulfilled)或已失敗(rejected)的 Promise,並在實務上靈活應用於條件判斷、預設值處理、錯誤攔截等情境。這些技巧能讓你的非同步程式碼更簡潔、易讀,也更容易維護。
Promise.resolve() 與 Promise.reject()
Promise.resolve(value)
:建立一個「已完成」的 Promise,狀態為 fulfilled
Promise.reject(error)
:建立一個「已失敗」的 Promise,狀態為 rejected
這是建立 Promise 的快捷方法,不需要寫 new Promise()
對比 Promise 的建立 以下比較「快捷方法」與「基本用法」的差異。使用 Promise.resolve()
或 Promise.reject()
會直接跳過 pending 狀態 ,不像 new Promise(...)
需要經過 resolve/reject 才改變狀態。這讓程式碼更簡潔,也更適合用於條件判斷或預設值處理。
promise-creation.js Promise .resolve ('成功' ).then (console .log );new Promise ((resolve ) => resolve ('成功' )).then (console .log );Promise .reject (new Error ('失敗' )).catch (console .error );new Promise ((resolve, reject ) => reject (new Error ('失敗' ))).catch (console .error );
實際應用場景 在實務開發中,Promise.resolve()
與 Promise.reject()
不只是語法糖,更是提升非同步流程彈性與可讀性的利器。這兩個方法常用於「條件判斷」、「參數驗證」以及「統一錯誤處理」等場景,能讓程式碼更簡潔、易於維護。以下整理幾個常見應用情境,協助你靈活掌握這些快捷技巧。若遇到尚未學過的語法,可先略過本節,日後再回來複習。
promise-real-world.js async function getUserData (userId ) { const cached = localStorage .getItem (`user_${userId} ` ); if (cached) { return Promise .resolve (JSON .parse (cached)); } const response = await fetch (`/api/users/${userId} ` ); const userData = await response.json (); localStorage .setItem (`user_${userId} ` , JSON .stringify (userData)); return userData; } function fetchUserPosts (userId ) { if (!userId || userId <= 0 ) { return Promise .reject (new Error ('無效的使用者 ID' )); } return fetch (`/api/users/${userId} /posts` ); } async function safeApiCall (apiFunction, fallbackValue ) { try { const result = await apiFunction (); return Promise .resolve (result); } catch (error) { console .error ('API 呼叫失敗,使用預設值:' , error.message ); return Promise .resolve (fallbackValue); } } safeApiCall ( () => fetch ('/api/data' ), { message : '預設資料' } ).then (console .log );
實際開發中的重要性
快取機制 :有快取時直接回傳 Promise.resolve(cachedData)
參數驗證 :驗證失敗時回傳 Promise.reject(new Error())
錯誤恢復 :API 失敗時回傳 Promise.resolve(defaultValue)
條件式邏輯 :根據條件決定是否呼叫真實 API
鏈式與錯誤傳遞 Promise 的強大之處在於可以「鏈式呼叫」,每個 .then()
的回傳值會自動傳遞給下一個 .then()
。更重要的是,錯誤會「冒泡」到最近的 .catch()
,讓我們可以集中處理所有錯誤。
鏈式呼叫的關鍵觀念
資料傳遞 :每個 .then()
的回傳值會成為下一個 .then()
的參數
錯誤冒泡 :任何步驟出錯,都會跳過後續 .then()
,直接執行 .catch()
錯誤恢復 :.catch()
可以回傳新值,讓鏈式呼叫繼續進行
統一處理 :所有錯誤都在一個地方處理,不需要分散在各個回呼中
鏈式呼叫:資料傳遞 promise-chain.js Promise .resolve (1 ) .then ((n ) => { console .log ('第一步:' , n); return n + 1 ; }) .then ((n ) => { console .log ('第二步:' , n); return n * 3 ; }) .then ((n ) => { console .log ('第三步:' , n); return `結果是 ${n} ` ; }) .then ((result ) => { console .log ('最終:' , result); });
錯誤傳遞:集中處理 promise-error-chain.js Promise .resolve (1 ) .then ((n ) => { console .log ('第一步:' , n); return n + 1 ; }) .then ((n ) => { console .log ('第二步:' , n); throw new Error ('第二步出錯了!' ); }) .then ((n ) => { console .log ('第三步:' , n); return n * 3 ; }) .catch ((error ) => { console .error ('❌ 捕捉到錯誤:' , error.message ); return '錯誤恢復值' ; }) .then ((result ) => { console .log ('✅ 錯誤處理後:' , result); });
跟著做:體驗鏈式呼叫 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
成功案例 :資料依序傳遞,每個步驟都執行
錯誤案例 :錯誤跳過中間步驟,直接到 .catch()
處理
解決巢狀地獄 現在我們將前面的巢狀地獄範例改寫成 Promise 版本,你會看到程式碼變得更加清晰易讀:
promise-solution.js function getUserPromise (id ) { return new Promise ((resolve, reject ) => { console .warn (`level 1: 開始取得使用者 ${id} 的資料。..` ); setTimeout (() => { if (false ) { console .log (`level 1: 使用者 ${id} 資料取得失敗` ); reject (new Error (`level 1: 找不到使用者 ID: ${id} ` )); return ; } console .log (`level 1: 成功,返回使用者 ${id} 資料` ); resolve ({ id : id, name : 'Loki' , email : 'loki@example.com' }); }, 2000 ); }); } function getProfilePromise (userId ) { return new Promise ((resolve, reject ) => { console .warn (`level 2: 開始取得使用者 ${userId} 的個人資料。..` ); setTimeout (() => { if (false ) { console .log (`level 2: 使用者 ${userId} 個人資料取得失敗` ); reject (new Error (`level 2: 個人資料載入失敗,使用者可能已停權` )); return ; } console .log (`level 2: 成功,返回使用者 ${userId} 個人資料` ); resolve ({ userId : userId, bio : '前端開發者' , avatar : 'avatar.jpg' , posts : 15 }); }, 2000 ); }); } function getPostsPromise (profileName ) { return new Promise ((resolve, reject ) => { console .warn (`level 3: 開始取得 ${profileName} 的文章列表。..` ); setTimeout (() => { if (false ) { console .log (`level 3: ${profileName} 的文章列表取得失敗` ); reject (new Error (`level 3: 文章列表載入失敗,伺服器忙碌中` )); return ; } console .log (`level 3: 成功,返回${profileName} 的文章列表` ); resolve ([ { id : 1 , title : 'JavaScript 基礎' , content : '...' }, { id : 2 , title : 'Promise 教學' , content : '...' }, { id : 3 , title : 'async/await 實戰' , content : '...' } ]); }, 2000 ); }); } console .clear ();console .log ('=== 開始 Promise 解決方案 ===' );getUserPromise (1 ) .then ((user ) => { console .log ('level 1 Done: 取得使用者:' , user.name ); return getProfilePromise (user.id ); }) .then ((profile ) => { console .log ('level 2 Done: 取得個人資料:' , profile.bio ); return getPostsPromise (profile.userId ); }) .then ((posts ) => { console .log ('level 3 Done: 取得文章列表:' , posts.length , '篇' ); console .log ('=== Promise 解決方案完成 ===' ); }) .catch ((error ) => { console .error ('❌ 流程中斷:' , error.message ); if (error.message .includes ('level 1' )) { console .log ('💡 建議:確認使用者 ID 是否正確' ); } else if (error.message .includes ('level 2' )) { console .log ('💡 建議:檢查使用者權限或聯絡管理員' ); } else if (error.message .includes ('level 3' )) { console .log ('💡 建議:重新整理頁面或稍後再試' ); } }); console .log ('=== 主程式繼續執行(非阻塞)===' );
跟著做:體驗 Promise 解決方案 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
主程式立即印出「主程式繼續執行」
依序等待每個 API 完成(每個約 2 秒)
總共需要約 6 秒才能完成整個流程
程式碼攤平 :不再有深層巢狀,易於閱讀
錯誤處理集中 :所有錯誤都在 .catch()
中統一處理
測試錯誤處理 :將任一函數中的 if (false)
改為 if (true)
,就能看到集中錯誤處理的效果。
Promise vs 巢狀地獄的對比
特性
巢狀地獄
Promise
可讀性
❌ 深層縮排,難以閱讀
✅ 鏈式呼叫,清晰易讀
錯誤處理
❌ 分散在各個回呼
✅ 集中在 .catch()
維護性
❌ 新增步驟困難
✅ 容易新增或修改步驟
除錯
❌ 難以追蹤錯誤來源
✅ 錯誤堆疊清晰
程式碼結構
❌ 向右延伸
✅ 向下延伸
Promise 組合方法 在實際開發中,我們經常需要「同時」執行多個非同步任務,並在全部完成後再進行後續處理。這時就可以利用 Promise 的「組合方法」來達成併行(parallel)執行的效果。這讓你能更有效率地管理多個非同步任務,避免一個一個等待,提升整體效能。
組合方法的適用情境
方法
適用情境
特點
Promise.all
需要全部資料才能繼續
任何一個失敗就整體失敗
Promise.allSettled
需要知道每個任務的結果
不管成功失敗都會完成
Promise.race
競速或超時處理
回傳最先完成的
Promise.any
多個備援方案
任一成功就算成功
Promise.all Promise.all()
會等待所有 Promise 都成功完成,如果任何一個失敗,整個 Promise.all 就會失敗。
promise-all.js function createTask (name, delay, shouldFail = false ) { return new Promise ((resolve, reject ) => { setTimeout (() => { if (shouldFail) { reject (new Error (`${name} 失敗了` )); } else { resolve (`${name} 成功` ); } }, delay); }); } console .log ('=== Promise.all 全部成功 ===' );Promise .all ([ createTask ('任務 A' , 300 ), createTask ('任務 B' , 200 ), createTask ('任務 C' , 100 ) ]) .then ((results ) => { console .log ('✅ 全部完成:' , results); }) .catch ((error ) => { console .error ('❌ 有任務失敗:' , error.message ); }); console .log ('=== Promise.all 有任務失敗 ===' );Promise .all ([ createTask ('任務 A' , 300 ), createTask ('任務 B' , 200 , true ), createTask ('任務 C' , 100 ) ]) .then ((results ) => { console .log ('✅ 全部完成:' , results); }) .catch ((error ) => { console .error ('❌ 有任務失敗:' , error.message ); });
Promise.allSettled Promise.allSettled()
會等待所有 Promise 都完成,不管成功或失敗,都會回傳每個 Promise 的狀態。
promise-allSettled.js console .log ('=== Promise.allSettled ===' );Promise .allSettled ([ createTask ('任務 A' , 300 ), createTask ('任務 B' , 200 , true ), createTask ('任務 C' , 100 ) ]) .then ((results ) => { console .log ('📊 所有任務狀態:' , results); const successful = results.filter (r => r.status === 'fulfilled' ).length ; const failed = results.filter (r => r.status === 'rejected' ).length ; console .log (`成功:${successful} 個,失敗:${failed} 個` ); });
Promise.race Promise.race()
會回傳最先完成的 Promise,不管是成功還是失敗。
promise-race.js console .log ('=== Promise.race ===' );Promise .race ([ createTask ('任務 A' , 300 ), createTask ('任務 B' , 200 ), createTask ('任務 C' , 100 ) ]) .then ((result ) => { console .log ('🏆 最先完成:' , result); }) .catch ((error ) => { console .error ('🏆 最先失敗:' , error.message ); }); console .log ('=== Promise.race 有失敗任務 ===' );Promise .race ([ createTask ('任務 A' , 300 ), createTask ('任務 B' , 150 , true ), createTask ('任務 C' , 100 ) ]) .then ((result ) => { console .log ('🏆 最先完成:' , result); }) .catch ((error ) => { console .error ('🏆 最先失敗:' , error.message ); });
Promise.any Promise.any()
會等待第一個成功的 Promise,如果全部失敗才會失敗。
promise-any.js console .log ('=== Promise.any ===' );Promise .any ([ createTask ('任務 A' , 300 ), createTask ('任務 B' , 200 , true ), createTask ('任務 C' , 100 ) ]) .then ((result ) => { console .log ('✅ 第一個成功:' , result); }) .catch ((error ) => { console .error ('❌ 全部失敗:' , error.message ); }); console .log ('=== Promise.any 全部失敗 ===' );Promise .any ([ createTask ('任務 A' , 300 , true ), createTask ('任務 B' , 200 , true ), createTask ('任務 C' , 100 , true ) ]) .then ((result ) => { console .log ('✅ 第一個成功:' , result); }) .catch ((error ) => { console .error ('❌ 全部失敗:' , error.message ); });
跟著做:體驗 Promise 組合方法 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
Promise.all :全部成功才完成,任何一個失敗就整體失敗
Promise.allSettled :等待全部完成,回傳每個任務的狀態
Promise.race :回傳最先完成的任務(不管成功失敗)
Promise.any :回傳第一個成功的任務,全部失敗才失敗
載入圖片的非同步操作 圖片載入是一個常見的非同步操作。傳統上我們會監聽 onload
和 onerror
事件,但這種寫法容易造成巢狀結構。利用 Promise,可以將圖片載入流程包裝成一個易於串接與錯誤處理的非同步任務,讓程式碼更簡潔、可讀性更高。
image-load-promise.js function loadImage (src ) { return new Promise ((resolve, reject ) => { const img = new Image (); img.onload = () => resolve (img); img.onerror = (e ) => reject (new Error ('圖片載入失敗' )); img.src = src; }); } loadImage ('https://picsum.photos/200' ) .then ((img ) => console .log ('圖片寬度:' , img.width )) .catch ((err ) => console .error (err.message ));
async/await async/await
是 ES2017 新增的語法糖,讓你能用「幾乎像同步程式」的方式撰寫非同步流程,大幅提升程式碼的可讀性與維護性。它其實是 Promise 的進階寫法,讓非同步邏輯變得更直覺、易懂,閱讀起來就像一般的直線程式碼。
核心概念 async/await 的主要目的是讓非同步程式碼看起來像同步程式碼 ,隱藏了 Promise 的 .then()
和 .catch()
語法,讓程式碼更容易閱讀和理解。
async :宣告一個非同步函式,該函式會自動將回傳值包裝成 Promise
await :等待 Promise 完成,只能在 async
函式內使用
錯誤處理 :使用傳統的 try/catch
語法處理錯誤,不需要 .catch()
為什麼需要 async? async
是為了使用 await
而存在的。await
只能在 async
函式內使用,這是 JavaScript 的語法規則。
function wrongFunction ( ) { const result = await somePromise (); } async function correctFunction ( ) { const result = await somePromise (); }
async 函式的特性 除了讓你使用 await
之外,async 函式還有一個特性:會自動將回傳值包裝成 Promise。透過這個特性,讓 async 函式仍可以與 Promise 鏈式呼叫相容。
function normalFunction ( ) { return "Hello" ; } console .log (normalFunction ()); async function asyncFunction ( ) { return "Hello" ; } console .log (asyncFunction ()); asyncFunction ().then (console .log );
與 Promise 對比差異 在學習 JavaScript 的非同步處理時,Promise
和 async/await
是兩種常見的寫法。雖然它們本質上都基於 Promise,但語法和錯誤處理方式有所不同。本節將透過範例,對比這兩種寫法的差異,幫助你理解何時該選用哪一種方式。
comparison.js function wait (ms ) { return new Promise ((resolve ) => setTimeout (resolve, ms)); } function fetchNumberAfter (ms, n ) { return new Promise ((resolve ) => setTimeout (() => resolve (n), ms)); } function promiseVersion ( ) { console .log ('開始' ); wait (300 ) .then (() => { console .log ('等待完成' ); return fetchNumberAfter (200 , 10 ); }) .then ((a ) => { return fetchNumberAfter (200 , 5 ).then ((b ) => a + b); }) .then ((result ) => { console .log ('結果:' , result); }) .catch ((err ) => { console .error ('錯誤:' , err.message ); }); } async function asyncVersion ( ) { try { console .log ('開始' ); await wait (300 ); console .log ('等待完成' ); const a = await fetchNumberAfter (200 , 10 ); const b = await fetchNumberAfter (200 , 5 ); console .log ('結果:' , a + b); } catch (err) { console .error ('錯誤:' , err.message ); } } console .log ('=== Promise 版本 ===' );promiseVersion ();console .log ('=== async/await 版本 ===' );asyncVersion ();
跟著做:體驗兩種寫法的差異 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
Promise 版本 :使用 .then()
鏈式呼叫,錯誤用 .catch()
處理
async/await 版本 :看起來像同步程式碼,錯誤用 try/catch
處理
結果相同 :兩種寫法最終都會得到相同的結果
與 Promise.all 的關係 async/await
和 Promise.all
解決的是不同的問題:
async/await :讓串行(一個接一個)的非同步程式碼看起來像同步
Promise.all :讓多個非同步任務併行(同時)執行
何時使用哪一種?
使用 async/await :當任務需要依序執行,後面的任務依賴前面的結果
使用 Promise.all :當任務可以同時執行,不需要等待彼此
結合使用 :在 async/await
函式內使用 Promise.all
來併行執行獨立任務
async-vs-promise-all.js function wait (ms, label ) { return new Promise ((resolve ) => { setTimeout (() => { console .log (`${label} 完成` ); resolve (label); }, ms); }); } async function sequential ( ) { console .log ('=== 串行執行 ===' ); const start = Date .now (); const result1 = await wait (300 , '任務 A' ); const result2 = await wait (300 , '任務 B' ); const result3 = await wait (300 , '任務 C' ); console .log ('串行完成,耗時:' , Date .now () - start, 'ms' ); return [result1, result2, result3]; } async function parallel ( ) { console .log ('=== 併行執行 ===' ); const start = Date .now (); const [result1, result2, result3] = await Promise .all ([ wait (300 , '任務 A' ), wait (300 , '任務 B' ), wait (300 , '任務 C' ) ]); console .log ('併行完成,耗時:' , Date .now () - start, 'ms' ); return [result1, result2, result3]; } sequential ().then (() => parallel ());
重要觀念
async/await
是為了使用 await
而存在的語法要求
await
只能在 async
函式內使用
await
會暫停當前函式執行,等待 Promise 完成
使用 await
時,Promise 的 .then()
和 .catch()
被隱藏了
錯誤處理使用 try/catch
,而不是 .catch()
Promise.all
仍然在 async/await
中有重要作用
AJAX 與 Fetch API AJAX 與 Fetch API 是現代網頁開發中不可或缺的技術,讓前端能夠與伺服器進行資料交換,實現動態內容更新。這一章節將帶你認識 AJAX 的基本觀念,並學會用 Fetch API 進行非同步資料請求,打造互動性更高的網頁。
什麼是 AJAX? AJAX (Asynchronous JavaScript and XML)是一種現代網頁開發技術,允許前端在不重新載入整個頁面 的情況下,向伺服器發送請求並即時取得回應。這種方式大幅提升了網頁應用的互動性與流暢度,讓使用者操作更接近桌面應用程式。
透過 AJAX 取得的資料,配合 DOM 操作 (如 innerHTML
、appendChild
、removeChild
等),可以只更新頁面的特定區塊(而非整頁重整),實現「單頁應用程式」(SPA, Single Page Application)的效果,帶來更快速、無縫的使用體驗。
AJAX 的核心概念
非同步 :請求發送後不會阻塞頁面,使用者可以繼續操作
背景通訊 :在背景與伺服器進行資料交換
資料取得 :取得伺服器回應的資料(通常是 JSON 格式)
AJAX 的演進
早期 :使用 XMLHttpRequest(XHR)物件
現代 :使用 Fetch API(基於 Promise)
未來 :可能會使用更現代的 API 如 Fetch with Streams
JSON:資料交換的標準格式 在學習 Fetch API 之前,我們需要先了解 JSON (JavaScript Object Notation),因為它是現代 API 資料交換的標準格式。JSON 是一種輕量級的資料交換格式,易於人閱讀和編寫,也易於機器解析和生成。
為什麼 API 大多使用 JSON?
輕量級 :比 XML 更簡潔,檔案大小更小
易於解析 :JavaScript 原生支援,解析速度快
跨平台 :所有程式語言都有 JSON 解析器
結構化 :支援巢狀物件和陣列,適合複雜資料
標準化 :RFC 7159 標準,確保相容性
JSON 的基本概念 JSON 是什麼? JSON 是一種基於 JavaScript 物件語法的資料格式,但它是純文字格式,不依賴於任何程式語言。這讓它成為不同系統間資料交換的理想選擇。
JSON 的資料類型:
字串 :"Hello World"
數字 :42
、3.14
布林值 :true
、false
null :null
陣列 :[1, 2, 3, "hello"]
物件 :{"name": "張三", "age": 25}
JSON 與 JavaScript 物件的轉換 在 JavaScript 中,我們經常需要在 JSON 字串和 JavaScript 物件之間進行轉換:
json-basics.js const user = { name : '張三' , age : 25 , email : 'zhang@example.com' , isActive : true , hobbies : ['閱讀' , '游泳' , '程式設計' ], address : { city : '台北市' , district : '信義區' } }; const jsonString = JSON .stringify (user);console .log ('JSON 字串:' , jsonString);const parsedUser = JSON .parse (jsonString);console .log ('解析後的物件:' , parsedUser);console .log ('使用者姓名:' , parsedUser.name );console .log ('使用者年齡:' , parsedUser.age );const prettyJson = JSON .stringify (user, null , 2 );console .log ('格式化的 JSON:' );console .log (prettyJson);
跟著做:體驗 JSON 轉換 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
物件轉 JSON :使用 JSON.stringify()
將物件轉為字串
JSON 轉物件 :使用 JSON.parse()
將字串轉為物件
格式化輸出 :使用 JSON.stringify(obj, null, 2)
產生美化的 JSON
JSON 的常見錯誤和注意事項 json-common-errors.js try { const invalidJson1 = JSON .parse ("{'name': '張三'}" ); } catch (error) { console .error ('JSON 語法錯誤 1:' , error.message ); } try { const invalidJson2 = JSON .parse ('{"name": "張三" // 這是註解}' ); } catch (error) { console .error ('JSON 語法錯誤 2:' , error.message ); } try { const invalidJson3 = JSON .parse ('{"name": "張三", "age": 25,}' ); } catch (error) { console .error ('JSON 語法錯誤 3:' , error.message ); } const validJson = JSON .parse ('{"name": "張三", "age": 25}' );console .log ('正確的 JSON:' , validJson);const specialChars = { message : '這是一個包含 "引號" 和 \n換行的訊息' , path : 'C:\\Users\\Documents\\file.txt' }; const escapedJson = JSON .stringify (specialChars);console .log ('特殊字元處理:' , escapedJson);const dataWithDate = { name : '張三' , createdAt : new Date () }; console .log ('日期物件轉 JSON:' , JSON .stringify (dataWithDate));const customDateJson = JSON .stringify (dataWithDate, (key, value ) => { if (value instanceof Date ) { return value.toISOString (); } return value; }); console .log ('自訂日期格式:' , customDateJson);
JSON 的重要限制
字串必須用雙引號 :"hello"
✅,'hello'
❌
不支援註解 :JSON 格式不允許註解
不支援尾隨逗號 :最後一個屬性後不能有逗號
不支援函數 :JSON 只能包含資料,不能包含函數
日期會轉為字串 :Date 物件會被轉為 ISO 字串格式
Fetch API:現代的 AJAX 解決方案 fetch()
是現代瀏覽器提供的 API,用於發送 HTTP 請求。它回傳一個 Promise,讓你可以用 .then()
或 async/await
來處理回應。
練習工具 為了練習 AJAX 和 API 串接,我們將使用 JSONPlaceholder ,這是一個免費的假資料 API 服務。
JSONPlaceholder 練習環境
網址 :https://jsonplaceholder.typicode.com
用途 :提供假資料來練習 API 串接
資源 :posts、comments、users、todos 等
特色 :支援 GET、POST、PUT、DELETE 等 HTTP 方法
免費 :不需要註冊或 API Key
基本語法 Fetch API 需要傳入目標網址(url)及相關選項(options),其回傳值為一個 Promise 物件,因此可透過 .then()
處理成功結果,或用 .catch()
捕捉錯誤,靈活管理非同步請求流程。
fetch (url, options?) .then (response => response.json ()) .then (data => console .log (data)) .catch (error => console .error ('Error:' , error));
為什麼需兩次 then
第一次 :fetch()
回傳的是 Response 物件,包含 HTTP 狀態、標頭等資訊,需要呼叫 .json()
來解析回應內容
第二次 :.json()
方法本身也是非同步的,回傳 Promise,所以需要再次 .then()
來取得解析後的資料
流程說明 :網路請求 → Response 物件 → JSON 解析 → 實際資料
JSON 解析的重要注意事項 當使用 Fetch API 取得資料時,大多數 API 都會回傳 JSON 格式的資料。了解如何正確處理 JSON 解析是使用 Fetch API 的關鍵。
fetch-json-handling.js async function fetchUserData ( ) { try { const response = await fetch ('https://jsonplaceholder.typicode.com/users/1' ); if (!response.ok ) { throw new Error (`HTTP ${response.status} : ${response.statusText} ` ); } const userData = await response.json (); console .log ('使用者資料:' , userData); console .log ('使用者姓名:' , userData.name ); console .log ('使用者信箱:' , userData.email ); return userData; } catch (error) { console .error ('取得資料失敗:' , error.message ); throw error; } } async function safeJsonParse (response ) { try { const contentType = response.headers .get ('content-type' ); if (contentType && contentType.includes ('application/json' )) { return await response.json (); } else { return await response.text (); } } catch (error) { console .error ('JSON 解析失敗:' , error.message ); throw new Error ('回應格式不正確' ); } } async function handleDifferentResponses ( ) { try { const jsonResponse = await fetch ('https://jsonplaceholder.typicode.com/users/1' ); const jsonData = await safeJsonParse (jsonResponse); console .log ('JSON 資料:' , jsonData); const textResponse = await fetch ('https://httpbin.org/plain' ); const textData = await safeJsonParse (textResponse); console .log ('文字資料:' , textData); } catch (error) { console .error ('處理回應失敗:' , error.message ); } } async function fetchWithErrorHandling ( ) { try { const response = await fetch ('https://jsonplaceholder.typicode.com/users/999' ); if (!response.ok ) { let errorMessage = `HTTP ${response.status} : ${response.statusText} ` ; try { const errorData = await response.json (); errorMessage = errorData.message || errorMessage; } catch { } throw new Error (errorMessage); } const data = await response.json (); return data; } catch (error) { console .error ('請求失敗:' , error.message ); throw error; } } async function fetchMultipleUsers ( ) { try { const userIds = [1 , 2 , 3 ]; const promises = userIds.map (id => fetch (`https://jsonplaceholder.typicode.com/users/${id} ` ) .then (response => response.json ()) ); const users = await Promise .all (promises); console .log ('多個使用者資料:' , users); users.forEach ((user, index ) => { console .log (`使用者 ${index + 1 } : ${user.name} (${user.email} )` ); }); return users; } catch (error) { console .error ('批次請求失敗:' , error.message ); throw error; } } fetchUserData ();handleDifferentResponses ();fetchWithErrorHandling ();fetchMultipleUsers ();
JSON 解析的常見陷阱
檢查回應狀態 :在解析 JSON 前先檢查 response.ok
Content-Type 檢查 :確認回應確實是 JSON 格式
錯誤處理 :使用 try-catch 包裝 JSON 解析
非同步處理 :response.json()
回傳 Promise,需要使用 await
空回應處理 :某些 API 可能回傳空字串或 null
最佳實踐
統一錯誤處理 :建立通用的 JSON 解析函數
類型檢查 :確認解析後的資料結構符合預期
預設值 :為可能為空的欄位提供預設值
驗證資料 :檢查必要欄位是否存在
你也可以利用 async/await 語法來更直覺地處理 fetch 非同步請求,讓程式碼更易讀且結構更清晰。
async function fetchData ( ) { try { const response = await fetch (url, options?); const data = await response.json (); console .log (data); } catch (error) { console .error ('Error:' , error); } }
兩種寫法的選擇
Promise.then :適合簡單的單次請求
async/await :適合複雜的邏輯和多個請求,可讀性更高
本課程後續範例 :將優先使用 async/await 寫法
options 參數說明 在進行 API 連線時,整個流程可分為兩大部分:
Request(請求) :由前端發送給伺服器的資料與設定。這些設定主要透過 options
參數來自訂,包括:
method
(HTTP 方法):如 GET、POST、PUT、DELETE 等
headers
(請求標頭):設定資料格式、認證等資訊
body
(請求內容):傳送資料內容(通常用於 POST、PUT、PATCH)
其他選項如 mode
(跨域模式)、credentials
(是否帶 cookies)、cache
(快取策略)等,可依需求靈活調整
Response(回應) :伺服器處理請求後回傳給前端的資料內容
options
參數用來細緻控制 Request(請求)的各種屬性,讓你能根據實際需求調整 API 請求的行為,以下是 options 詳細說明:
預設選項與可選值
範例
說明
method: 'GET'
(GET, POST, PUT, DELETE, PATCH)
method: ‘POST’
HTTP 方法
headers: {}
(物件)
headers: { ‘Content-Type’: ‘application/json’ }
請求標頭物件(HTTP 標頭資訊),傳遞額外資訊給伺服器
body: undefined
(字串)
body: JSON.stringify(data)
請求內容(POST、PUT、PATCH 時使用)
mode: 'cors'
(cors, no-cors, same-origin)
mode: ‘cors’
請求模式(跨域處理),處理跨域請求問題
credentials: 'omit'
(omit, same-origin, include)
credentials: ‘include’
控制是否發送 cookies(影響認證)
cache: 'default'
(default, no-cache, reload, force-cache)
cache: ‘reload’
控制瀏覽器快取策略(影響效能)
redirect: 'follow'
(follow, error, manual)
redirect: ‘follow’
重導向處理
referrer: 'client'
(no-referrer, client, origin)
referrer: ‘origin’
來源頁面設定
signal: undefined
(AbortController 實例)
signal: controller.signal
中止控制器(請求取消功能)
headers 用來設定 HTTP 請求標頭,常見的標頭包括:
Content-Type(內容類型標頭) 用來指定 HTTP 請求內容的資料格式,讓伺服器正確解析傳送過去的資料。這個標頭在 POST、PUT、PATCH 等需要傳送資料的請求中特別重要。
application/json
:表示傳送的是 JSON 格式資料,常用於 API 溝通。前端需搭配 JSON.stringify()
將物件轉為字串。
application/x-www-form-urlencoded
:表示資料以表單格式(key=value&key2=value2)傳送,常見於傳統 HTML 表單提交。
multipart/form-data
:用於檔案上傳,可以同時傳送文字與檔案內容。需搭配 FormData
物件使用,瀏覽器會自動處理邊界(boundary)設定。
小技巧:Content-Type 設定時機
若使用 FormData
物件,通常不需要手動設定 Content-Type
,瀏覽器會自動補上正確的 multipart/form-data 格式(包含 boundary 參數)。
若傳送 JSON,務必設定 Content-Type: application/json
,否則伺服器可能無法正確解析資料。
Authorization(認證標頭) 用於傳遞用戶身份驗證資訊給伺服器,確保 API 請求的安全性。常見用法如下:
Bearer <token>
:採用 Bearer Token(通常為 JWT,JSON Web Token),用於 OAuth2 或現代 API 認證。前端需將伺服器發給的 token 夾帶在此欄位,格式如 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
。
Basic <credentials>
:基本認證,將帳號密碼以 帳號:密碼
方式經 base64 編碼後傳送。例如:Authorization: Basic dXNlcjpwYXNzd29yZA==
。此方式多用於內部系統或簡單驗證,安全性較低。
注意事項
Bearer Token 請妥善保管,避免外洩,建議搭配 HTTPS 傳輸。
Basic 認證不建議用於公開網路,除非有加密保護。
Custom Headers(自訂標頭) 是指除了標準 HTTP 標頭外,開發者可根據需求額外加入的欄位,通常以 X-
開頭,讓前後端能傳遞特殊資訊。這些標頭可用於追蹤、驗證、版本控制等多種情境。
X-Custom-Header
:自訂標頭(通常以 X- 開頭),可用來傳遞專案自定義的資訊,例如追蹤 ID、API 版本、特殊驗證碼等。伺服器端需自行解析這些欄位。
User-Agent
:瀏覽器或用戶端軟體的資訊,包含作業系統、瀏覽器名稱與版本等。伺服器可根據此資訊調整回應內容,或進行裝置相容性判斷。
Accept
:用來告訴伺服器前端可接受的回應格式(如 application/json
、text/html
等),伺服器可依此決定回傳資料的格式,提升 API 彈性與相容性。
小技巧:自訂標頭應用情境
可用於 API 版本控管(如 X-API-Version: 2
)
傳遞追蹤資訊(如 X-Request-ID
方便日誌追蹤)
前端與後端協議特殊驗證機制
method 方法:GET 與 POST HTTP 方法是與伺服器溝通的基本方式,不同的方法代表不同的操作意圖。在實際開發中,最常用的是 GET 和 POST 兩種方法。
GET 請求:讀取資料 GET 請求用於從伺服器取得資料,通常不會改變伺服器狀態。這是最常見的請求類型,用於查詢資料、取得頁面內容等。
GET 請求特點
用途 :讀取資料,不改變伺服器狀態
資料傳遞 :透過 URL 參數傳遞
安全性 :資料會顯示在 URL 中,不適合傳送敏感資訊
快取 :可以被瀏覽器快取
長度限制 :URL 長度有限制
fetch-get.js async function getTodo (id ) { try { const response = await fetch (`https://jsonplaceholder.typicode.com/todos/${id} ` ); if (!response.ok ) { throw new Error (`HTTP ${response.status} : ${response.statusText} ` ); } const todo = await response.json (); return todo; } catch (error) { console .error ('取得資料失敗:' , error.message ); throw error; } } getTodo (1 ) .then (todo => console .log ('Todo:' , todo)) .catch (error => console .error ('錯誤:' , error)); async function searchTodos (userId, completed ) { try { const params = new URLSearchParams ({ userId : userId, completed : completed }); const response = await fetch (`https://jsonplaceholder.typicode.com/todos?${params} ` ); if (!response.ok ) { throw new Error (`HTTP ${response.status} : ${response.statusText} ` ); } const todos = await response.json (); return todos; } catch (error) { console .error ('搜尋失敗:' , error.message ); throw error; } } searchTodos (1 , true ) .then (todos => console .log ('已完成的任務:' , todos)) .catch (error => console .error ('錯誤:' , error));
POST 請求:建立資料 POST 請求用於向伺服器發送資料,通常會建立新的資源。這是用於提交表單、上傳檔案、建立新記錄等操作的主要方法。
POST 請求特點
用途 :建立新資料,可能改變伺服器狀態
資料傳遞 :透過請求主體(body)傳遞
安全性 :資料不會顯示在 URL 中,較安全
快取 :通常不會被快取
長度限制 :沒有 URL 長度限制
fetch-post.js async function createPost (postData ) { try { const response = await fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify (postData) }); if (!response.ok ) { throw new Error (`HTTP ${response.status} : ${response.statusText} ` ); } const newPost = await response.json (); return newPost; } catch (error) { console .error ('建立資料失敗:' , error.message ); throw error; } } const newPost = { title : '我的新文章' , body : '這是文章內容' , userId : 1 }; createPost (newPost) .then (post => console .log ('新文章:' , post)) .catch (error => console .error ('錯誤:' , error)); async function submitForm (formData ) { try { const response = await fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , }, body : new URLSearchParams (formData) }); if (!response.ok ) { throw new Error (`HTTP ${response.status} : ${response.statusText} ` ); } const result = await response.json (); return result; } catch (error) { console .error ('表單提交失敗:' , error.message ); throw error; } } const formData = { title : '表單標題' , body : '表單內容' , userId : 1 }; submitForm (formData) .then (result => console .log ('表單提交成功:' , result)) .catch (error => console .error ('錯誤:' , error));
跟著做:體驗 GET 與 POST 請求 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
GET 請求 :取得單一資料和查詢多筆資料
POST 請求 :建立新資料和提交表單
錯誤處理 :統一的錯誤處理方式
參數傳遞 :URL 參數 vs 請求主體的差異
實際應用場景
GET :取得文章列表、搜尋功能、取得使用者資料
POST :發表文章、註冊帳號、上傳檔案、提交表單
重要觀念
GET vs POST :GET 用於讀取,POST 用於建立
安全性 :敏感資料使用 POST,避免在 URL 中暴露
快取 :GET 請求可以被快取,POST 通常不會
冪等性 :GET 是冪等的(多次請求結果相同),POST 不是
操作範例 在實際開發中,HTTP 標頭(headers)不僅用於基本的資料傳遞,還能靈活應用於多種場景,例如 API 版本管理、請求除錯追蹤、A/B 測試、使用者行為分析與安全驗證等。透過自訂標頭,前後端可以協議更多元的資訊,讓 API 互動更具彈性與可擴充性。
常見的傳遞與接收 示範如何使用 fetch 搭配完整的 options 物件,向 JSONPlaceholder API 發送 POST 請求,並自訂 HTTP 標頭與傳送 JSON 格式資料
fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Custom-Header' : 'my-value' }, body : JSON .stringify ({ title : '我的新文章' , body : '這是文章內容' , userId : 1 }), cache : 'no-cache' , }) .then (response => { console .log ('Status:' , response.status ); console .log ('Headers:' , response.headers ); return response.json (); }) .then (data => console .log ('新建立的文章:' , data));
JSON 資料傳送 最常見的 API 資料格式,用於傳送結構化的資料。
fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Request-Type' : 'json-data' }, body : JSON .stringify ({ title : '我的文章標題' , body : '這是文章的內容' , userId : 1 }) }) .then (response => response.json ()) .then (data => { console .log ('JSON 資料傳送成功:' , data); }) .catch (error => { console .error ('JSON 資料傳送失敗:' , error); });
JSON 資料傳送要點
使用 Content-Type: application/json
使用 JSON.stringify()
將物件轉為字串
適合傳送複雜的結構化資料
伺服器會自動解析 JSON 格式
表單資料傳送 傳統的 HTML 表單提交格式,用於簡單的鍵值對資料。
fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , 'X-Request-Type' : 'form-data' }, body : 'title=表單標題&body=表單內容&userId=1' }) .then (response => response.json ()) .then (data => { console .log ('表單資料傳送成功:' , data); }) .catch (error => { console .error ('表單資料傳送失敗:' , error); }); function createFormData (data ) { const params = new URLSearchParams (); for (const key in data) { params.append (key, data[key]); } return params.toString (); } const formData = createFormData ({ title : '動態標題' , body : '動態內容' , userId : 1 }); fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' , 'X-Request-Type' : 'dynamic-form' }, body : formData }) .then (response => response.json ()) .then (data => { console .log ('動態表單資料傳送成功:' , data); });
表單資料傳送要點
使用 Content-Type: application/x-www-form-urlencoded
資料格式為 key1=value1&key2=value2
適合簡單的鍵值對資料
可以使用 URLSearchParams
動態建立
檔案上傳 用於上傳檔案到伺服器,支援多種檔案類型。
function uploadFile (file ) { const formData = new FormData (); formData.append ('file' , file); formData.append ('title' , '檔案上傳測試' ); formData.append ('description' , '這是一個測試檔案' ); return fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'X-Request-Type' : 'file-upload' , 'X-File-Name' : file.name , 'X-File-Size' : file.size }, body : formData }); } const mockFile = new File (['檔案內容' ], 'test.txt' , { type : 'text/plain' });uploadFile (mockFile) .then (response => response.json ()) .then (data => { console .log ('檔案上傳成功:' , data); }) .catch (error => { console .error ('檔案上傳失敗:' , error); });
檔案上傳注意事項
不要手動設定 Content-Type
,讓瀏覽器自動設定
瀏覽器會自動加入 boundary
參數
使用 FormData
物件來包裝檔案和額外資料
可以加入自訂標頭來傳遞檔案相關資訊
多部分資料傳送 同時傳送文字資料和檔案,適合複雜的表單提交。
function uploadWithData (file, userData ) { const formData = new FormData (); formData.append ('avatar' , file); formData.append ('name' , userData.name ); formData.append ('email' , userData.email ); formData.append ('bio' , userData.bio ); return fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'X-Request-Type' : 'multipart-data' , 'X-User-ID' : userData.id }, body : formData }); } const mockFile = new File (['頭像圖片' ], 'avatar.jpg' , { type : 'image/jpeg' });const userData = { id : 123 , name : '張三' , email : 'zhang@example.com' , bio : '這是一個測試用戶' }; uploadWithData (mockFile, userData) .then (response => response.json ()) .then (data => { console .log ('多部分資料傳送成功:' , data); }) .catch (error => { console .error ('多部分資料傳送失敗:' , error); });
跟著做:體驗不同內容類型 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
JSON 資料 :傳送結構化資料
表單資料 :傳送簡單鍵值對
檔案上傳 :上傳檔案到伺服器
多部分資料 :同時傳送檔案和文字
實際應用場景
JSON :API 資料交換、複雜物件傳送
表單資料 :簡單表單提交、URL 參數
檔案上傳 :圖片上傳、文件上傳
多部分資料 :用戶資料表單、產品資訊提交
HTTP 標頭是前端與伺服器溝通的重要橋樑,除了標準的標頭外,我們還可以傳遞自訂標頭來實現特定的功能需求。自訂標頭讓前後端能夠傳遞額外的資訊,實現更靈活和強大的 API 互動。
HTTP 標頭包含了兩大類:
標準標頭 :如 Content-Type
、Authorization
、User-Agent
等
自訂標頭 :通常以 X-
開頭,如 X-Custom-Header
、X-API-Version
等
自訂標頭的常見用途:
API 版本控制 :指定使用哪個版本的 API
請求追蹤 :為每個請求分配唯一 ID,便於除錯
特殊驗證 :傳遞額外的認證或驗證資訊
功能開關 :控制伺服器的特定功能
統計分析 :傳遞使用者行為或統計資訊
自訂標頭命名與注意事項
建議以 X-
開頭(如 X-API-Version
),並用連字號分隔單字
標頭名稱區分大小寫,避免使用特殊字元,保持簡潔
不要在標頭中傳遞敏感資訊,並避免標頭值過長
確認伺服器支援自訂標頭,部分標頭需 CORS 設定允許
基本用法 以下範例展示如何在 fetch 請求中加入自訂 HTTP 標頭(Custom Headers)。自訂標頭常用於 API 版本控管、請求追蹤、或傳遞專案特定資訊。只需在 headers 物件中加入自訂欄位(通常以 X-
開頭),即可讓前後端溝通更靈活。
fetch ('https://jsonplaceholder.typicode.com/posts' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-Custom-Header' : 'custom-value' , 'X-API-Version' : '2' , 'X-Request-ID' : 'req-12345' }, body : JSON .stringify ({ title : '測試文章' , body : '這是測試內容' , userId : 1 }) }) .then (response => response.json ()) .then (data => { console .log ('請求成功:' , data); }) .catch (error => { console .error ('請求失敗:' , error); });
API 版本控制 這個範例示範如何用自訂標頭 X-API-Version
來指定要呼叫的 API 版本,讓前端可以根據需求切換不同版本的 API。
function createAPIClient (version = 'v1' ) { const baseURL = 'https://api.example.com' ; async function request (endpoint, options = {} ) { const url = `${baseURL} /${endpoint} ` ; const headers = { 'Content-Type' : 'application/json' , 'X-API-Version' : version, 'X-Client-Version' : '1.0.0' , ...options.headers }; return fetch (url, { ...options, headers }); } return { version : version, request : request, async getUsers ( ) { const response = await request ('users' ); return response.json (); }, async getPosts ( ) { const response = await request ('posts' ); return response.json (); } }; } const apiV1 = createAPIClient ('v1' );const apiV2 = createAPIClient ('v2' );apiV1.getUsers ().then (users => console .log ('V1 格式:' , users)); apiV2.getUsers ().then (users => console .log ('V2 格式:' , users));
請求追蹤與除錯 這個範例示範如何在每一次發送 fetch 請求時,自動產生唯一的請求 ID,並將其加入自訂 HTTP 標頭,方便後端進行請求追蹤與除錯。
function createRequestTracker ( ) { let requestCount = 0 ; function generateRequestId ( ) { requestCount++; const timestamp = Date .now (); const random = Math .random ().toString (36 ).substr (2 , 9 ); return `req-${timestamp} -${requestCount} -${random} ` ; } async function trackedRequest (url, options = {} ) { const requestId = generateRequestId (); console .log (`開始請求 ${requestId} :${url} ` ); const headers = { 'X-Request-ID' : requestId, 'X-Request-Timestamp' : Date .now (), 'X-Client-IP' : '192.168.1.1' , ...options.headers }; const startTime = Date .now (); try { const response = await fetch (url, { ...options, headers }); const endTime = Date .now (); const duration = endTime - startTime; console .log (`請求 ${requestId} 完成,耗時:${duration} ms` ); return response; } catch (error) { console .error (`請求 ${requestId} 失敗:` , error); throw error; } } return { trackedRequest : trackedRequest, getRequestCount : () => requestCount }; } const tracker = createRequestTracker ();tracker.trackedRequest ('https://jsonplaceholder.typicode.com/posts/1' ) .then (response => response.json ()) .then (data => console .log ('資料:' , data));
功能開關與實驗性功能 示範如何用「功能開關」(Feature Toggle) 控制伺服器端的特定功能開啟或關閉,並將狀態透過自訂 HTTP 標頭傳遞給後端,方便進行 A/B 測試或實驗性功能管理。
function createFeatureToggle ( ) { const features = { newUI : true , betaFeatures : false , analytics : true }; async function requestWithFeatures (url, options = {} ) { const headers = { 'X-Feature-NewUI' : features.newUI ? 'enabled' : 'disabled' , 'X-Feature-Beta' : features.betaFeatures ? 'enabled' : 'disabled' , 'X-Feature-Analytics' : features.analytics ? 'enabled' : 'disabled' , ...options.headers }; return fetch (url, { ...options, headers }); } function toggleFeature (featureName ) { if (features.hasOwnProperty (featureName)) { features[featureName] = !features[featureName]; console .log (`${featureName} 已切換為:${features[featureName]} ` ); } } function getFeatureStatus (featureName ) { return features[featureName] || false ; } return { requestWithFeatures : requestWithFeatures, toggleFeature : toggleFeature, getFeatureStatus : getFeatureStatus }; } const featureToggle = createFeatureToggle ();featureToggle.requestWithFeatures ('https://jsonplaceholder.typicode.com/posts' ) .then (response => response.json ()) .then (data => { console .log ('使用功能開關的資料:' , data); }); featureToggle.toggleFeature ('newUI' ); console .log ('新 UI 狀態:' , featureToggle.getFeatureStatus ('newUI' ));
統計分析與使用者行為追蹤 示範如何透過自訂 HTTP 標頭,將使用者行為(如瀏覽頁面、點擊按鈕等)資訊傳遞到後端伺服器,達到統計分析與行為追蹤的目的。
function createAnalyticsTracker ( ) { const sessionId = 'session-' + Date .now () + '-' + Math .random ().toString (36 ).substr (2 , 9 ); const userAgent = navigator.userAgent ; const screenResolution = `${screen.width} x${screen.height} ` ; async function trackEvent (eventName, eventData = {} ) { const headers = { 'X-Session-ID' : sessionId, 'X-User-Agent' : userAgent, 'X-Screen-Resolution' : screenResolution, 'X-Event-Name' : eventName, 'X-Event-Timestamp' : Date .now (), 'X-Referrer' : document .referrer || 'direct' , ...eventData }; return fetch ('https://api.example.com/analytics' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , ...headers }, body : JSON .stringify ({ event : eventName, data : eventData, timestamp : Date .now () }) }); } return { trackEvent : trackEvent, sessionId : sessionId }; } const analytics = createAnalyticsTracker ();analytics.trackEvent ('page_view' , { 'X-Page-URL' : window .location .href , 'X-Page-Title' : document .title }); analytics.trackEvent ('button_click' , { 'X-Button-ID' : 'search-button' , 'X-Button-Text' : '搜尋' }); console .log ('追蹤器已建立,Session ID:' , analytics.sessionId );
動態標頭管理 這個範例展示如何建立一個「動態標頭管理系統」,可以靈活設定、移除或取得 HTTP 請求的自訂標頭,並透過統一的 request 方法發送帶有這些標頭的 fetch 請求,方便管理 API 請求時的標頭資訊。
function createHeaderManager ( ) { const defaultHeaders = { 'Content-Type' : 'application/json' , 'X-Client-Type' : 'web' , 'X-Client-Version' : '1.0.0' }; const dynamicHeaders = {}; function setDynamicHeader (name, value ) { dynamicHeaders[name] = value; } function removeDynamicHeader (name ) { delete dynamicHeaders[name]; } function getAllHeaders ( ) { return { ...defaultHeaders, ...dynamicHeaders }; } async function request (url, options = {} ) { const headers = { ...getAllHeaders (), ...options.headers }; return fetch (url, { ...options, headers }); } return { setDynamicHeader : setDynamicHeader, removeDynamicHeader : removeDynamicHeader, getAllHeaders : getAllHeaders, request : request }; } const headerManager = createHeaderManager ();headerManager.setDynamicHeader ('X-User-ID' , 'user123' ); headerManager.setDynamicHeader ('X-Session-Token' , 'token456' ); headerManager.request ('https://jsonplaceholder.typicode.com/posts' ) .then (response => response.json ()) .then (data => console .log ('資料:' , data)); setTimeout (() => { headerManager.setDynamicHeader ('X-User-ID' , 'user789' ); console .log ('標頭已更新' ); console .log ('目前所有標頭:' , headerManager.getAllHeaders ()); }, 2000 );
包含 cookies 的請求 在實際開發中,若需要讓 API 請求自動攜帶使用者的登入狀態、購物車資訊等 cookies,必須正確設定 fetch 的 credentials
參數。這通常用於「已登入的使用者取得個人資料」或「查詢購物車內容」等情境。請注意,cookies 具有網域限制,僅會傳送給設定它的網域,跨網域請求時需特別留意。
前端(瀏覽器)如何包含 cookies:
當設定 credentials: 'include'
時,瀏覽器會自動將該網域的 cookies 加入 HTTP 請求標頭
格式:Cookie: name1=value1; name2=value2; name3=value3
瀏覽器會根據網域、路徑、過期時間等規則決定要傳送哪些 cookies
如何在 Network 面板中查看 cookies:
開啟瀏覽器開發者工具(F12)
切換到 Network 標籤
執行上述程式碼
在 Network 面板中找到對應的請求
點擊請求,查看 Request Headers 區塊
尋找 Cookie
欄位,會看到發送的 cookies 內容
為什麼看不到 Cookie 標頭?
網域限制 :cookies 只能傳送給設定它的網域
跨域請求 :向 jsonplaceholder.typicode.com
發送請求時,瀏覽器不會傳送當前網域的 cookies
實際應用 :在真實應用中,cookies 通常由伺服器設定,用於同域請求
document .cookie = 'sessionId=abc123; path=/' ;document .cookie = 'userId=456; path=/' ;document .cookie = 'cart=item1,item2; path=/' ;console .log ('目前瀏覽器的 cookies:' , document .cookie );console .log ('注意:這些 cookies 只會傳送給當前網域' );fetch ('/api/test' , { credentials : 'include' }) .then (response => { console .log ('向當前網域發送請求,cookies 會被包含' ); console .log ('在 Network 面板中會看到 Cookie 標頭' ); return response.text (); }) .catch (error => { console .log ('這個請求會失敗(因為沒有 /api/test 端點),但可以看到 cookies 被發送' ); console .log ('錯誤:' , error.message ); });
後端(伺服器)如何提取 cookies:
app.get ('/api/users' , (req, res ) => { const cookies = req.headers .cookie ; console .log ('收到的 cookies:' , cookies); const sessionId = req.cookies .sessionId ; const userId = req.cookies .userId ; const cart = req.cookies .cart ; console .log ('使用者 ID:' , userId); console .log ('購物車:' , cart); res.json ({ message : '已收到您的 cookies' }); });
實際流程:
瀏覽器發送請求時自動包含 cookies
伺服器從 Cookie
標頭中讀取 cookies
伺服器根據 cookies 內容決定回傳什麼資料
瀏覽器快取機制與控制 在網頁開發中,瀏覽器會自動快取 HTTP 請求的結果以提升效能。然而,當需要確保取得最新資料時,我們需要了解快取機制並學會控制它。
快取機制的基本原理
瀏覽器快取是由伺服器回應的 HTTP 標頭 和瀏覽器的快取策略 共同決定的:
伺服器控制快取的 HTTP 標頭:
Cache-Control :最重要的快取控制標頭
max-age=3600
:快取 1 小時
no-cache
:每次都要驗證
no-store
:完全不快取
Expires :指定快取過期時間
ETag :資源版本標識,用於驗證快取是否有效
Last-Modified :資源最後修改時間
瀏覽器快取決策流程:
檢查快取是否存在
檢查快取是否過期(根據伺服器標頭)
發送條件請求(如果需要驗證)
根據伺服器回應決定是否使用快取
伺服器端設定範例
app.get ('/api/data' , (req, res ) => { res.set ('Cache-Control' , 'max-age=3600' ); res.set ('ETag' , 'abc123' ); res.json ({ data : 'some data' }); }); app.get ('/api/important-data' , (req, res ) => { res.set ('Cache-Control' , 'no-cache' ); res.json ({ data : 'important data' }); });
fetch 的 cache 選項
fetch 的 cache
參數讓前端可以覆蓋伺服器的快取設定,主動控制資料取得方式:
選項
行為
適用場景
default
遵循伺服器快取標頭
一般資料,平衡效能與即時性
no-cache
每次驗證快取有效性
重要資料,確保正確性
reload
強制重新載入,忽略快取
即時資料(股價、通知)
force-cache
強制使用快取
靜態資源(圖片、CSS)
實際操作範例
fetch ('https://jsonplaceholder.typicode.com/posts/1' , { cache : 'reload' }) .then (response => { console .log ('強制重新載入:確保取得最新資料' ); return response.json (); }) .then (data => { console .log ('最新資料:' , data); }); fetch ('https://jsonplaceholder.typicode.com/posts/1' , { cache : 'default' }) .then (response => { console .log ('使用預設快取:可能使用快取資料' ); console .log ('Response Headers:' , response.headers ); return response.json (); }) .then (data => { console .log ('資料:' , data); }); console .log ('=== 在 Network 面板中觀察 ===' );console .log ('第一次請求:Status 200 (從伺服器取得)' );console .log ('第二次請求:Status 200 (from cache) 或 Status 304 (Not Modified)' );
重要觀念
伺服器主導 :快取行為主要由伺服器的 HTTP 標頭決定
前端覆蓋 :fetch 的 cache 選項可以覆蓋伺服器設定
實際觀察 :在 Network 面板中可以看到快取的實際行為
開發建議 :使用 reload
確保取得最新資料
生產建議 :謹慎使用,避免不必要的請求
可取消的請求(AbortController) 在實際應用中,使用者可能會在請求完成前切換頁面、取消操作或開始新的請求。如果不及時取消正在進行的請求,可能會造成資源浪費、資料競爭或錯誤的 UI 更新。AbortController
提供了一個優雅的解決方案來取消 fetch 請求。
AbortController 的核心概念
AbortController :用於建立可以取消的請求
AbortSignal :傳遞給 fetch 的信號,用於監聽取消事件
abort() :觸發取消事件的方法
常見應用場景
搜尋功能 :使用者快速輸入時,取消之前的搜尋請求
頁面切換 :離開頁面時取消未完成的請求
檔案上傳 :使用者取消上傳時停止請求
競速請求 :只保留最後一個請求,取消之前的
為什麼需要取消請求?
效能優化 :避免不必要的網路流量和伺服器負載
資料一致性 :防止舊請求的結果覆蓋新請求的結果
使用者體驗 :避免頁面切換後仍顯示舊資料
資源管理 :及時釋放網路連接和記憶體資源
基本用法
const controller = new AbortController ();fetch ('https://jsonplaceholder.typicode.com/posts' , { signal : controller.signal }) .then (response => response.json ()) .then (data => { console .log ('請求完成:' , data); }) .catch (error => { if (error.name === 'AbortError' ) { console .log ('請求被取消' ); } else { console .error ('請求失敗:' , error); } }); controller.abort ();
實際應用:可取消的搜尋功能
let searchController = null ;function startSearch (keyword ) { if (searchController) { searchController.abort (); console .log ('取消之前的搜尋請求' ); } searchController = new AbortController (); console .log (`開始搜尋:${keyword} ` ); fetch (`https://jsonplaceholder.typicode.com/posts?q=${keyword} ` , { signal : searchController.signal }) .then (response => response.json ()) .then (data => { console .log (`搜尋結果:找到 ${data.length} 筆資料` ); updateSearchResults (data); }) .catch (error => { if (error.name === 'AbortError' ) { console .log ('搜尋被取消' ); } else { console .error ('搜尋失敗:' , error); showErrorMessage (error.message ); } }); } function updateSearchResults (data ) { console .log ('更新搜尋結果到 UI' ); } function showErrorMessage (message ) { console .log ('顯示錯誤訊息:' , message); } console .log ('=== 搜尋功能測試 ===' );startSearch ('hello' ); setTimeout (() => startSearch ('world' ), 1000 );
進階用法:超時控制
function fetchWithTimeout (url, timeoutMs = 5000 ) { const controller = new AbortController (); const timeoutId = setTimeout (() => { controller.abort (); }, timeoutMs); return fetch (url, { signal : controller.signal }) .then (response => { clearTimeout (timeoutId); return response.json (); }) .catch (error => { clearTimeout (timeoutId); if (error.name === 'AbortError' ) { throw new Error ('請求超時' ); } throw error; }); } fetchWithTimeout ('https://jsonplaceholder.typicode.com/posts' , 3000 ) .then (data => console .log ('成功取得資料:' , data)) .catch (error => console .error ('錯誤:' , error.message ));
多個請求的統一管理
class RequestManager { constructor ( ) { this .controllers = new Map (); } startRequest (requestId, url ) { this .cancelRequest (requestId); const controller = new AbortController (); this .controllers .set (requestId, controller); return fetch (url, { signal : controller.signal }) .then (response => response.json ()) .finally (() => { this .controllers .delete (requestId); }); } cancelRequest (requestId ) { const controller = this .controllers .get (requestId); if (controller) { controller.abort (); this .controllers .delete (requestId); console .log (`請求 ${requestId} 已取消` ); } } cancelAll ( ) { this .controllers .forEach ((controller, requestId ) => { controller.abort (); console .log (`請求 ${requestId} 已取消` ); }); this .controllers .clear (); } } const requestManager = new RequestManager ();requestManager.startRequest ('search' , 'https://jsonplaceholder.typicode.com/posts' ) .then (data => console .log ('搜尋完成:' , data)); requestManager.startRequest ('user' , 'https://jsonplaceholder.typicode.com/users/1' ) .then (data => console .log ('使用者資料:' , data)); setTimeout (() => { requestManager.cancelRequest ('search' ); }, 1000 ); setTimeout (() => { requestManager.cancelAll (); }, 2000 );
跟著做:體驗可取消請求 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
基本用法 :如何建立和取消請求
搜尋功能 :快速輸入時自動取消舊請求
超時控制 :結合 AbortController 實現請求超時
統一管理 :管理多個可取消的請求
實際應用場景
搜尋框 :使用者輸入時取消之前的搜尋
檔案上傳 :提供取消上傳按鈕
頁面切換 :離開頁面時取消未完成的請求
競速請求 :只保留最後一個請求的結果
注意事項
錯誤處理 :被取消的請求會拋出 AbortError
,需要特別處理
資源清理 :記得清理 timeout 和 controller 引用
瀏覽器支援 :AbortController 在現代瀏覽器中支援良好
替代方案 :舊版瀏覽器可能需要使用 XMLHttpRequest 的 abort() 方法
RESTful API 設計原則 REST (Representational State Transfer)是一種 API 設計風格,讓 API 更容易理解和使用。它是由 Roy Fielding 在 2000 年提出的博士論文中的概念,後來成為現代 Web API 設計的重要參考標準。
為什麼要學 RESTful API?
在實際開發中,我們經常需要設計 API 來讓前端和後端溝通。RESTful API 提供了一套標準化的設計方式,讓我們的 API 更容易理解、維護和擴展。
RESTful API 的實際用途
統一標準 :讓團隊成員都能快速理解 API 的用途
提高效率 :標準化的設計讓開發速度更快
易於維護 :一致的設計模式讓程式碼更容易維護
便於擴展 :良好的設計讓系統更容易擴展新功能
傳統方式:僅使用 GET 和 POST 在 RESTful API 出現之前,過去 Web 開發主要依賴兩種 HTTP 方法:GET (拿取資料)和 POST (寫入資料)。這種方式會產生一些不容易理解的問題:
語義不明確、操作難以理解 :無法直接 URL 反映操作意圖,例如 /updateUser
、/deleteUser
,看不出是對哪筆資源進行 CRUD(新增、查詢、修改、刪除)動作。
HTTP 方法誤用 :所有寫入、更新、刪除等操作都用 POST,導致程式碼可讀性與維護性降低。
URL 與端點設計混亂 :同一類型 user 資源的 CRUD 操作需定義多個不同端點,缺乏一致性且難以記憶。
資料結構與回應格式不統一 :每個 API 回傳的資料格式、欄位名稱都可能不同,前後端整合時需針對每個端點額外處理。
維護困難、成本高 :每個操作都需獨立維護端點與程式碼,當需求變動時需同時修改多個地方。
Request
URL : /getUser?id=1
Method : GET
Parameters :
id
(number, required) - 使用者 ID
Request Body
Response
{ "id" : 1 , "name" : "Leanne Graham" , "username" : "Bret" , "email" : "Sincere@april.biz" , "address" : { "street" : "Kulas Light" , "suite" : "Apt. 556" , "city" : "Gwenborough" , "zipcode" : "92998-3874" } , "phone" : "1-770-736-8031 x56442" , "website" : "hildegard.org" }
Request
URL : /createUser
Method : POST
Parameters : 無
Request Body
{ "name" : "Leanne Graham" , "username" : "Bret" , "email" : "Sincere@april.biz" , "address" : { "street" : "Kulas Light" , "suite" : "Apt. 556" , "city" : "Gwenborough" , "zipcode" : "92998-3874" } , "phone" : "1-770-736-8031 x56442" , "website" : "hildegard.org" }
Response
{ "success" : true , "message" : "使用者資料已新增" , }
Request
URL : /updateUser
Method : POST
Parameters : 無
Request Body
{ "id" : 1 , "name" : "Leanne Graham (已更新)" , "username" : "Bret" , "email" : "Sincere.updated@april.biz" , "address" : { "street" : "Kulas Light" , "suite" : "Apt. 556" , "city" : "Gwenborough" , "zipcode" : "92998-3874" } , "phone" : "1-770-736-8031 x56442" , "website" : "hildegard.org" }
Response
{ "success" : true , "message" : "使用者資料已更新" , }
Request
URL : /deleteUser
Method : POST
Parameters : 無
Request Body
Response
{ "success" : true , "message" : "使用者資料已刪除" , }
RESTful API 的核心概念 RESTful API(表述性狀態轉移)設計風格,會根據不同操作目的,選用合適的 HTTP 方法(如 GET、POST、PUT、PATCH、DELETE)來清楚表達對資源的操作語意。這樣設計的好處是:看到 URL 和 HTTP 方法,就知道這個 API 要做什麼。
HTTP 方法
用途
實際例子
GET
讀取資料
GET /users
- 取得所有使用者列表
GET
讀取資料
GET /users/1
- 取得使用者 id 為 1 的資料
POST
建立資料
POST /users
- 建立新使用者
PUT
完整更新
PUT /users/1
- 更新使用者 id 為 1 的所有資料
PATCH
部分更新
PATCH /users/1
- 只更新使用者局部資料
DELETE
刪除資料
DELETE /users/1
- 刪除使用者 id 為 1 的資料
語義清晰度 :RESTful 方式從 URL 就能看出操作意圖
HTTP 方法使用 :RESTful 方式正確使用各種 HTTP 方法
資料格式 :RESTful 方式使用 JSON,傳統方式使用表單資料
一致性 :RESTful 方式有統一的設計模式
可維護性 :RESTful 方式更容易理解和維護
RESTful API 設計中,資料結構的一致性 是確保 API 易於理解和使用的重要原則。這意味著同一個資源在不同操作中應該保持相同的資料結構,讓前後端開發者能夠預測 API 的行為。
資料結構一致性的核心觀念:
interface UserResource { id : number; name : string; email : string; phone : string; address : string; avatar : string; createdAt : string; updatedAt : string; }
此時對應的 CRUD 方式則具備相同的拿取與寫入。
Request
URL : /users
Method : GET
Parameters : 無
Request Body
Response
[ { "id" : 1 , "name" : "張三" , "email" : "zhang@example.com" , "phone" : "0912345678" , "address" : "台北市" , "avatar" : "avatar.jpg" , "createdAt" : "2025-01-01T00:00:00Z" , "updatedAt" : "2025-01-01T00:00:00Z" } , { "id" : 2 , "name" : "王小明" , "email" : "xiaoming@example.com" , "phone" : "0912-345-678" , "address" : "台北市" , "avatar" : "avatar2.jpg" , "createdAt" : "2025-01-02T00:00:00Z" , "updatedAt" : "2025-01-02T00:00:00Z" } ]
Request
URL : /users/1
Method : GET
Parameters :
id
(number, required) - 使用者 ID
Request Body
Response
{ "id" : 1 , "name" : "張三" , "email" : "zhang@example.com" , "phone" : "0912345678" , "address" : "台北市" , "avatar" : "avatar.jpg" , "createdAt" : "2025-01-01T00:00:00Z" , "updatedAt" : "2025-01-01T00:00:00Z" }
Request
URL : /users
Method : POST
Parameters : 無
Request Body
{ "name" : "張三" , "email" : "zhang@example.com" , "phone" : "0912345678" , "address" : "台北市" , "avatar" : "avatar.jpg" , }
Response
{ "success" : true , "message" : "使用者資料已新增" , }
Request
URL : /users
Method : PUT
Parameters : 無
Request Body
{ "id" : 1 , "name" : "張三" , "email" : "zhang@example.com" , "phone" : "0912345678" , "address" : "台北市" , "avatar" : "avatar.jpg" , }
Response
{ "success" : true , "message" : "使用者資料已更新" , }
Request
URL : /users/1
Method : PATCH
Parameters : 無
Request Body
{ "email" : "zhang@example.com" , "phone" : "0912345678" , }
Response
{ "success" : true , "message" : "使用者資料已更新" , }
Request
URL : /users
Method : DELETE
Parameters : 無
Request Body
Response
{ "success" : true , "message" : "使用者資料已刪除" , }
重要觀念
GET 回傳的資料結構 = POST 建立時需要的資料結構 = PUT 更新時需要的完整資料結構
PATCH 只需要提供要更新的欄位,不需要完整結構
伺服器管理的欄位 (如 id、createdAt、updatedAt)通常不需要在請求中提供
資料結構一致性 讓 API 更容易理解和使用
以路徑區分資料類型與子分類 在設計 API 時,常透過不同的 URL 路徑來區分各種資料類型,並根據需求進行 CRUD(建立、讀取、更新、刪除)操作。例如 /users
代表使用者資料,/posts
代表文章資料。每個主要路徑下還可以細分子資源,例如 /users/1/posts
代表特定使用者的文章。這種結構有助於讓資料分類更清晰,並方便管理與擴充。
GET /users GET /users/:userId POST /users PUT /users/:userId PATCH /users/:userId DELETE /users/:userId GET /posts GET /posts/:postId POST /posts PUT /posts/:postId PATCH /posts/:postId DELETE /posts/:postId GET /users/:userId/posts GET /cart POST /cart/items PUT /cart/items/:itemId DELETE /cart/items/:itemId
實作 RESTful API 客戶端 以下範例使用 JSONPlaceholder 這個免費的假資料 API 服務,展示如何實作符合 RESTful 設計原則的 API 客戶端。
JSONPlaceholder 練習環境
網址 :https://jsonplaceholder.typicode.com
用途 :提供假資料來練習 API 串接
資源 :users、posts、comments、todos 等
特色 :支援 GET、POST、PUT、PATCH、DELETE 等 HTTP 方法
免費 :不需要註冊或 API Key
restful-api-client.js function createUserAPI (baseURL = 'https://jsonplaceholder.typicode.com' ) { function handleResponse (response ) { if (!response.ok ) { throw new Error (`HTTP ${response.status} : ${response.statusText} ` ); } return response.json (); } async function getAllUsers ( ) { const response = await fetch (`${baseURL} /users` ); return handleResponse (response); } async function getUser (id ) { const response = await fetch (`${baseURL} /users/${id} ` ); return handleResponse (response); } async function postUser (userData ) { const response = await fetch (`${baseURL} /users` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify (userData), }); return handleResponse (response); } async function putUser (id, userData ) { const response = await fetch (`${baseURL} /users/${id} ` , { method : 'PUT' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify (userData), }); return handleResponse (response); } async function patchUser (id, userData ) { const response = await fetch (`${baseURL} /users/${id} ` , { method : 'PATCH' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify (userData), }); return handleResponse (response); } async function deleteUser (id ) { const response = await fetch (`${baseURL} /users/${id} ` , { method : 'DELETE' , }); return handleResponse (response); } return { getAllUsers, getUser, postUser, putUser, patchUser, deleteUser, }; } const userAPI = createUserAPI ();async function demonstrateCRUD (mode, userData = null ) { if (!userData && (mode === 'put' || mode === 'patch' )) { userData = await userAPI.getUser (1 ); } try { switch (mode) { case 'getAll' : const users = await userAPI.getAllUsers (); console .log (`📋 找到 ${users.length} 個使用者` ); break ; case 'get' : const user = await userAPI.getUser (1 ); console .log ('👤 使用者資料:' , user); break ; case 'post' : const newUser = { name : '李小明' , email : 'li@example.com' , username : 'liming' , }; const createdUser = await userAPI.postUser (newUser); console .log ('✅ 新建立的使用者:' , createdUser); break ; case 'put' : const updatedData = { id : 1 , name : '張三(完整更新)' , username : userData.username , email : 'zhang.complete@example.com' , address : userData.address , phone : userData.phone , website : userData.website , company : userData.company , }; const updatedUser = await userAPI.putUser (1 , updatedData); console .log ('✅ 完整更新後:' , updatedUser); break ; case 'patch' : const patchData = { email : 'zhang.partial@example.com' , }; const patchedUser = await userAPI.patchUser (1 , patchData); console .log ('✅ 部分更新後:' , patchedUser); break ; case 'delete' : await userAPI.deleteUser (1 ); console .log ('🗑️ 使用者已刪除' ); break ; } } catch (error) { console .error ('❌ 操作失敗:' , error.message ); } } demonstrateCRUD ('getAll' );demonstrateCRUD ('get' );demonstrateCRUD ('post' );demonstrateCRUD ('put' );demonstrateCRUD ('patch' );demonstrateCRUD ('delete' );
跟著做:體驗 RESTful API 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
完整的 CRUD 操作 :讀取、建立、更新、刪除
HTTP 方法的語義 :每個方法都有明確的用途
統一的錯誤處理 :一致的錯誤處理方式
資料結構一致性 :API 回傳的資料結構保持一致
Client 端儲存 在網頁開發中,我們經常需要在前端儲存一些資料,例如使用者的偏好設定、購物車內容、登入狀態等。瀏覽器提供了多種儲存機制,每種都有其特定的用途和限制。了解這些儲存方式的差異,能幫助我們選擇最適合的解決方案。
為什麼需要 Client 端儲存?
提升使用者體驗 :記住使用者的偏好設定,避免重複輸入
減少伺服器負載 :將一些資料暫存在前端,減少不必要的網路請求
離線功能 :在沒有網路連線時仍能提供基本功能
狀態管理 :管理應用程式的狀態,如購物車、表單資料等
瀏覽器儲存機制概覽 瀏覽器提供了三種主要的儲存機制,每種都有不同的特性和適用場景:
儲存方式
容量限制
生命週期
自動傳送
主要用途
Cookie
~4KB
可設定過期時間
是(每次請求)
會話識別、認證
localStorage
5-10MB
永久(除非手動刪除)
否
使用者偏好、應用設定
sessionStorage
5-10MB
分頁關閉即消失
否
暫存資料、表單狀態
graph TD
A["瀏覽器儲存機制"] --> B["Cookie"]
A --> C["Web Storage"]
C --> D["localStorage"]
C --> E["sessionStorage"]
B --> F["會話識別<br/>認證資訊"]
D --> G["使用者偏好<br/>應用設定"]
E --> H["暫存資料<br/>表單狀態"]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#e8f5e8
style D fill:#f3e5f5
style E fill:#f3e5f5
Cookie:最古老的儲存機制 Cookie 是最早的瀏覽器儲存機制,主要用於會話識別和認證。它的特點是會自動隨每個 HTTP 請求傳送到伺服器,這讓它特別適合儲存認證資訊。
Cookie 的基本概念 Cookie 是什麼? Cookie 是伺服器發送給瀏覽器的小型文字檔案,瀏覽器會將它儲存起來,並在每次向該伺服器發送請求時自動附帶。
Cookie 的組成結構:
name=value; expires=date; path=/; domain=example.com; secure; httponly
name=value :Cookie 的名稱和值
expires :過期時間
path :Cookie 的作用路徑
domain :Cookie 的作用網域
secure :僅在 HTTPS 連線時傳送
httponly :防止 JavaScript 存取(提高安全性)
Cookie 的基本操作 cookie-basics.js function setSimpleCookie (name, value, days = 7 ) { const expires = new Date (); expires.setTime (expires.getTime () + days * 24 * 60 * 60 * 1000 ); document .cookie = `${name} =${value} ; expires=${expires.toUTCString()} ; path=/` ; } function getCookie (name ) { const nameEQ = name + "=" ; const cookies = document .cookie .split (';' ); for (let i = 0 ; i < cookies.length ; i++) { let cookie = cookies[i]; while (cookie.charAt (0 ) === ' ' ) { cookie = cookie.substring (1 , cookie.length ); } if (cookie.indexOf (nameEQ) === 0 ) { return cookie.substring (nameEQ.length , cookie.length ); } } return null ; } function deleteCookie (name ) { document .cookie = `${name} =; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` ; } setSimpleCookie ('theme' , 'dark' , 30 ); console .log ('目前主題:' , getCookie ('theme' )); deleteCookie ('theme' );
跟著做:體驗 Cookie 操作 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
設定 Cookie :setSimpleCookie('theme', 'dark', 30)
讀取 Cookie :getCookie('theme')
會回傳 ‘dark’
刪除 Cookie :deleteCookie('theme')
後再讀取會回傳 null
在開發者工具中查看 Cookie
按 F12 開啟開發者工具
切換到 Application(應用程式)標籤
在左側找到 Cookies
選擇你的網域,就能看到設定的 Cookie
Cookie 的進階用法 cookie-advanced.js function setCookie (name, value, options = {} ) { const { days = 7 , path = '/' , domain = '' , secure = false , sameSite = 'Lax' } = options; const expires = new Date (); expires.setTime (expires.getTime () + days * 24 * 60 * 60 * 1000 ); let cookieString = `${encodeURIComponent (name)} =${encodeURIComponent (value)} ` ; cookieString += `; expires=${expires.toUTCString()} ` ; cookieString += `; path=${path} ` ; if (domain) { cookieString += `; domain=${domain} ` ; } if (secure) { cookieString += '; Secure' ; } cookieString += `; SameSite=${sameSite} ` ; document .cookie = cookieString; } setCookie ('user_preference' , 'dark_mode' );setCookie ('session_id' , 'abc123' , { days : 1 , path : '/admin' , secure : true , sameSite : 'Strict' }); function getAllCookies ( ) { const cookies = {}; document .cookie .split (';' ).forEach (cookie => { const [name, value] = cookie.trim ().split ('=' ); if (name && value) { cookies[decodeURIComponent (name)] = decodeURIComponent (value); } }); return cookies; } console .log ('所有 Cookie:' , getAllCookies ());
Cookie 的安全注意事項
容量限制 :每個 Cookie 約 4KB,總容量有限
自動傳送 :每次請求都會自動傳送,可能造成不必要的網路流量
安全性 :敏感資訊不應存在 Cookie 中,容易被 XSS 攻擊竊取
HttpOnly :重要的認證 Cookie 應由伺服器設定 HttpOnly 屬性
Web Storage:現代瀏覽器儲存方案 Web Storage 是 HTML5 引入的現代儲存機制,包含 localStorage
和 sessionStorage
。相比 Cookie,Web Storage 提供了更大的儲存空間和更簡潔的 API。
選擇儲存方式的建議
使用 localStorage :使用者偏好、應用設定、需要長期保存的資料
使用 sessionStorage :表單狀態、購物車、暫存資料、分頁專用資料
使用 Cookie :認證資訊、會話識別、需要自動傳送到伺服器的資料
localStorage:持久化儲存 localStorage
用於儲存需要長期保存的資料,即使關閉瀏覽器分頁或重新啟動瀏覽器,資料仍然存在。
localStorage-basics.js localStorage .setItem ('username' , '張三' );localStorage .setItem ('theme' , 'dark' );localStorage .setItem ('user_preferences' , JSON .stringify ({ language : 'zh-TW' , notifications : true , autoSave : false })); const username = localStorage .getItem ('username' );const theme = localStorage .getItem ('theme' );const preferences = JSON .parse (localStorage .getItem ('user_preferences' ) || '{}' );console .log ('使用者名稱:' , username);console .log ('主題設定:' , theme);console .log ('使用者偏好:' , preferences);if (localStorage .getItem ('username' )) { console .log ('使用者已登入' ); } else { console .log ('使用者未登入' ); } localStorage .removeItem ('theme' );function getAllLocalStorage ( ) { const data = {}; for (let i = 0 ; i < localStorage .length ; i++) { const key = localStorage .key (i); const value = localStorage .getItem (key); try { data[key] = JSON .parse (value); } catch { data[key] = value; } } return data; } console .log ('所有 localStorage 資料:' , getAllLocalStorage ());
跟著做:體驗 localStorage 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
資料儲存 :使用 setItem()
儲存各種類型的資料
資料讀取 :使用 getItem()
讀取資料
JSON 處理 :複雜物件需要 JSON.stringify()
和 JSON.parse()
資料檢查 :檢查特定資料是否存在
資料管理 :刪除和清空資料
在開發者工具中查看 localStorage
按 F12 開啟開發者工具
切換到 Application(應用程式)標籤
在左側找到 Local Storage
選擇你的網域,就能看到儲存的資料
sessionStorage:分頁級別儲存 sessionStorage
的資料只在當前分頁有效,關閉分頁後資料就會消失。這讓它特別適合儲存暫存資料,如表單狀態、購物車內容等。
sessionStorage-basics.js sessionStorage .setItem ('current_step' , '2' );sessionStorage .setItem ('form_data' , JSON .stringify ({ name : '李小明' , email : 'li@example.com' , phone : '0912345678' })); const currentStep = sessionStorage .getItem ('current_step' );const formData = JSON .parse (sessionStorage .getItem ('form_data' ) || '{}' );console .log ('目前步驟:' , currentStep);console .log ('表單資料:' , formData);function saveFormStep (step, data ) { sessionStorage .setItem ('current_step' , step.toString ()); sessionStorage .setItem ('form_data' , JSON .stringify (data)); console .log (`步驟 ${step} 已儲存` ); } function loadFormStep ( ) { const step = sessionStorage .getItem ('current_step' ) || '1' ; const data = JSON .parse (sessionStorage .getItem ('form_data' ) || '{}' ); return { step : parseInt (step), data }; } saveFormStep (1 , { name : '張三' });saveFormStep (2 , { name : '張三' , email : 'zhang@example.com' });saveFormStep (3 , { name : '張三' , email : 'zhang@example.com' , phone : '0912345678' });const { step, data } = loadFormStep ();console .log (`載入步驟 ${step} ,資料:` , data);function addToCart (product ) { const cart = JSON .parse (sessionStorage .getItem ('cart' ) || '[]' ); cart.push (product); sessionStorage .setItem ('cart' , JSON .stringify (cart)); console .log ('商品已加入購物車' ); } function getCart ( ) { return JSON .parse (sessionStorage .getItem ('cart' ) || '[]' ); } function clearCart ( ) { sessionStorage .removeItem ('cart' ); console .log ('購物車已清空' ); } addToCart ({ id : 1 , name : 'iPhone' , price : 30000 });addToCart ({ id : 2 , name : 'MacBook' , price : 50000 });console .log ('購物車內容:' , getCart ());
實際應用範例 主題切換功能 theme-switcher.js class ThemeManager { constructor ( ) { this .currentTheme = localStorage .getItem ('theme' ) || 'light' ; this .applyTheme (); } toggleTheme ( ) { this .currentTheme = this .currentTheme === 'light' ? 'dark' : 'light' ; localStorage .setItem ('theme' , this .currentTheme ); this .applyTheme (); console .log (`主題已切換為:${this .currentTheme} ` ); } setTheme (theme ) { this .currentTheme = theme; localStorage .setItem ('theme' , this .currentTheme ); this .applyTheme (); console .log (`主題已設定為:${this .currentTheme} ` ); } applyTheme ( ) { document .body .className = `theme-${this .currentTheme} ` ; } getCurrentTheme ( ) { return this .currentTheme ; } } const themeManager = new ThemeManager ();console .log ('目前主題:' , themeManager.getCurrentTheme ());
購物車功能 shopping-cart.js class ShoppingCart { constructor ( ) { this .items = this .loadCart (); } loadCart ( ) { const cartData = sessionStorage .getItem ('shopping_cart' ); return cartData ? JSON .parse (cartData) : []; } saveCart ( ) { sessionStorage .setItem ('shopping_cart' , JSON .stringify (this .items )); } addItem (product ) { const existingItem = this .items .find (item => item.id === product.id ); if (existingItem) { existingItem.quantity += 1 ; } else { this .items .push ({ ...product, quantity : 1 }); } this .saveCart (); console .log (`商品「${product.name} 」已加入購物車` ); } removeItem (productId ) { this .items = this .items .filter (item => item.id !== productId); this .saveCart (); console .log ('商品已從購物車移除' ); } updateQuantity (productId, quantity ) { const item = this .items .find (item => item.id === productId); if (item) { item.quantity = Math .max (0 , quantity); if (item.quantity === 0 ) { this .removeItem (productId); } else { this .saveCart (); } } } getItems ( ) { return this .items ; } getTotal ( ) { return this .items .reduce ((total, item ) => { return total + (item.price * item.quantity ); }, 0 ); } clear ( ) { this .items = []; this .saveCart (); console .log ('購物車已清空' ); } getItemCount ( ) { return this .items .reduce ((count, item ) => count + item.quantity , 0 ); } } const cart = new ShoppingCart ();cart.addItem ({ id : 1 , name : 'iPhone 15' , price : 35000 }); cart.addItem ({ id : 2 , name : 'AirPods Pro' , price : 8000 }); cart.addItem ({ id : 1 , name : 'iPhone 15' , price : 35000 }); console .log ('購物車內容:' , cart.getItems ());console .log ('商品總數:' , cart.getItemCount ());console .log ('總價:' , cart.getTotal ());cart.updateQuantity (1 , 3 ); console .log ('更新後總價:' , cart.getTotal ());
認證機制:Session vs Token 在網頁應用中,認證是確保使用者身份的重要機制。主要有兩種方式:Session 和 Token。
Session 認證機制 Session 認證是傳統的認證方式,伺服器會保存使用者的會話狀態。
session-auth.js async function login (username, password ) { try { const response = await fetch ('/api/login' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify ({ username, password }), credentials : 'include' }); if (response.ok ) { const data = await response.json (); console .log ('登入成功' ); localStorage .setItem ('user_info' , JSON .stringify ({ id : data.user .id , username : data.user .username , email : data.user .email })); return data; } else { throw new Error ('登入失敗' ); } } catch (error) { console .error ('登入錯誤:' , error.message ); throw error; } } function checkLoginStatus ( ) { const userInfo = localStorage .getItem ('user_info' ); return userInfo ? JSON .parse (userInfo) : null ; } async function logout ( ) { try { await fetch ('/api/logout' , { method : 'POST' , credentials : 'include' }); localStorage .removeItem ('user_info' ); console .log ('登出成功' ); } catch (error) { console .error ('登出錯誤:' , error.message ); } } const currentUser = checkLoginStatus ();if (currentUser) { console .log ('目前登入使用者:' , currentUser.username ); } else { console .log ('使用者未登入' ); }
Token 認證機制 Token 認證是現代的認證方式,伺服器簽發包含使用者資訊的 token。
token-auth.js async function loginWithToken (username, password ) { try { const response = await fetch ('/api/login' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON .stringify ({ username, password }) }); if (response.ok ) { const data = await response.json (); console .log ('登入成功' ); localStorage .setItem ('auth_token' , data.token ); localStorage .setItem ('user_info' , JSON .stringify (data.user )); return data; } else { throw new Error ('登入失敗' ); } } catch (error) { console .error ('登入錯誤:' , error.message ); throw error; } } function getAuthHeaders ( ) { const token = localStorage .getItem ('auth_token' ); return token ? { 'Authorization' : `Bearer ${token} ` } : {}; } async function authenticatedRequest (url, options = {} ) { const headers = { 'Content-Type' : 'application/json' , ...getAuthHeaders (), ...options.headers }; const response = await fetch (url, { ...options, headers }); if (response.status === 401 ) { localStorage .removeItem ('auth_token' ); localStorage .removeItem ('user_info' ); throw new Error ('認證失敗,請重新登入' ); } return response; } function logoutWithToken ( ) { localStorage .removeItem ('auth_token' ); localStorage .removeItem ('user_info' ); console .log ('登出成功' ); } async function getUserProfile ( ) { try { const response = await authenticatedRequest ('/api/profile' ); const profile = await response.json (); console .log ('使用者資料:' , profile); return profile; } catch (error) { console .error ('取得資料失敗:' , error.message ); } }
Session vs Token 對比 認證機制概述
Session 認證 :傳統方式,伺服器保存會話狀態
Token 認證 :現代方式,伺服器簽發自包含 token
特性對比表
特性/認證方式
Session 認證
Token 認證
狀態管理
有狀態(伺服器儲存)
無狀態(伺服器不儲存)
安全性
高(敏感資訊在伺服器) • 安全性高
中等(Token 可能被竊取) • 前端需妥善保存
撤銷能力
即時撤銷 • 可以即時撤銷(登出時清除伺服器端 session)
難以即時撤銷 • 撤銷困難
擴展性
較差(需要 session 共享) • 擴展困難
良好(適合分散式系統) • 分散式友好
資料庫查詢
每次請求都查詢 • 每次查詢資料庫
可減少查詢 • 減少資料庫查詢
跨域支援
有限
良好(支援多種客戶端和跨域請求)
記憶體使用
較高(伺服器需儲存 session) • 記憶體使用量大
較低
網路傳輸
較小(僅 session ID)
較大(完整 Token) • Token 大小較大
優點
• 即時撤銷 • 完全控制 • 安全性高 • 適合即時控制
• 無狀態 • 分散式友好 • 減少資料庫查詢 • 跨域支援
缺點
• 需要伺服器儲存 • 擴展困難 • 每次查詢資料庫 • 記憶體使用量大
• 撤銷困難 • Token 大小較大 • 前端需妥善保存 • 過期時間固定
認證流程對比
步驟
Session 認證流程
Token 認證流程
1. 登入
使用者登入 → 伺服器建立 session
使用者登入 → 伺服器簽發 JWT Token
2. 回傳
伺服器回傳 session ID → 儲存在 Cookie
伺服器回傳 Token → 前端儲存
3. 請求
後續請求 → 瀏覽器自動帶上 session ID
後續請求 → 前端在 Header 中帶上 Token
4. 驗證
伺服器驗證 session ID → 確認使用者身份
伺服器驗證 Token → 確認使用者身份
選擇建議 何時選擇 Session 認證?
需要即時控制使用者狀態
安全性要求高的應用
單一伺服器架構
需要即時撤銷功能
何時選擇 Token 認證?
分散式系統或微服務架構
需要跨域支援
API 服務或第三方整合
需要減少伺服器負載
安全注意事項
敏感資訊 :不要在前端儲存密碼、信用卡號等敏感資訊
Token 安全 :JWT Token 應設定適當的過期時間
XSS 防護 :避免將敏感資訊存在 localStorage(容易被 XSS 攻擊竊取)
CSRF 防護 :使用 SameSite Cookie 或 CSRF Token 防止跨站請求偽造
HTTPS :在生產環境中務必使用 HTTPS 保護資料傳輸
常見錯誤處理模式 在實際開發中,我們需要妥善處理儲存相關的錯誤,確保應用程式的穩定性。
storage-error-handling.js function safeSetItem (key, value ) { try { const serializedValue = typeof value === 'string' ? value : JSON .stringify (value); localStorage .setItem (key, serializedValue); return true ; } catch (error) { console .error ('儲存失敗:' , error.message ); if (error.name === 'QuotaExceededError' ) { console .log ('儲存空間不足,嘗試清理舊資料...' ); cleanupOldData (); try { localStorage .setItem (key, serializedValue); return true ; } catch (retryError) { console .error ('清理後仍無法儲存:' , retryError.message ); return false ; } } return false ; } } function safeGetItem (key, defaultValue = null ) { try { const value = localStorage .getItem (key); if (value === null ) { return defaultValue; } try { return JSON .parse (value); } catch { return value; } } catch (error) { console .error ('讀取失敗:' , error.message ); return defaultValue; } } function cleanupOldData ( ) { const keysToKeep = ['user_preferences' , 'auth_token' ]; const allKeys = Object .keys (localStorage ); allKeys.forEach (key => { if (!keysToKeep.includes (key)) { localStorage .removeItem (key); } }); console .log ('舊資料清理完成' ); } function checkStorageSpace ( ) { try { const testKey = '__storage_test__' ; const testValue = 'x' .repeat (1024 * 1024 ); localStorage .setItem (testKey, testValue); localStorage .removeItem (testKey); console .log ('儲存空間充足' ); return true ; } catch (error) { console .warn ('儲存空間不足:' , error.message ); return false ; } } const userData = { id : 123 , name : '張三' , preferences : { theme : 'dark' , language : 'zh-TW' } }; if (safeSetItem ('user_data' , userData)) { console .log ('使用者資料儲存成功' ); } else { console .log ('使用者資料儲存失敗' ); } const savedUserData = safeGetItem ('user_data' , {});console .log ('讀取的使用者資料:' , savedUserData);checkStorageSpace ();
跟著做:體驗錯誤處理 將上述程式碼複製到瀏覽器開發者工具的 Console 中執行,你會看到:
安全儲存 :使用 safeSetItem()
避免儲存錯誤
安全讀取 :使用 safeGetItem()
避免讀取錯誤
空間檢查 :使用 checkStorageSpace()
檢查可用空間
自動清理 :當空間不足時自動清理舊資料
實際應用建議
總是使用安全的儲存函數
定期檢查儲存空間
實作資料清理機制
提供適當的錯誤訊息給使用者
延伸閱讀
MDN:Promise
、async/await
、fetch
、AbortController
You-Dont-Need-jQuery:現代瀏覽器 API
OWASP:前端安全與 Token 儲存建議