這一課完成後:你會使用 fetch() 與 async / await 取得 JSON,判斷 HTTP response 是否成功,處理 loading / success / error 三種狀態,並把 API 回傳的 array/object 渲染到 DOM。

先把網站用 HTTP server 跑起來

前幾課直接雙擊 index.html 也能完成很多練習,但從網路請求開始,建議養成從本機 HTTP server 開網站的習慣。

在 web-basics-playground 根目錄開 Terminal。如果你有 Python:

python -m http.server 8000

Windows 如果使用 py:

py -m http.server 8000

然後在瀏覽器打開:

http://localhost:8000

現在頁面的來源不再是 file://...,而是一個真正的 HTTP origin。

Browser → http://localhost:8000 → 你的本機 server → index.html

API 先理解成「另一個程式提供的介面」

這一課會用 JSONPlaceholder 當公開測試 API。它提供假的資料,適合練習 request / response,不需要 API key。

例如這個網址:

https://jsonplaceholder.typicode.com/todos/1

直接在瀏覽器打開,你會看到類似:

{
  "userId": 1,
  "id": 1,
  "title": "...",
  "completed": false
}

這不是 HTML,而是 JSON:很常用來讓不同程式交換結構化資料。

第一次 fetch:先只看 Console

在 script.js:

fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  });

重新整理頁面,打開 DevTools Console。你應該會看到一個 JavaScript object。

fetch URL → 等待 HTTP response → response.json() → JavaScript data

這種 .then() 寫法很常見,你要看得懂;但這門課接下來主要用 async / await,因為對初學者比較容易順著流程閱讀。

為什麼 fetch 不是立刻回傳資料?

網路請求需要時間。瀏覽器要送出 request、等待另一台伺服器處理,再接收 response。

所以這種想法不成立:

const data = fetch("https://example.com/data");
console.log(data.title);

fetch() 回傳的是 Promise,不是已經完成的 JSON 資料。

現在發出工作 → Promise 代表未來結果 → 完成後才能使用資料

async / await:把非同步流程寫得比較像一般順序

async function loadTodo() {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/todos/1"
  );

  const data = await response.json();
  console.log(data);
}

loadTodo();

async 表示這個函式裡可以使用 await;await 則表示:這個非同步步驟完成後,再繼續這個函式後面的程式。

它不是把整個瀏覽器凍結。其他事件與瀏覽器工作仍然可以繼續。

Response 和 JSON data 是兩個不同東西

這兩行不要混成同一個概念:

const response = await fetch(url);
const data = await response.json();

response 包含 HTTP response 的資訊,例如 status、headers、是否成功;data 才是把 response body 解析成 JavaScript 值之後的結果。

HTTP Response → status / headers / body
response.json() → 解析 body → JavaScript object / array

很重要:404 / 500 不一定會讓 fetch 自動 throw

很多初學者會以為只要伺服器回 404 或 500,fetch() 就一定直接進 catch。實際上,fetch 通常在「網路請求本身失敗」時才 reject;HTTP error response 仍然可能正常得到一個 Response。

所以要自己檢查:

const response = await fetch(url);

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

response.ok 通常代表 status 在成功範圍。

網路有回應 ≠ 請求成功
response.ok → 再判斷 HTTP 結果

把完整錯誤處理補起來

async function loadTodo() {
  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    );

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("載入失敗:", error);
  }
}

loadTodo();

try 裡放可能失敗的流程;catch 接住錯誤。

錯誤可能來自網路中斷、CORS、錯誤 URL、伺服器問題、解析資料失敗,或你自己的程式。

不要只有成功畫面:先建立三種 UI state

任何需要等待外部資料的 UI,至少先想三種狀態:

Loading → 正在等
Success → 資料到了
Error → 沒有成功

HTML:

<button id="load-data" type="button">載入資料</button>
<p id="status">尚未載入。</p>
<div id="result"></div>

JavaScript:

const loadButton = document.querySelector("#load-data");
const status = document.querySelector("#status");
const result = document.querySelector("#result");

async function loadTodo() {
  status.textContent = "Loading...";
  result.textContent = "";

  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    );

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const todo = await response.json();

    status.textContent = "Loaded.";
    result.textContent = todo.title;
  } catch (error) {
    status.textContent = "載入失敗,請稍後再試。";
    console.error(error);
  }
}

loadButton.addEventListener("click", loadTodo);

現在前幾課全部串起來了:

click event → async function → fetch → JSON → state → DOM

等待時先防止重複點擊

如果使用者連點五次,就可能同時送出五個 request。最簡單的改善:

async function loadTodo() {
  loadButton.disabled = true;
  status.textContent = "Loading...";

  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    );

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const todo = await response.json();
    result.textContent = todo.title;
    status.textContent = "Loaded.";
  } catch (error) {
    status.textContent = "載入失敗。";
    console.error(error);
  } finally {
    loadButton.disabled = false;
  }
}

