Bài 7.5: Bài tập thực hành fetch dữ liệu API với JavaScript

Chào mừng bạn quay trở lại với chuỗi bài viết về Lập trình Web Front-end!

Trong thế giới web hiện đại, các trang web không còn là những tài liệu tĩnh chỉ hiển thị nội dung có sẵn. Chúng ngày càng trở nên động, tương táccá nhân hóa. Để làm được điều này, chúng ta cần có khả năng giao tiếp với các dịch vụ web bên ngoài để lấy về dữ liệu mới nhất, gửi thông tin người dùng, hoặc thực hiện các tác vụ trên máy chủ. Đây chính là lúc API (Application Programming Interface) và khả năng "fetch" dữ liệu của JavaScript phát huy sức mạnh!

Trong bài viết này, chúng ta sẽ cùng nhau thực hành cách sử dụng API fetch - một công cụ hiện đại và mạnh mẽ của JavaScript - để tải dữ liệu từ các nguồn bên ngoài. Hãy sẵn sàng để mang dữ liệu từ internet vào ứng dụng web của bạn!

API fetch Là Gì và Tại Sao Chúng Ta Sử Dụng Nó?

Trước đây, việc gửi các yêu cầu HTTP (như GET, POST) từ trình duyệt web thường được thực hiện bằng đối tượng XMLHttpRequest (thường gọi tắt là XHR). Mặc dù nó hoạt động, cú pháp của XHR khá rườm rà và khó quản lý, đặc biệt là khi xử lý các tác vụ bất đồng bộ phức tạp.

API fetch được giới thiệu để giải quyết vấn đề này. fetch cung cấp một cách thức hiện đại, linh hoạt và dựa trên Promise để thực hiện các yêu cầu mạng. Điều này giúp code của chúng ta trở nên sạch sẽ, dễ đọcdễ bảo trì hơn rất nhiều.

Khi bạn "fetch" dữ liệu từ một API, về cơ bản bạn đang gửi một yêu cầu đến một địa chỉ web cụ thể (URL). Địa chỉ này không trả về một trang web HTML để hiển thị, mà thường trả về dữ liệu thô ở định dạng có cấu trúc, phổ biến nhất là JSON. Nhiệm vụ của JavaScript là gửi yêu cầu đó, nhận phản hồi, xử lý dữ liệu nhận được (ví dụ: chuyển từ JSON sang đối tượng JavaScript) và sau đó sử dụng dữ liệu đó để cập nhật giao diện người dùng hoặc thực hiện các logic khác.

Bắt Đầu Với Yêu Cầu GET Đơn Giản Nhất

Yêu cầu GET là loại yêu cầu phổ biến nhất, được dùng để lấy dữ liệu từ máy chủ. API fetch làm cho việc này cực kỳ đơn giản.

Hãy xem ví dụ đầu tiên, chúng ta sẽ lấy dữ liệu của một bài viết từ một API mẫu công khai: JSONPlaceholder (một dịch vụ cung cấp các API giả để thử nghiệm).

// Đường dẫn API để lấy bài viết có ID là 1
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';

// Sử dụng fetch để gửi yêu cầu GET
fetch(apiUrl)
  // Bước 1: Xử lý phản hồi ban đầu (Response object)
  .then(response => {
    // Kiểm tra xem yêu cầu có thành công không (status code trong khoảng 200-299)
    if (!response.ok) {
      throw new Error(`Lỗi HTTP! Status: ${response.status}`);
    }
    // Trích xuất dữ liệu dạng JSON từ phản hồi.
    // response.json() cũng trả về một Promise.
    return response.json();
  })
  // Bước 2: Xử lý dữ liệu đã được parse từ JSON thành đối tượng JavaScript
  .then(data => {
    console.log('Dữ liệu bài viết:', data);
    // Bây giờ bạn có thể sử dụng đối tượng 'data' để làm gì đó,
    // ví dụ: hiển thị lên giao diện người dùng
    // document.getElementById('postTitle').innerText = data.title;
    // document.getElementById('postBody').innerText = data.body;
  })
  // Bước 3: Xử lý bất kỳ lỗi nào xảy ra trong quá trình fetch hoặc xử lý response
  .catch(error => {
    console.error('Có lỗi xảy ra khi fetch dữ liệu:', error);
    // Hiển thị thông báo lỗi cho người dùng
  });

