Học bằng cách đọc theo thứ tự

fetch — lấy dữ liệu từ API

Lấy JSON từ một URL bằng fetch và json(), nhận biết lỗi bằng response.ok, viết URL tương đối và tải hai file cùng lúc bằng Promise.all.

Đến giờ, các bài tập đều viết dữ liệu thẳng vào code. Trên một trang web thật, giá và số lượng tồn kho luôn thay đổi, nên mỗi lần được mở, trang lại hỏi API của server (URL mà chương trình dùng để lấy dữ liệu).

Bài này nói về fetch để lấy dữ liệu từ một URL, và response.ok cho biết yêu cầu có thành công hay không.

Lấy JSON từ API — fetch và json()

Giả sử bạn muốn trang lịch sử đơn hàng hiển thị các đơn hàng đang lưu trên server. Đơn hàng được gửi về dưới dạng chuỗi JSON, nên bạn cần một cách để chờ đến khi dữ liệu tới, và một bước chuyển chuỗi đó trở lại thành mảng. Trong các bài tập, thay cho API thật, bạn sẽ lấy các file JSON đặt trong thư mục fixtures của trang này.

fetch (hàm yêu cầu server gửi dữ liệu tại một URL) trả về một Promise, và Promise này được fulfilled với một Response đại diện cho phản hồi của server. status (con số bằng 200 khi thành công) và header (thông tin về phản hồi, được gửi trước phần nội dung) tới trước, còn phần nội dung (body) tới sau, nên bạn phải await response.json() thêm một lần nữa để lấy body.

// Lấy JSON đơn hàng đặt trên trang này (cách viết URL sẽ nói ở phần sau)
const response = await fetch("fixtures/vi/orders.json");
console.log(response.status);          // 200

// Đọc hết body rồi chuyển từ JSON sang mảng
const orders = await response.json();
console.log(orders.length);            // 3
console.log(orders[1].total);          // 12600

// Từ đây trở đi, đó là một mảng bình thường
const preparing = orders.filter((order) => order.status === "Đang chuẩn bị");
console.log(preparing.map((order) => order.id).join(", "));   // A-3002, A-3003
Thứ tự phản hồi tới và hai lần await
fetch(url) gửiyêu cầu tới serverPromise vẫnđang pendingStatus 200 vàheader tớiawait fetch(…)xong, có ResponseBody tới đầy đủ,chuyển từ JSONawait json() xong,có mảng 3 đơn
await của fetch kết thúc ngay khi header tới. Mảng chỉ có sau await của json(), lúc body đã được đọc hết.

json() đọc body đến hết và chuyển đổi giống như JSON.parse rồi mới trả về giá trị, nên bạn dùng await để chờ kết quả. Sau khi chuyển đổi, orders là một mảng bình thường, dùng được ngay filtermap.

In Response ra sẽ chỉ thấy {}

Trong console của bài tập, console.log(response) in ra {}, giống như khi in một phần tử DOM. response.json() không có await là một Promise, nên cũng in ra {}. Muốn kiểm tra Response, hãy in status; muốn kiểm tra body, hãy in giá trị nhận được khi await json().

Trên màn hình quản lý thành viên, bạn lấy danh sách thành viên từ server và liệt kê tên. Danh sách thành viên có tại fixtures/vi/users.json.

① Hãy lấy danh sách thành viên và in ra status của phản hồi.

② Hãy chuyển body thành mảng và in ra số thành viên.

③ Hãy chỉ lấy tên, nối lại bằng “, ” rồi in ra.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Viết URL theo vị trí của trang — URL tương đối

Ở phần trước, chỉ cần viết "fixtures/vi/orders.json" là đã lấy được file. URL đầy đủ bắt đầu bằng https:// chứa cả domain (phần ngay sau https://, như runner.desktechlearn.com, cho biết đó là trang web nào), nên mỗi khi trang web chuyển sang địa chỉ khác, bạn lại phải sửa URL.

URL tương đối (URL chỉ ghi vị trí tính từ URL của trang hiện tại) được tính từ thư mục chứa trang nếu phía trước không có gì, và từ gốc của domain nếu bắt đầu bằng /. Code của bài tập không chạy trên trang bài viết mà chạy trong runner.html, một trang không có giao diện nằm trên runner.desktechlearn.com.

// URL của trang mà code này chạy (console của bài tập)
// https://runner.desktechlearn.com/tools/js-editor/runner.html

// Không có / ở đầu: tính từ js-editor, thư mục chứa runner.html
const fromPage = await fetch("fixtures/vi/orders.json");
console.log(fromPage.url);   // url là URL thực sự được yêu cầu
// https://runner.desktechlearn.com/tools/js-editor/fixtures/vi/orders.json

