Bài 7.3: Fetch API và AJAX

Bài 7.3: Fetch API và AJAX
Chào mừng trở lại với chuỗi bài viết của chúng ta về lập trình Front-end hiện đại! Ở các bài trước, chúng ta đã xây dựng nên giao diện tĩnh bằng HTML và làm cho nó trở nên sống động hơn với CSS và JavaScript. Tuy nhiên, thế giới Web thực tế hiếm khi đứng yên. Các ứng dụng hiện đại cần tương tác với máy chủ để lấy dữ liệu mới, gửi thông tin người dùng, mà không cần phải tải lại toàn bộ trang. Đây chính là lúc sức mạnh của việc giao tiếp bất đồng bộ phát huy tác dụng, và AJAX cùng Fetch API là hai công cụ chủ chốt giúp chúng ta làm điều đó.
Bạn đã bao giờ thấy một trang web cập nhật nội dung mới mà không bị nháy trắng hay tải lại toàn bộ chưa? Đó chính là nhờ kỹ thuật này! Thay vì gửi một yêu cầu tới máy chủ, đợi máy chủ xử lý, gửi lại toàn bộ trang HTML mới và trình duyệt phải vẽ lại từ đầu, chúng ta chỉ cần gửi một yêu cầu nhỏ để lấy hoặc gửi dữ liệu, sau đó dùng JavaScript để cập nhật một phần của trang web. Điều này mang lại trải nghiệm người dùng nhanh hơn, mượt mà hơn và tiết kiệm băng thông đáng kể.
AJAX là gì? Hiểu về khái niệm Bất đồng bộ
Thuật ngữ AJAX (viết tắt của Asynchronous JavaScript and XML) thực chất không phải là một công nghệ duy nhất, mà là một tập hợp các kỹ thuật sử dụng nhiều công nghệ Web khác nhau để tạo ra các ứng dụng Web bất đồng bộ. Cốt lõi của AJAX là khả năng gửi yêu cầu và nhận phản hồi từ máy chủ một cách ngầm (trong nền), không làm gián đoạn hoạt động hiện tại của người dùng trên trang web.
Ban đầu, tên gọi có chữ "XML" vì XML là định dạng dữ liệu phổ biến để trao đổi thông tin. Tuy nhiên, ngày nay, JSON (JavaScript Object Notation) đã trở thành định dạng dữ liệu được ưa chuộng hơn rất nhiều do tính gọn nhẹ và dễ làm việc với JavaScript. Vì vậy, dù tên gọi là AJAX, chúng ta thường trao đổi dữ liệu dưới định dạng JSON.
Công cụ nguyên thủy và "linh hồn" ban đầu của AJAX trong JavaScript là đối tượng XMLHttpRequest
(viết tắt là XHR).
Làm việc với XMLHttpRequest
(Phiên bản "Cổ điển")
XMLHttpRequest
là API gốc được sử dụng để thực hiện các yêu cầu HTTP bất đồng bộ trong trình duyệt. Mặc dù vẫn còn được hỗ trợ và đôi khi bạn vẫn có thể gặp trong các codebase cũ, cú pháp của nó khá dài dòng và dựa trên các callback để xử lý các sự kiện (như khi yêu cầu hoàn thành, khi có lỗi...).
Hãy xem một ví dụ đơn giản về việc lấy dữ liệu từ một API công khai bằng XMLHttpRequest
:
// Example 1: The classic XMLHttpRequest
console.log('Đang gửi yêu cầu bằng XHR...');
const xhr = new XMLHttpRequest(); // 1. Tạo một đối tượng XMLHttpRequest
// 2. Cấu hình yêu cầu: Phương thức GET, URL, và true cho chế độ bất đồng bộ
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
// 3. Định nghĩa hàm xử lý khi yêu cầu hoàn thành thành công
xhr.onload = function() {
// Kiểm tra mã trạng thái HTTP (2xx là thành công)
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Yêu cầu XHR thành công!');
// Dữ liệu nhận được là chuỗi JSON, cần parse nó thành đối tượng JavaScript
const data = JSON.parse(xhr.responseText);
console.log('Dữ liệu nhận được:', data);
} else {
// Xử lý các mã lỗi HTTP (ví dụ: 404 Not Found, 500 Internal Server Error)
console.error('Yêu cầu XHR thất bại với mã trạng thái:', xhr.status);
console.error('Nội dung lỗi:', xhr.statusText);
}
};
// 4. Định nghĩa hàm xử lý khi có lỗi mạng
xhr.onerror = function() {
console.error('Đã xảy ra lỗi mạng khi gửi yêu cầu XHR.');
};
// 5. Gửi yêu cầu
xhr.send();
console.log('Đã gọi lệnh gửi yêu cầu XHR (nhưng nó chạy bất đồng bộ)');
Giải thích code XHR:
- Chúng ta tạo một đối tượng
XMLHttpRequest
. - Sử dụng phương thức
open()
để thiết lập loại yêu cầu (GET), URL đích và chế độ bất đồng bộ (true
). - Gán các hàm xử lý cho các sự kiện:
onload
(khi yêu cầu hoàn thành, dù thành công hay thất bại về mặt HTTP) vàonerror
(khi có lỗi mạng). - Bên trong
onload
, chúng ta kiểm traxhr.status
để xác định yêu cầu có thực sự thành công ở cấp độ HTTP hay không (các mã 2xx). xhr.responseText
chứa dữ liệu phản hồi từ máy chủ dưới dạng chuỗi. Chúng ta dùngJSON.parse()
để chuyển nó thành đối tượng JavaScript nếu biết chắc phản hồi là JSON.- Cuối cùng,
xhr.send()
thực thi yêu cầu. Do nó chạy bất đồng bộ, các lệnhconsole.log
sauxhr.send()
sẽ chạy trước khi nhận được phản hồi từ máy chủ.
Bạn có thể thấy, ngay cả với một yêu cầu đơn giản, code XHR cũng khá dài dòng và việc quản lý nhiều callback có thể trở nên phức tạp, đặc biệt khi cần thực hiện nhiều yêu cầu liên tiếp (dẫn đến tình trạng "callback hell").
Fetch API: Tương lai của việc gửi yêu cầu HTTP
Để khắc phục những hạn chế của XMLHttpRequest
và cung cấp một API hiện đại hơn, dễ sử dụng hơn, dựa trên Promise, Fetch API đã ra đời và nhanh chóng trở thành chuẩn mực mới.
Fetch API cung cấp một giao diện mạnh mẽ và linh hoạt hơn nhiều để làm việc với các yêu cầu HTTP. Lợi thế lớn nhất của nó là việc sử dụng Promise, giúp chúng ta viết code bất đồng bộ mạch lạc và dễ đọc hơn, đặc biệt khi kết hợp với async/await
.
Ví dụ cơ bản với Fetch (sử dụng .then()
)
Hãy tái tạo ví dụ lấy dữ liệu trên bằng Fetch API:
// Example 2: Basic Fetch API (using .then())
console.log('Đang gửi yêu cầu bằng Fetch...');
fetch('https://jsonplaceholder.typicode.com/todos/1') // 1. Gọi fetch với URL đích
.then(response => {
console.log('Đã nhận phản hồi từ Fetch.');
// 2. Fetch chỉ reject Promise khi có lỗi mạng.
// Các lỗi HTTP (như 404, 500) vẫn coi là thành công ở cấp độ Promise.
// Cần kiểm tra thuộc tính .ok hoặc .status.
if (!response.ok) {
// Nếu có lỗi HTTP, ném ra một lỗi để chuyển xuống .catch()
throw new Error(`Lỗi HTTP! Mã trạng thái: ${response.status}`);
}
// 3. Phản hồi.json() cũng trả về một Promise, parse body thành JSON
return response.json();
})
.then(data => {
// 4. Dữ liệu JSON đã được parse
console.log('Yêu cầu Fetch thành công!');
console.log('Dữ liệu nhận được:', data);
})
.catch(error => {
// 5. Xử lý bất kỳ lỗi nào xảy ra trong quá trình fetch hoặc xử lý Promise
console.error('Đã xảy ra lỗi khi fetch:', error);
});
console.log('Đã gọi lệnh gửi yêu cầu Fetch (nhưng nó chạy bất đồng bộ)');
Giải thích code Fetch (.then()):
- Gọi hàm
fetch()
với URL đích. Hàm này ngay lập tức trả về một Promise. - Chúng ta sử dụng
.then()
để xử lý khi Promise được resolve (thành công ở cấp độ mạng)..then()
đầu tiên nhận đối tượngResponse
. - Điểm quan trọng khác biệt so với XHR: Fetch chỉ reject Promise khi có lỗi mạng (ví dụ: mất kết nối, CORS bị chặn). Nó không reject Promise khi máy chủ trả về các mã trạng thái lỗi HTTP như 404 (Not Found) hay 500 (Internal Server Error). Do đó, chúng ta phải tự kiểm tra thuộc tính
response.ok
(true nếu status code là 2xx) hoặcresponse.status
để phát hiện lỗi HTTP. Nếu có lỗi, chúng ta chủ động dùngthrow new Error()
để đẩy lỗi xuống khối.catch()
. - Đối tượng
Response
có các phương thức bất đồng bộ khác nhau để lấy body của phản hồi theo định dạng mong muốn, ví dụ:response.json()
(trả về Promise parse body thành JSON),response.text()
(trả về Promise parse body thành chuỗi),response.blob()
, v.v. .then()
thứ hai nhận dữ liệu đã được parse (trong trường hợp này là đối tượng JavaScript từ JSON)..catch()
bắt mọi lỗi xảy ra trong chuỗi Promise, bao gồm cả lỗi mạng ban đầu và các lỗi HTTP mà chúng ta chủ động ném ra.
Cú pháp này trông gọn gàng hơn nhiều so với XHR, đặc biệt là khi xử lý các phản hồi bất đồng bộ nhờ cấu trúc Promise.
Làm việc với Đối tượng Response
Đối tượng Response
mà chúng ta nhận được trong .then()
đầu tiên của Fetch chứa rất nhiều thông tin hữu ích về phản hồi từ máy chủ, không chỉ riêng phần dữ liệu (body). Một số thuộc tính và phương thức phổ biến bao gồm:
ok
: Boolean,true
nếustatus
nằm trong khoảng 200-299. Rất hữu ích để kiểm tra thành công.status
: Số, mã trạng thái HTTP (ví dụ: 200, 404, 500).statusText
: Chuỗi, mô tả ngắn gọn về mã trạng thái (ví dụ: "OK", "Not Found").headers
: Một đối tượngHeaders
chứa tất cả các header của phản hồi.url
: Chuỗi, URL cuối cùng mà yêu cầu đã được gửi tới (có thể khác với URL gốc nếu có redirect).json()
: Phương thức bất đồng bộ trả về Promise phân tích body thành JSON.text()
: Phương thức bất đồng bộ trả về Promise phân tích body thành chuỗi văn bản.blob()
: Phương thức bất đồng bộ trả về Promise phân tích body thành đối tượngBlob
(thường dùng cho file nhị phân).formData()
: Phương thức bất đồng bộ trả về Promise phân tích body dưới dạngFormData
.arrayBuffer()
: Phương thức bất đồng bộ trả về Promise phân tích body thànhArrayBuffer
(thường dùng cho dữ liệu nhị phân thô).
Nhớ rằng các phương thức json()
, text()
, v.v., đều trả về Promise, nên bạn cần dùng .then()
tiếp theo hoặc await
để lấy được dữ liệu thực sự.
Fetch API với async/await
Cách làm việc với Promise trở nên cực kỳ dễ đọc và gọn gàng hơn nữa khi chúng ta sử dụng cú pháp async/await
. Đây là cách hiện đại và được khuyến khích sử dụng nhất hiện nay.
// Example 3: Fetch API with async/await
console.log('Đang gửi yêu cầu bằng Fetch với async/await...');
async function fetchDataAsync() { // 1. Định nghĩa một hàm async
try { // 2. Sử dụng try...catch để bắt lỗi
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); // 3. Dùng await để chờ Promise fetch resolved
console.log('Đã nhận phản hồi từ Fetch (async/await).');
if (!response.ok) { // 4. Vẫn kiểm tra lỗi HTTP
throw new Error(`Lỗi HTTP! Mã trạng thái: ${response.status}`);
}
const data = await response.json(); // 5. Dùng await để chờ Promise response.json() resolved
console.log('Yêu cầu Fetch thành công (async/await)!');
console.log('Dữ liệu nhận được:', data);
} catch (error) { // 6. Bắt lỗi (cả lỗi mạng và lỗi HTTP chúng ta ném ra)
console.error('Đã xảy ra lỗi khi fetch (async/await):', error);
}
}
fetchDataAsync(); // 7. Gọi hàm async
console.log('Đã gọi hàm fetchDataAsync (nhưng nó chạy bất đồng bộ)');
Giải thích code Fetch (async/await):
- Chúng ta bọc code trong một hàm được đánh dấu bằng từ khóa
async
. Điều này cho phép sử dụngawait
bên trong hàm đó. - Khối
try...catch
là cách tiêu chuẩn để xử lý lỗi trong codeasync/await
. Bất kỳ lỗi nào xảy ra (bao gồm cả việc ném lỗi bằngthrow
) sẽ được bắt bởicatch
. - Từ khóa
await
chỉ có thể sử dụng bên trong hàmasync
. Nó tạm dừng việc thực thi của hàmasync
cho đến khi Promise mà nó đứng trước được resolve. Giá trị trả về của Promise sẽ được gán vào biến. - Chúng ta
await fetch(url)
để nhận về đối tượngResponse
. - Chúng ta vẫn cần kiểm tra
response.ok
hoặcresponse.status
để xử lý các lỗi HTTP. Nếu có lỗi, chúng tathrow
mộtError
mới, và lỗi này sẽ được bắt bởi khốicatch
. - Chúng ta
await response.json()
để chờ quá trình parse body thành JSON hoàn thành và gán kết quả vào biếndata
. - Nếu mọi thứ thành công, code tiếp tục chạy sau
await response.json()
. Nếu có lỗi ở bất kỳ bước nào, luồng điều khiển sẽ nhảy ngay đến khốicatch
.
Cách viết với async/await
thường được coi là cách rõ ràng nhất, giúp code bất đồng bộ nhìn gần giống với code đồng bộ thông thường, rất dễ đọc và bảo trì.
Gửi dữ liệu với Fetch (Ví dụ POST request)
Fetch API không chỉ dùng để lấy dữ liệu (GET). Nó cũng rất mạnh mẽ để gửi dữ liệu đến máy chủ bằng các phương thức HTTP khác như POST, PUT, DELETE, v.v. Để làm điều này, chúng ta truyền thêm một đối tượng cấu hình (options object) làm tham số thứ hai cho hàm fetch()
.
Đối tượng cấu hình này có thể chứa nhiều thuộc tính như method
, headers
, body
, mode
, cache
, v.v. Phổ biến nhất là method
, headers
, và body
.
// Example 4: Fetch API POST request
console.log('Đang gửi yêu cầu POST bằng Fetch...');
async function postData() {
const dataToSend = { // Dữ liệu chúng ta muốn gửi (dạng đối tượng JS)
title: 'Bài viết mới từ Fetch API',
body: 'Đây là nội dung của bài viết được gửi đi.',
userId: 99,
};
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', { // 1. URL đích (endpoint POST)
method: 'POST', // 2. Phương thức HTTP
headers: { // 3. Các Header của yêu cầu
'Content-Type': 'application/json', // Quan trọng: Nói cho máy chủ biết chúng ta đang gửi JSON
// Có thể thêm các header khác như 'Authorization', 'X-Requested-With', v.v.
},
body: JSON.stringify(dataToSend), // 4. Body của yêu cầu: Chuyển đối tượng JS thành chuỗi JSON
});
console.log('Đã nhận phản hồi từ yêu cầu POST.');
if (!response.ok) {
throw new Error(`Lỗi HTTP khi POST! Mã trạng thái: ${response.status}`);
}
const result = await response.json(); // 5. Máy chủ thường phản hồi lại với dữ liệu đã được tạo
console.log('Yêu cầu POST thành công!');
console.log('Dữ liệu máy chủ trả về:', result); // Thường chứa ID của resource vừa tạo
} catch (error) {
console.error('Đã xảy ra lỗi khi POST:', error);
}
}
postData(); // Gọi hàm postData async
Giải thích code Fetch (POST):
- Chúng ta chuẩn bị dữ liệu cần gửi trong một đối tượng JavaScript (
dataToSend
). - Gọi
fetch()
với URL đích và thêm đối tượng cấu hình{ ... }
làm tham số thứ hai. method: 'POST'
chỉ định đây là yêu cầu POST. Các phương thức khác có thể là'PUT'
,'DELETE'
, v.v. (mặc định là'GET'
).headers
: Đây là một đối tượng chứa các header HTTP. Header'Content-Type': 'application/json'
là cực kỳ quan trọng khi bạn gửi dữ liệu JSON, nó cho máy chủ biết cách xử lý body của yêu cầu.body
: Đây là dữ liệu thực tế bạn muốn gửi. Với Fetch,body
phải là một chuỗi,Blob
,BufferSource
,FormData
hoặcURLSearchParams
. Vì chúng ta gửi dữ liệu JSON, chúng ta dùngJSON.stringify()
để chuyển đối tượng JavaScriptdataToSend
thành một chuỗi JSON.- Các bước xử lý phản hồi và lỗi (
await response.json()
, kiểm traresponse.ok
,try...catch
) tương tự như yêu cầu GET. Máy chủ khi nhận yêu cầu POST thành công thường sẽ trả về dữ liệu của resource vừa được tạo (ví dụ: bao gồm cả ID mới được gán).
Xử lý lỗi chuyên sâu hơn
Như đã nhắc, việc kiểm tra response.ok
hoặc response.status
là bắt buộc với Fetch để xử lý các lỗi HTTP. Bạn có thể mở rộng phần xử lý lỗi trong khối catch
để cung cấp thông tin chi tiết hơn cho người dùng hoặc log lỗi đầy đủ hơn.
Ví dụ, bạn có thể cố gắng đọc body của phản hồi ngay cả khi có lỗi để xem máy chủ có trả về thông báo lỗi cụ thể nào không:
async function fetchDataWithErrorHandling() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/non-existent-resource'); // URL sẽ gây lỗi 404
if (!response.ok) {
console.error(`Lỗi HTTP! Mã trạng thái: ${response.status}`);
// Thử đọc body của lỗi nếu có
try {
const errorBody = await response.text(); // Hoặc response.json() nếu bạn biết chắc format
console.error('Nội dung lỗi từ máy chủ:', errorBody);
} catch (bodyError) {
console.error('Không thể đọc nội dung lỗi từ máy chủ.');
}
throw new Error(`Yêu cầu thất bại với mã trạng thái: ${response.status}`); // Ném lỗi để catch bên ngoài xử lý
}
const data = await response.json();
console.log('Dữ liệu nhận được:', data);
} catch (error) {
console.error('Đã xảy ra lỗi khi fetch:', error);
}
}
fetchDataWithErrorHandling();
Trong ví dụ này, nếu response.ok
là false
, chúng ta log mã trạng thái và cố gắng đọc body của phản hồi để xem có thông báo lỗi chi tiết từ máy chủ hay không trước khi ném lỗi chính ra ngoài.
So sánh ngắn gọn: XHR vs Fetch
Đặc điểm | XMLHttpRequest (AJAX "cổ điển") |
Fetch API |
---|---|---|
API | Dựa trên Event và Callbacks. | Dựa trên Promise. |
Cú pháp | Dài dòng, phân tán (open, set headers, event listeners, send...). | Ngắn gọn, dễ đọc hơn, đặc biệt với async/await . |
Xử lý lỗi | Sự kiện onerror cho lỗi mạng, kiểm tra status trong onload . |
Promise reject chỉ khi lỗi mạng; phải tự kiểm tra response.ok cho lỗi HTTP. |
Body | Truy cập qua responseText , responseXML . |
Truy cập qua các phương thức Promise-based: json() , text() , v.v. |
Tính năng | Ít linh hoạt hơn cho các yêu cầu phức tạp, không hỗ trợ streaming. | Hỗ trợ tốt hơn cho các tính năng HTTP hiện đại, dễ làm việc với Streams. |
Tương thích | Rất tốt, được hỗ trợ rộng rãi trên mọi trình duyệt cũ/mới. | Tốt trên các trình duyệt hiện đại; cần polyfill cho trình duyệt cũ hơn (hiếm khi cần thiết trong phát triển mới). |
Trong phát triển Web hiện đại, Fetch API là lựa chọn được khuyến khích do cú pháp Promise-based dễ đọc, dễ bảo trì và khả năng xử lý mạnh mẽ hơn. Tuy nhiên, việc hiểu về AJAX và XMLHttpRequest
vẫn quan trọng để nắm bắt lịch sử và nguyên lý hoạt động của việc giao tiếp bất đồng bộ trên Web.
Việc làm chủ Fetch API là một kỹ năng thiết yếu cho bất kỳ nhà phát triển Front-end nào ngày nay, giúp bạn xây dựng các ứng dụng động, responsive và có trải nghiệm người dùng tuyệt vời bằng cách tương tác hiệu quả với các API Backend.
Hãy thực hành với Fetch API trong các dự án của bạn. Thử lấy dữ liệu từ các API công khai, hiển thị chúng trên trang web, hoặc tạo các form gửi dữ liệu lên máy chủ. Đó là cách tốt nhất để nắm vững công cụ mạnh mẽ này!
Comments