Giải thích code:

  1. fetch(apiUrl): Đây là lời gọi hàm fetch. Nó nhận vào URL của API mà bạn muốn gửi yêu cầu. Mặc định, fetch sẽ gửi một yêu cầu GET. Hàm này ngay lập tức trả về một Promise.
  2. .then(response => { ... }): Khi fetch hoàn thành việc gửi yêu cầu và nhận được phản hồi ban đầu từ máy chủ, Promise sẽ được giải quyết (resolved). Phản hồi này là một đối tượng Response.
    • Chúng ta kiểm tra !response.ok để đảm bảo status code HTTP nằm trong khoảng 2xx (thành công). Nếu không, chúng ta ném ra một Error để chuyển sang block .catch(). Đây là một bước cực kỳ quan trọngfetch không tự động coi các mã lỗi HTTP (như 404 Not Found, 500 Internal Server Error) là lỗi làm Promise bị từ chối (rejected).
    • response.json(): Đối tượng Response chứa phản hồi thô. Để làm việc với dữ liệu JSON, chúng ta gọi phương thức json(). Quan trọng: response.json() cũng là một hàm bất đồng bộtrả về một Promise khác. Promise này sẽ giải quyết (resolve) với dữ liệu JSON đã được parse thành đối tượng JavaScript.
  3. .then(data => { ... }): Khi response.json() hoàn thành và parse dữ liệu JSON thành công, Promise thứ hai sẽ giải quyết. Dữ liệu đã parse này (trong ví dụ này là một đối tượng JavaScript chứa thông tin bài viết) sẽ được truyền vào callback này. Đây là nơi bạn sẽ xử lý dữ liệu thực tế - hiển thị nó, lưu trữ nó, v.v.
  4. .catch(error => { ... }): Nếu bất kỳ lỗi nào xảy ra trong quá trình gửi yêu cầu mạng ban đầu (ví dụ: không kết nối được internet) hoặc nếu bạn throw new Error trong bất kỳ block .then() nào (như chúng ta đã làm với lỗi HTTP), Promise sẽ bị từ chối (rejected), và block .catch() này sẽ được thực thi để xử lý lỗi đó.

Gửi Dữ Liệu Với Yêu Cầu POST

Ngoài việc lấy dữ liệu, bạn cũng cần có khả năng gửi dữ liệu lên máy chủ, ví dụ: tạo một bài viết mới, đăng ký người dùng, gửi form... Chúng ta sử dụng yêu cầu POST cho mục đích này.

Để gửi yêu cầu POST (hoặc các loại yêu cầu khác như PUT, PATCH, DELETE), chúng ta cần cung cấp một đối tượng cấu hình thứ hai cho hàm fetch().

const apiUrl = 'https://jsonplaceholder.typicode.com/posts'; // Endpoint để tạo bài viết mới

const newPostData = {
  title: 'Tiêu đề bài viết mới của tôi',
  body: 'Nội dung tuyệt vời của bài viết.',
  userId: 1,
};

fetch(apiUrl, {
  method: 'POST', // Xác định loại yêu cầu là POST
  headers: {
    // Cho máy chủ biết rằng chúng ta đang gửi dữ liệu dạng JSON
    'Content-Type': 'application/json',
    // Các headers khác có thể cần thiết (ví dụ: Authorization cho xác thực)
    // 'Authorization': 'Bearer your_token_here'
  },
  // Chuyển đối tượng JavaScript thành chuỗi JSON để gửi đi
  body: JSON.stringify(newPostData),
})
  .then(response => {
    if (!response.ok) {
      throw new Error(`Lỗi HTTP! Status: ${response.status}`);
    }
    // Máy chủ thường trả về dữ liệu của tài nguyên vừa được tạo
    return response.json();
  })
  .then(data => {
    console.log('Bài viết mới đã được tạo thành công:', data);
    // Dữ liệu trả về thường chứa ID của bài viết vừa tạo
  })
  .catch(error => {
    console.error('Có lỗi xảy ra khi tạo bài viết:', error);
  });

Giải thích code:

  1. fetch(apiUrl, { ... }): Chúng ta truyền URL và một đối tượng cấu hình.
  2. method: 'POST': Chỉ định rõ phương thức HTTP là POST. Nếu không có method, mặc định sẽ là GET.
  3. headers: { ... }: Một đối tượng chứa các HTTP header.
    • 'Content-Type': 'application/json': Header này cực kỳ quan trọng để cho máy chủ biết rằng dữ liệu trong phần body của yêu cầu là dạng JSON. Nếu thiếu hoặc sai, máy chủ có thể không hiểu dữ liệu bạn gửi.
    • Bạn có thể thêm các header khác tại đây, ví dụ như header xác thực (Authorization).
  4. body: JSON.stringify(newPostData): Phần body chứa dữ liệu mà bạn muốn gửi lên máy chủ.
    • newPostData là một đối tượng JavaScript.
    • JSON.stringify() chuyển đổi đối tượng JavaScript này thành một chuỗi JSON. Hầu hết các API hiện đại đều mong đợi dữ liệu gửi lên (trong các yêu cầu POST, PUT, PATCH) dưới dạng JSON.