finally 不管成功或失敗都會執行,很適合做「恢復按鈕」、「關閉 loading」這類收尾。

從一筆資料進到一整個陣列

改抓:

https://jsonplaceholder.typicode.com/todos

回傳的是 array。為了不要一次畫 200 筆,我們只取前五筆:

const todos = await response.json();
const firstFive = todos.slice(0, 5);

接著把它們渲染到 DOM:

const list = document.querySelector("#todo-list");

list.replaceChildren();

for (const todo of firstFive) {
  const item = document.createElement("li");
  item.textContent = todo.title;

  if (todo.completed) {
    item.classList.add("done");
  }

  list.append(item);
}

這就是非常典型的前端資料流:

API array → JavaScript array → loop → createElement → DOM

把「取得資料」和「畫畫面」拆開

程式開始變長後,不要把所有東西塞進同一個函式。

function renderTodos(todos) {
  list.replaceChildren();

  for (const todo of todos) {
    const item = document.createElement("li");
    item.textContent = todo.title;
    list.append(item);
  }
}

async function loadTodos() {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/todos"
  );

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const todos = await response.json();
  renderTodos(todos.slice(0, 5));
}

這樣責任比較清楚:

loadTodos → 負責取得資料
renderTodos → 負責把資料轉成 DOM

這種拆法之後做 Web App 會非常重要。

DevTools Network:不要只看 Console

打開 DevTools → Network,再按「載入資料」。你可以看到瀏覽器真的送出一筆 request。

點進去可以觀察:

Request URL
Method
Status code
Response headers
Response body
花了多久

當 API 出問題時,Network panel 常常比只看 JavaScript error 更直接。

CORS:不是你的 fetch 語法錯了

瀏覽器有 same-origin security model。當你的網站向另一個 origin 讀資料時,對方伺服器必須允許這種跨來源存取。

如果 Console 出現 CORS 錯誤,不代表你能靠前端 JavaScript 自己「關掉 CORS」。真正的允許規則是由伺服器 response 決定。

不要照網路教學安裝「停用瀏覽器安全性」的奇怪外掛來解正式專案。練習時改用允許跨來源存取的 API;自己的 Web App 則在 backend / API 正確設定。

不要把 API key 寫在前端 JavaScript

任何送到瀏覽器的 HTML / CSS / JavaScript,使用者基本上都能查看。

所以這種做法不是秘密:

const API_KEY = "super-secret-key";

如果某個服務要求必須保密的 server-side key,它不應該直接出現在公開前端 bundle 裡。Web App 路徑會再正式處理 backend、environment variables 與 secrets。

Public browser code → 不適合保存秘密
Secret credential → 通常交給 backend / server-side environment

GET 先學會就夠,其他 HTTP methods 先知道名字

這一課主要使用 GET,也就是「取得資源」。之後你還會遇到:

GET → 取得
POST → 建立 / 送資料
PUT / PATCH → 更新
DELETE → 刪除

它們不是 JavaScript 專屬概念,而是 HTTP API 很常見的介面設計。真正建立自己的 API 會放到 Web App。

小挑戰:做一個可以重新載入的 Todo 資料頁

需求 1:頁面有「載入 Todos」按鈕、status 區、list 區。

需求 2:按下按鈕後才發出 fetch request。

需求 3:等待時顯示 Loading,並暫時 disabled 按鈕。

需求 4:一定檢查 response.ok。

需求 5:使用 async / await 與 try / catch / finally。

需求 6:抓 /todos,至少顯示前 5 筆。

需求 7:完成的 todo 加上不同 CSS class。

需求 8:錯誤時畫面要有可理解訊息,不可以只 console.error。

需求 9:在 DevTools Network 找到這次 request,確認 status code。

故意讓它失敗一次

把 URL 暫時改成不存在的資源,例如一個錯誤 route,觀察:

Network status → response.ok → throw → catch → Error UI

再把 URL 修回正確。你不是只在測「成功會不會動」,而是在測「失敗時網站會不會壞成一片空白」。

最後收斂:外部資料進入畫面的完整路徑

User Event → Fetch Request → HTTP Response → JSON → JavaScript State/Data → Render DOM → User sees result

到這裡,Web Basics 的核心零件其實已經全部學過:HTML、CSS、JavaScript、DOM、Events、Fetch。

下一課不會再介紹一個新的基礎技術,而是把這六塊放到同一個專案裡,做一個真正可操作、可重新載入資料、能處理失敗狀態的前端作品。

完成條件:你能從使用者事件啟動 fetch request,使用 async / await 取得 JSON,檢查 HTTP response、處理 loading / error state,並把 array/object 資料安全地渲染成 DOM。