// Có / ở đầu: tính từ gốc của domain
const fromRoot = await fetch("/tools/js-editor/fixtures/vi/orders.json");
console.log(fromRoot.url === fromPage.url);   // true (cùng trỏ tới một file)

// Nếu bỏ sót fixtures/vi/, nó sẽ tìm ngay trong js-editor
const noFolder = await fetch("orders.json");
console.log(noFolder.url);
// https://runner.desktechlearn.com/tools/js-editor/orders.json
Vị trí của runner.html và fixtures
runner.desktechlearn.com — gốc của domain (/)
  • URL bắt đầu bằng / được tính từ đây
/tools/js-editor/
  • runner.html — trang nơi code bài tập chạy
  • URL không có / ở đầu được tính từ đây
Thư mục fixtures/vi
  • Chứa orders.json, users.jsonproducts.json
fixtures nằm trong js-editor, cùng thư mục với runner.html. Có / ở đầu hay không sẽ quyết định nơi bắt đầu tìm.

URL bắt đầu bằng / luôn trỏ tới cùng một file, dù trang nằm trong thư mục nào. URL không có / thì vẫn dùng được khi bạn chuyển cả trang lẫn fixtures sang thư mục khác. Bảng dưới đây cho thấy mỗi URL bắt đầu tìm từ đâu và có tìm thấy file hay không.

URLBắt đầu từTìm thấy file?
fixtures/vi/orders.jsonjs-editor
/tools/js-editor/fixtures/vi/orders.jsonGốc của domainCó (cùng file)
orders.jsonjs-editorKhông (js-editor không có)

DevTools tính URL từ trang bài viết

Nếu mở Console của DevTools trên trang bài viết rồi chạy fetch("fixtures/vi/orders.json"), điểm bắt đầu sẽ là URL của trang bài viết, nên yêu cầu không bao giờ tới được fixtures. Server của trang bài viết trả về HTML của trang với status 200 kể cả cho đường dẫn không tồn tại, nên trông như đã lấy được dữ liệu, nhưng dữ liệu nhận về không phải JSON.

Trên màn hình quản trị của một cửa hàng đồ gia dụng, bạn cần tính tổng giá các sản phẩm. brokenPath chứa URL của danh sách sản phẩm nhưng bị thiếu thư mục fixtures/vi/.

① Hãy lấy dữ liệu bằng brokenPath và in ra status có phải 200 hay không (true / false).

② Hãy sửa URL để tính từ thư mục của trang, lấy dữ liệu và in ra số sản phẩm.

③ Hãy lấy dữ liệu bằng URL tính từ gốc của domain và in ra tổng giá.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Nhận biết yêu cầu thất bại — response.ok và status

Nếu bạn gõ sai URL hoặc file đã bị xóa, server sẽ trả lời rằng không tìm thấy. Vì vẫn có phản hồi trả về, fetch vẫn fulfilled với một Response. Kể cả khi đặt trong try / catch, code cũng không nhảy sang catch mà chạy tiếp dòng sau, tức dòng đọc body.

response.oktrue khi status nằm trong khoảng từ 200 đến 299. Mã thành công không chỉ có 200 (201 cũng là một mã thành công), nên hãy dựa vào ok để biết yêu cầu có thành công không. Với file không tồn tại, server thường trả về 404 (Not Found), nhưng server của console bài tập trả về 403 (Forbidden).

// URL gõ nhầm orders thành order
const response = await fetch("fixtures/vi/order.json");

// Dù không tìm thấy file, fetch vẫn fulfilled và trả về một Response
console.log(response.ok);       // false
console.log(response.status);   // 404 hoặc 403 (tùy server; console bài tập trả về 403)

// Chỉ chuyển body thành mảng khi ok là true
if (response.ok) {
  const orders = await response.json();
  console.log(`Đơn hàng: ${orders.length}`);
} else {
  console.log("Không tải được đơn hàng của bạn");   // Không tải được đơn hàng của bạn
}
Kiểm tra ok hay bỏ qua: ba kết quả
await fetch(url)vẫn fulfilledorders.jsonstatus 200order.jsonstatus 403 v.v.order.jsonstatus 403 v.v.ok là truenên gọi json()ok là falsekhông gọi json()Bỏ qua ok,gọi json() luônNhận mảng3 đơn hàngHiện thông báokhông tải đượcSyntaxError: bodykhông phải JSON
fetch vẫn fulfilled kể cả khi file không tồn tại. Gọi json() trên body không phải JSON sẽ gây ra SyntaxError.