Sau đó, quy trình xử lý response với .then().catch() tương tự như với yêu cầu GET, nhưng dữ liệu data trả về ở .then thứ hai sẽ là phản hồi của máy chủ sau khi xử lý yêu cầu POST (thường là dữ liệu của tài nguyên vừa được tạo, bao gồm cả ID mới).

Tối Giản Code Với async/await

Như bạn thấy, cách tiếp cận .then().then().catch() với Promise rất hiệu quả, nhưng đôi khi chuỗi .then() dài có thể làm giảm tính đọc hiểu. JavaScript cung cấp cú pháp async/await để viết code bất đồng bộ trông giống như code đồng bộ, giúp code dễ đọc và dễ viết hơn nhiều.

Hãy viết lại ví dụ GET đầu tiên bằng async/await:

const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';

// Định nghĩa một hàm bất đồng bộ bằng từ khóa 'async'
async function fetchPostData() {
  // Sử dụng try...catch để xử lý lỗi trong async/await
  try {
    // 'await' tạm dừng việc thực thi hàm cho đến khi Promise được giải quyết.
    // fetch() trả về Promise giải quyết với Response object.
    const response = await fetch(apiUrl);

    // Vẫn cần kiểm tra response.ok cho lỗi HTTP
    if (!response.ok) {
      throw new Error(`Lỗi HTTP! Status: ${response.status}`);
    }

    // 'await' tiếp tục tạm dừng cho đến khi response.json() hoàn thành
    // và parse dữ liệu.
    const data = await response.json();

    console.log('Dữ liệu bài viết (async/await):', data);
    // Sử dụng dữ liệu 'data' ở đây
    return data; // Có thể trả về dữ liệu từ hàm async
  } catch (error) {
    // Xử lý bất kỳ lỗi nào xảy ra (mạng, HTTP, parse JSON)
    console.error('Có lỗi xảy ra khi fetch dữ liệu (async/await):', error);
    // Bạn có thể ném lại lỗi hoặc trả về null/undefined tùy ý
    throw error; // Ném lại lỗi để hàm gọi bên ngoài có thể bắt
  }
}

// Gọi hàm async
fetchPostData();

// Hoặc nếu bạn muốn sử dụng giá trị trả về:
/*
fetchPostData()
  .then(data => {
    console.log('Nhận được dữ liệu từ hàm async:', data);
  })
  .catch(error => {
    console.error('Hàm async đã báo lỗi:', error);
  });
*/

Giải thích code:

  1. async function fetchPostData() { ... }: Từ khóa async đặt trước định nghĩa hàm báo hiệu rằng hàm này sẽ thực thi bất đồng bộ và có thể sử dụng từ khóa await bên trong. Hàm async luôn trả về một Promise.
  2. try { ... } catch (error) { ... }: Với async/await, chúng ta sử dụng khối try...catch để xử lý lỗi. Bất kỳ lỗi nào xảy ra bên trong khối try (bao gồm cả lỗi từ các Promise bị từ chối hoặc lỗi chúng ta tự ném ra) sẽ được bắt bởi khối catch.
  3. const response = await fetch(apiUrl);: Từ khóa await chỉ có thể được sử dụng bên trong hàm async. Nó "đợi" cho Promise mà nó đứng trước (ở đây là Promise từ fetch(apiUrl)) được giải quyết. Khi Promise giải quyết, giá trị giải quyết (Response object) sẽ được gán cho biến response. Code sẽ tạm dừng tại dòng này cho đến khi nhận được phản hồi ban đầu.
  4. if (!response.ok) { ... }: Việc kiểm tra lỗi HTTP vẫn cần thiết như trước.
  5. const data = await response.json();: Tương tự, chúng ta await Promise trả về từ response.json() để đợi cho dữ liệu JSON được parse hoàn chỉnh. Dữ liệu parse được gán cho biến data.
  6. Code sau dòng await response.json() sẽ chỉ chạy khi dữ liệu đã sẵn sàng. Điều này tạo cảm giác code chạy theo trình tự đồng bộ, mặc dù thực tế nó đang đợi các thao tác bất đồng bộ hoàn thành.

Cú pháp async/await thường được ưa chuộng vì nó làm cho code bất đồng bộ trở nên mạch lạc và dễ theo dõi luồng thực thi hơn, đặc biệt khi xử lý nhiều thao tác bất đồng bộ nối tiếp nhau.

