JavaScript – Fetch 與網路請求(Networking)
主題:JSON 解析
簡介
在前端開發中,與後端或第三方 API 交換資料是最常見的需求,而 JSON(JavaScript Object Notation)已成為事實上的資料交換格式。
透過 fetch 取得的回應往往是 JSON 字串,若不正確解析,就會導致程式錯誤、使用者體驗下降,甚至安全問題。因此,掌握 JSON 解析的正確流程與技巧,是每位 JavaScript 開發者必備的基礎能力。
本篇文章將從 概念說明、實作範例、常見陷阱與最佳實踐,一步步帶你了解如何在 fetch 中安全、有效地處理 JSON,並提供多個可直接套用的程式碼範例,讓你在實務專案中快速上手。
核心概念
1. 為什麼要使用 JSON?
- 輕量且易讀:相較於 XML,JSON 結構簡潔,直接對應 JavaScript 物件。
- 跨平台支援:幾乎所有程式語言都有成熟的 JSON 解析函式庫。
- 效能佳:字串大小較小,傳輸與解析速度快。
2. fetch 回傳的 Response 物件
fetch(url, options) 會回傳一個 Promise,解決後得到 Response 物件。Response 本身不會自動把內容轉成 JavaScript 物件,必須呼叫以下方法之一:
| 方法 | 目的 | 回傳值 |
|---|---|---|
text() |
取得純文字 | Promise<string> |
json() |
解析為 JSON 物件 | Promise<any> |
blob() |
取得二進位檔案 | Promise<Blob> |
arrayBuffer() |
取得二進位緩衝區 | Promise<ArrayBuffer> |
重點:只有呼叫
response.json()才會把 JSON 字串自動解析成 JavaScript 物件,且此過程會自動拋出 SyntaxError(若字串不是合法的 JSON)。
3. 解析流程的完整範例
fetch('https://api.example.com/users')
.then(response => {
// 檢查 HTTP 狀態碼是否成功 (200~299)
if (!response.ok) {
// 自訂錯誤訊息,拋出讓後面的 catch 捕捉
throw new Error(`Network response was not ok (${response.status})`);
}
// 直接呼叫 json() 解析
return response.json();
})
.then(data => {
// data 已是 JavaScript 物件
console.log('取得的使用者資料:', data);
})
.catch(error => {
// 處理網路錯誤、JSON 解析錯誤或自訂錯誤
console.error('發生錯誤:', error);
});
程式碼範例
以下提供 5 個常見且實用的 JSON 解析範例,每個範例都附上說明與最佳實踐。
範例 1️⃣ 基本 GET 請求與 JSON 解析
// 取得公開的天氣資訊
fetch('https://api.openweathermap.org/data/2.5/weather?q=Taipei&appid=YOUR_KEY')
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json(); // 解析 JSON
})
.then(weather => {
// 直接使用解構賦值取得需要的欄位
const { main: { temp }, weather: [{ description }] } = weather;
console.log(`目前氣溫 ${temp}K,天氣 ${description}`);
})
.catch(err => console.error('取得天氣失敗:', err));
技巧:使用 解構賦值 可以一次取出深層結構,程式碼更簡潔。
範例 2️⃣ POST 請求:送出 JSON 並解析回傳結果
const newUser = {
name: 'Alice',
email: 'alice@example.com',
role: 'admin'
};
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 告訴伺服器請求主體是 JSON
},
body: JSON.stringify(newUser) // 將物件序列化成字串
})
.then(res => {
if (res.status === 201) return res.json(); // 成功建立,解析回傳的使用者物件
// 其他狀態碼視為錯誤
return res.text().then(txt => { throw new Error(txt); });
})
.then(createdUser => {
console.log('新使用者已建立:', createdUser);
})
.catch(err => console.error('建立使用者失敗:', err));
重點:
Content-Type: application/json必須與JSON.stringify同時使用,否則伺服器可能無法正確解析請求體。
範例 3️⃣ 使用 async/await 重構
async function fetchProducts() {
try {
const response = await fetch('https://api.example.com/products');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json(); // 直接解析
// 篩選出價格低於 1000 的商品
const cheap = products.filter(p => p.price < 1000);
console.log('低價商品:', cheap);
} catch (e) {
console.error('取得商品失敗:', e);
}
}
fetchProducts();
優點:
async/await讓非同步流程更像同步程式,易於閱讀與除錯。
範例 4️⃣ 處理 巨量 JSON(分頁或流式)
當 API 回傳大量資料時,直接一次 json() 可能會造成記憶體壓力。此時可配合 分頁 或 ReadableStream:
async function fetchAllPages() {
let page = 1;
const allItems = [];
while (true) {
const res = await fetch(`https://api.example.com/items?page=${page}`);
if (!res.ok) throw new Error(`Page ${page} error`);
const { data, nextPage } = await res.json(); // 假設回傳 { data: [], nextPage: number|null }
allItems.push(...data);
if (!nextPage) break; // 沒有下一頁就結束
page = nextPage;
}
console.log('共取得', allItems.length, '筆資料');
return allItems;
}
fetchAllPages();
實務建議:若 API 支援
Range或Cursor,盡量使用分頁機制,避免一次載入過大 JSON。
範例 5️⃣ 防止 JSON 注入(安全性)
有時候後端會回傳包含使用者輸入的字串,若直接 eval 會產生安全風險。永遠使用 JSON.parse(或 response.json())而非 eval:
// 錯誤做法(千萬別這樣!)
const unsafe = '{"name":"<script>alert(1)</script>"}';
const obj = eval('(' + unsafe + ')'); // 可能執行惡意腳本
// 正確做法
const safeObj = JSON.parse(unsafe);
console.log(safeObj.name); // 只會得到字串,不會執行腳本
結論:
JSON.parse只會把字串轉成資料結構,絕不會執行其中的程式碼,確保前端不受注入攻擊。
常見陷阱與最佳實踐
| 常見陷阱 | 說明 | 解決方法 |
|---|---|---|
忘記檢查 response.ok |
即使 HTTP 404/500,response.json() 仍會嘗試解析,導致不易察覺的錯誤。 |
在呼叫 json() 前先 if (!res.ok) throw new Error(...)。 |
直接使用 response.text() 再 JSON.parse |
若忘記 await 或忘記回傳 Promise,會得到 Promise 本身。 |
使用 await response.json()(或 return response.json())即可。 |
| 跨域 CORS 錯誤 | 瀏覽器會阻擋未授權的跨域請求,返回的 Response 會是 opaque,無法讀取內容。 |
確認伺服器有正確設定 Access-Control-Allow-Origin,或使用 JSONP / Proxy。 |
| JSON 數字精度問題 | JavaScript 的 Number 只能安全表示到 2^53-1,過大整數會失真。 |
若需要大數字,請在伺服器端傳回字串,或使用 BigInt(JSON.parse 仍會是字串,需要自行轉換)。 |
多層嵌套導致 undefined |
直接存取深層屬性時若其中一層不存在會拋 TypeError。 |
使用 可選鏈 (obj?.prop?.sub) 或先檢查 if (obj && obj.prop). |
最佳實踐清單
- 統一錯誤處理:建立一個
handleResponse函式,負責檢查狀態碼、解析 JSON、捕捉錯誤。 - 使用
async/await:讓非同步流程更直觀,特別是在多個串接請求時。 - 設定
Accept: application/json:告訴伺服器你希望得到 JSON。 - 避免
eval:永遠使用JSON.parse,切勿自行拼接字串後eval。 - 在 TypeScript 中加上型別:利用介面 (
interface) 定義回傳結構,提升開發時的 IntelliSense 與編譯安全。
// 範例:通用的 response 處理函式
async function fetchJSON(url, options = {}) {
const res = await fetch(url, {
headers: { 'Accept': 'application/json', ...options.headers },
...options
});
if (!res.ok) {
const errMsg = await res.text();
throw new Error(`HTTP ${res.status}: ${errMsg}`);
}
return res.json(); // 直接回傳解析後的物件
}
實際應用場景
| 場景 | 需求 | JSON 解析的角色 |
|---|---|---|
| SPA(單頁應用) | 動態載入列表、表單提交、即時搜尋 | fetch + response.json() 為主,配合 debounce 減少請求次數。 |
| 行動端離線緩存 | 需要在離線時仍能瀏覽先前資料 | 取得 JSON 後使用 IndexedDB 或 LocalStorage 寫入本機,之後再 JSON.parse 讀回。 |
| 儀表板(Dashboard) | 同時顯示多個圖表、需要即時更新 | 使用 Promise.all 同時取得多筆 JSON,或 WebSocket 配合 JSON.stringify / JSON.parse 進行雙向通訊。 |
| 第三方服務整合 | 如支付、地圖、社群登入 | 多數第三方 API 回傳 JSON,必須正確解析才能取得授權 token、回傳結果等關鍵資訊。 |
| 伺服器端渲染(SSR) | 先在 Node.js 端取得資料後渲染 HTML | node-fetch(或原生 fetch)同樣返回 Response,解析方式不變,只是執行環境在伺服器。 |
總結
- JSON 是現代 Web 應用的核心資料格式,透過
fetch搭配response.json()可以安全、快速地把伺服器回傳的字串轉成可操作的 JavaScript 物件。 - 正確的 錯誤檢查、跨域設定、以及 安全解析(避免
eval)是避免常見問題的關鍵。 - 使用
async/await、可選鏈、以及 解構賦值,可以讓程式碼更簡潔、可讀性更高。 - 在實務開發中,將 JSON 解析 與 錯誤統一處理、分頁/緩存策略 結合,才能打造效能佳、使用者體驗好的前端應用。
掌握以上概念與實作技巧,你將能在任何需要與後端或第三方 API 互動的情境下,穩定且安全地處理 JSON 資料,為你的 JavaScript 專案奠定堅實的基礎。祝你寫程式愉快,持續探索更深入的前端技術!