Ở cột bên phải, body là nội dung mô tả lỗi chứ không phải JSON (trong console bài tập là XML), nên json() báo lỗi SyntaxError ngay tại dòng đó. Cột giữa kiểm tra ok trước và hiện thông báo mà không đọc body, nên khi yêu cầu thất bại, màn hình hiện một thông báo thay vì một lỗi.

Bạn tải danh sách sản phẩm và đánh giá để hiển thị trên trang sản phẩm. Phần đánh giá chưa được công khai.

① Hãy lấy fixtures/vi/products.json và in ra có thành công hay không (true / false).

② Hãy lấy fixtures/vi/reviews.json và in ra có thành công hay không.

③ Nếu ② thất bại, hãy in ra “Chưa có đánh giá nào”; nếu thành công, hãy in ra số đánh giá theo dạng “Đánh giá: 5”.

④ Nếu ① thành công, hãy chuyển body thành mảng và in ra tên sản phẩm có tồn kho bằng 0.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Lấy hai file cùng lúc — hàm async và Promise.all

Để hiển thị số thành viên cạnh số đơn hàng, màn hình quản trị cần hai file JSON. Hai file không phụ thuộc vào nhau, nên bạn tải chúng song song (bắt đầu nhiều việc cùng lúc rồi mới chờ) như trong bài async / await. Nhưng nếu gọi fetch hai lần rồi truyền kết quả cho Promise.all, giá trị bạn nhận về là hai Response chứ không phải body.

Nếu gói toàn bộ phần từ fetch đến json() vào một hàm async, Promise nhận được khi gọi hàm đó sẽ fulfilled với giá trị của body. Gọi hàm đó hai lần rồi chờ bằng Promise.all, bạn sẽ nhận được một mảng chứa giá trị của các body theo đúng thứ tự đã truyền vào.

// Gói phần từ fetch đến json() và trả về giá trị của body (null nếu không tìm thấy)
async function loadJson(path) {
  const response = await fetch(path);
  if (!response.ok) {
    return null;
  }
  return await response.json();
}

// Truyền hai lời gọi fetch thì nhận được các Response
const responses = await Promise.all([
  fetch("fixtures/vi/products.json"),
  fetch("fixtures/vi/orders.json"),
]);
console.log(responses[0].length);   // undefined (không phải mảng)

// Gọi loadJson hai lần thì cả hai bắt đầu cùng lúc, và Promise.all gom đủ mảng của các body
const [products, orders] = await Promise.all([
  loadJson("fixtures/vi/products.json"),
  loadJson("fixtures/vi/orders.json"),
]);
console.log(`Sản phẩm: ${products.length} / Đơn hàng: ${orders.length}`);   // Sản phẩm: 4 / Đơn hàng: 3
Kết quả của fetch không chứa body
Gọi fetch() hailần, truyền vàoXong khi cả haiResponse đã tớiNhận được2 Responseresponses[0].lengthlà undefinedGọi loadJson() hailần, truyền vàoMỗi lời gọi chờđến hết json()Nhận được2 mảngproducts.lengthlà 4
Hàng trên kết thúc ngay khi các Response tới. Muốn gom các body, hãy truyền kết quả của một hàm chờ đến hết json().

Chỉ cần một trong các Promise được truyền vào bị rejected, Promise.all sẽ thất bại mà không trả về kết quả nào khác. loadJson fulfilled với null khi okfalse, nên dù thiếu một file, bạn vẫn nhận được mảng còn lại.

Trên màn hình tổng hợp doanh thu, bạn tải cùng lúc thành viên, đơn hàng và đánh giá. Mảng đường dẫn paths đã được khai báo sẵn, và phần đánh giá chưa được công khai.

① Hãy viết hàm async loadList trả về giá trị của body khi thành công và mảng rỗng khi thất bại.

② Hãy đưa từng đường dẫn trong paths qua loadList, chờ tất cả cùng lúc, rồi in ra một dòng như “Thành viên: 4 / Đơn hàng: 3 / Đánh giá: 0”.

③ Hãy dùng userId để tìm thành viên đã đặt đơn hàng đầu tiên, rồi in ra tên người đó.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Sau khi chạy const response = await fetch(url);, response chứa gì?

Câu 2Với một đường dẫn không tồn tại, ngay sau khi chạy const res = await fetch(url); thì trạng thái sẽ thế nào?

Câu 3Trong code chạy ở /tools/js-editor/runner.html, fetch("users.json") sẽ tìm ở đâu?