Một Vài Lưu Ý Quan Trọng Khi Sử Dụng fetch

  • CORS (Cross-Origin Resource Sharing): Đây là một vấn đề bảo mật phổ biến khi bạn cố gắng fetch dữ liệu từ một domain khác với domain chứa trang web của bạn. Trình duyệt sẽ kiểm tra xem máy chủ API có cho phép yêu cầu từ domain của bạn hay không thông qua các header CORS. Nếu không được cấu hình đúng, trình duyệt sẽ chặn yêu cầu, và bạn sẽ thấy lỗi liên quan đến CORS trong console. Việc cấu hình CORS đúng nằm ở phía máy chủ cung cấp API, nhưng bạn cần hiểu về nó để xử lý lỗi khi gặp phải.
  • Xử lý Lỗi Toàn Diện: Luôn luôn bao gồm .catch() (hoặc try...catch với async/await) để xử lý các trường hợp lỗi (lỗi mạng, lỗi HTTP, lỗi parse dữ liệu). Hãy thông báo cho người dùng nếu có lỗi xảy ra thay vì để ứng dụng "treo" hoặc hiển thị sai.
  • Headers: Chú ý đến các header cần thiết, đặc biệt là Content-Type khi gửi dữ liệu và Authorization khi cần xác thực.
  • Trạng Thái Loading: Khi fetch dữ liệu, có một khoảng thời gian chờ đợi phản hồi từ máy chủ. Đối với các ứng dụng thực tế, bạn nên hiển thị trạng thái "đang tải" (loading) cho người dùng để cải thiện trải nghiệm. Bạn có thể dùng các biến trạng thái (state) trong JavaScript để quản lý điều này.
  • Hủy Yêu Cầu (AbortController): Đối với các yêu cầu kéo dài hoặc khi người dùng chuyển hướng trang, việc có thể hủy yêu cầu đang chờ là quan trọng để tránh lãng phí tài nguyên và ngăn chặn các callback cũ chạy trên giao diện mới. API fetch hỗ trợ AbortController để làm điều này, nhưng nó là một chủ đề nâng cao hơn.

Thực Hành: Tải Danh Sách Dữ Liệu

Thay vì chỉ tải một mục dữ liệu, hãy thử tải về một danh sách các bài viết từ API JSONPlaceholder.

const listApiUrl = 'https://jsonplaceholder.typicode.com/posts'; // Endpoint lấy danh sách bài viết

async function fetchAllPosts() {
  try {
    const response = await fetch(listApiUrl);

    if (!response.ok) {
      throw new Error(`Lỗi HTTP! Status: ${response.status}`);
    }

    // response.json() sẽ parse mảng JSON thành mảng các đối tượng JavaScript
    const posts = await response.json();

    console.log('Danh sách bài viết:', posts);
    // Bây giờ bạn có thể lặp qua mảng 'posts' để hiển thị từng bài viết
    // Ví dụ: posts.forEach(post => console.log(post.title));

    return posts;

  } catch (error) {
    console.error('Có lỗi xảy ra khi fetch danh sách bài viết:', error);
    throw error;
  }
}

fetchAllPosts(); // Gọi hàm để chạy

Trong ví dụ này, await response.json() sẽ chuyển đổi phản hồi JSON (mà trong trường hợp này là một mảng các đối tượng bài viết) thành một mảng các đối tượng JavaScript. Bạn có thể dễ dàng lặp qua mảng này và xử lý từng đối tượng con.

Tổng Kết

Chúc mừng! Bạn đã làm quen và thực hành các thao tác cơ bản nhưng cực kỳ quan trọng với API fetch trong JavaScript. Chúng ta đã tìm hiểu cách:

  • Gửi yêu cầu GET để lấy dữ liệu.
  • Xử lý phản hồi ban đầu và kiểm tra lỗi HTTP.
  • Parse dữ liệu JSON nhận được.
  • Sử dụng .then().catch() để xử lý Promise.
  • Gửi yêu cầu POST để gửi dữ liệu lên máy chủ, bao gồm cấu hình method, headers, và body.
  • Tối ưu hóa code bất đồng bộ bằng cú pháp async/awaittry...catch.
  • Lưu ý một vài điểm quan trọng như CORS và xử lý lỗi.

Khả năng tương tác với API là một kỹ năng cốt lõi của một lập trình viên Front-end hiện đại. fetch API là công cụ tiêu chuẩn để làm điều đó. Hãy tiếp tục luyện tập với các API công khai khác để nắm vững kỹ năng này nhé! Bạn có thể tìm thêm nhiều API mẫu trên mạng bằng cách tìm kiếm "public APIs for testing".

Hẹn gặp lại bạn trong các bài viết tiếp theo!

Comments

There are no comments at the moment.