Câu 1Sau khi chạy const response = await fetch(url);, response chứa gì?
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
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 filter và map.
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().
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
- URL bắt đầu bằng
/được tính từ đây
runner.html— trang nơi code bài tập chạy- URL không có
/ở đầu được tính từ đây
- Chứa
orders.json,users.jsonvàproducts.json
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.
| URL | Bắt đầu từ | Tìm thấy file? |
|---|---|---|
| fixtures/vi/orders.json | js-editor | Có |
| /tools/js-editor/fixtures/vi/orders.json | Gốc của domain | Có (cùng file) |
| orders.json | js-editor | Khô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.
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.ok là true 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
}
Ở 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.
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
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 ok là false, nên dù thiếu một file, bạn vẫn nhận được mảng còn lại.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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?