Web nâng cao (10) - JavaScript cho React (9) - Fetch_Async_Await

Bài trước: Web nâng cao (9) - JavaScript cho React (8) - Promise

-----

1.1.1       Fetch API căn bản

Như đã đề cập ở các phần trước, để trao đổi thông tin giữa trình duyệt và server, bạn đã được làm quen với đối tượng XMLHttpRequest của JavaScript. Có một cách khác để trình duyệt có thể trao đổi thông tin với server là sử dụng Fetch API.

Fetch có nghĩa là chạy đến và mang về một cái gì đó, do vậy, có thể tạm hiểu trình duyệt sẽ dùng fetch để kết nối đến server và mang thông tin về. API là viết tắt của Application Programming Interface, là công cụ (phương thức, hàm) dùng để lập trình giao tiếp giữa các phần mềm, thiết bị với nhau.

Fetch API là một thành phần của JavaScript, được sử dụng để trao đổi thông tin giữa client-server. Bạn có thể sử dụng Fetch API để tạo HTTP request, nhằm trao đổi dữ liệu giữa trình duyệt web và server.

Fetch API hoạt động dưới hình thức một promise; cho phép trao đổi thông tin theo kiểu bất đồng bộ. Với Fetch API, việc lập trình giao tiếp giữa trình duyệt và server được thực hiện đơn giản, rõ ràng và linh hoạt hơn so với XMLHttpRequest. Hầu hết các trình duyệt đều đã hỗ trợ Fetch API.

Fetch là một phương thức toàn cục của JavaScript, do vậy bạn có thể kiểm tra bằng đoạn mã sau:

console.log('fetch' in window);

    // true

Fetch hoạt động dưới dạng một promise, nên nó sẽ tồn tại ở một trong 3 trạng thái: trạng thái chờ (pending), trạng thái thực hiện thành công, và trạng thái thực hiện thất bại.

Do fetch là phương thức có sẵn của JavaScript nên bạn không cần quan tâm tới phần định nghĩa promise của fetch, mà chỉ quan tâm tới quá trình gọi fetch để thực thi và xử lý kết quả do fetch trả về.

Để tạo một request, bạn sử dụng cú pháp sau:

fetch(url);

Trong đó, url chính là địa chỉ của server; trạng thái của fetch khi mới được tạo ra sẽ ở trạng thái “chờ”. Ví dụ:

console.log(fetch("https://jsonplaceholder.typicode.com/todos/1"));

    // pending

Nếu promise thực hiện thành công (resolved), khi đó hàm callback trong then() sẽ được thực thi:

fetch(url)

    .then( (response) => {

      // đoạn mã xử lý cho trường hợp resolve được gọi

    })

Nếu promise thực hiện thất bại (rejected), khi đó hàm callback trong catch() sẽ được thực thi:

fetch(url)

    .then( (response) => {

      // đoạn mã xử lý cho trường hợp fetch thành công (resolve)

    })

    .catch( (error) => {

      // đoạn mã xử lý cho trường hợp fetch thất bại

    });

Ví dụ, sử dụng fetch để lấy thông tin người dùng trên mạng:

 fetch("https://jsonplaceholder.typicode.com/todos/1")

    .then( (response) => {

      console.log("lay duoc du lieu", response);

    })

    .catch( (error) => {

      console.log("bi loi", error);

    });

Bạn có thể sửa URL thành một đường dẫn không tồn tại (ví dụ : https://jsonplaceholder.typicode.com/todosabc/1), để fetch trả về lỗi? Nghĩa là phương thức reject sẽ được thực thi? Tuy nhiên, kết quả không phải vậy. Phương thức resolve vẫn được gọi và hàm callback trong catch không được thực thi. Tại sao có hiện tượng này?

Nếu bạn mở đối tượng response trong cửa sổ console của trình duyệt, bạn sẽ thấy trường status có giá trị là 404, nghĩa là không tìm thấy tài nguyên. Vậy, phương thức reject được thực thi khi nào? Thực tế nó chỉ được gọi khi mạng bị lỗi hoặc trình duyệt không thể gọi API vì một lý do nào đó. Bạn thử ngắt kết nối mạng và chạy lại đoạn mã để có được trạng thái reject của fetch.

Do fetch là một đối tượng, nên ngoài các phương thức, nó còn có các thuộc tính đi kèm, như response.ok, response.status; dựa vào giá trị của các thuộc tính này, bạn có thêm thông tin trước khi thực hiện các xử lý tiếp theo. Ví dụ, thuộc tính response.ok sẽ có giá trị là true nếu mã trạng thái của server trả về (status) là 200-299, ngược lại là false; response.status sẽ trả về mã trạng thái do server trả về. Bạn thử với  các URL khác nhau (đúng và sai) để xem kết quả trả về của response.status và response.ok trong đoạn mã sau:

   ...

    fetch("https://jsonplaceholder.typicode.com/todos/1")

    .then( (response) => {

      console.log(response.status);

      console.log(response.ok);

    })

   ...

Bạn nên kiểm tra response.ok là true thì mới thực hiện các xử lý với dữ liệu từ server trả về:

...

    fetch("https://jsonplaceholder.typicode.com/todosa/1")

    .then( (response) => {

      if(!response.ok) {

        console.log("Khong co du lieu", response.status);

      }

      // doan ma xu ly du lieu

    })

...

Dữ liệu server trả về sẽ ở dạng luồng dữ liệu (stream), nên bạn có thể chuyển nó sang dạng JSON để xử lý. Lưu ý: phương thức response.json() không trả về dữ liệu dạng JSON, mà nó trả về một promise mới, phương thức resolve của promise này sẽ trả về dữ liệu dạng JSON. Ví dụ:

 fetch("https://jsonplaceholder.typicode.com/todos/1")

    .then( (response) => {

      if(!response.ok) {

        console.log("Khong co du lieu", response.status);

      }

      return response.json();

    })

    .then ( (dataJSON) => {

      console.log(dataJSON);

    })

    .catch( (error) => {

      console.log("bi loi", error);

    });

1.1.2       Lập trình bất đồng bộ bằng async/await

Phiên bản ES6 đã hỗ trợ kĩ thuật lập trình bất đồng bộ bằng promise. Với ES7, bạn có thêm một kĩ thuật lập trình bất đồng bộ nữa là async/await. Ưu điểm của async/await so với promise là viết mã đơn giản, dễ đọc, dễ kiểm tra lỗi.

Với promise, để viết một hàm nghiệp vụ chạy theo kiểu bất đồng bộ, bạn phải nhúng promise vào trong hàm nghiệp vụ đó, ví dụ:

const layDuLieu = () => {

        return new Promise( (resolve, reject) => {

 

        });

      };

Với async/await, cách làm cũng tương tự, dùng từ khóa async (viết tắt của asynchronous – bất đồng bộ) để khai báo một hàm sẽ chạy theo kiểu bất đồng bộ, ví dụ:

const layDuLieu = async () => {

 

    };

Vậy là hàm layDuLieu() sẽ chạy theo chế độ bất đồng bộ, và khi thực thi, hàm layDuLieu() sẽ trả về một promise.

    const layDuLieu = async () => {

 

    };

 

    const test = layDuLieu();

    console.log(test);

    // Promise {<fulfilled>: undefined}

Giả sử trong hàm layDuLieu(), chúng ta sử dụng fetch để lấy dữ liệu từ server về client, chúng ta có thể viết như sau:

    const layDuLieu = async () => {

      fetch("https://jsonplaceholder.typicode.com/todos/1")

        .then( (response) => {

          return response.json();

        })

        .then ( (dataJSON) => {

          console.log(dataJSON);

        })

    };

    layDuLieu();

// {userId: 1, id: 1, title: 'delectus aut autem', completed: false}

Ở đoạn mã trên, phương thức fetch() lại là một promise, do vậy JavaScript sẽ tách fetch() ra khỏi luồng chính của chương trình, đợi cho tới khi fetch() thực thi xong, nó sẽ gọi các hàm callback trong phương thức then(). Với từ khóa await (nghĩa là “chờ đợi”) đặt phía trước fetch() sẽ thay thế vai trò của các phương thức then(). Đoạn mã trên sẽ được viết lại như sau:

const layDuLieu = async () => {

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

      const dataJSON = await response.json();

 

      return dataJSON;

    };

    layDuLieu().then( (data) => {

      console.log(data);

    });

    // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}

Các phương thức nằm sau từ khóa await sẽ trả về một promise. Nên khi thực thi hàm layDuLieu(), bạn sẽ sử dụng các phương thức then() và catch để xử lý cho các trường hợp promise thực hiện thành công hoặc thất bại.

Ví dụ cho trường hợp promise trả về thất bại, bạn thử chạy đoạn mã sau đây sau khi đã ngắt kết nối mạng để xem kết quả.

const layDuLieu = async () => {

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

      const dataJSON = await response.json();

 

      return dataJSON;

    };

    layDuLieu()

    .then( (data) => {

      console.log(data);

    })

    .catch( (error) => {

      console.log("loi: ", error.message);

    });

    // loi:  Failed to fetch

Bạn hoàn toàn có thể cho promise trả về thất bại (rejected) khi có lỗi trong quá trình xử lý. Ví dụ, mặc định với status là 404, fetch() vẫn trả về response, tuy nhiên, thực tế là không lấy được dữ liệu. Trong trường hợp này, bạn sẽ sử dụng từ khóa throw để chủ động trả về thất bại. Ví dụ:

const layDuLieu = async () => {

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

      if (response.status !== 200) {

        throw new Error('Khong fetch duoc du lieu!');

      }

      const dataJSON = await response.json();

 

      return dataJSON;

    };

    layDuLieu()

    .then( (data) => {

      console.log(data);

    })

    .catch( (error) => {

      console.log("loi: ", error.message);

    });

    // loi:  Khong fetch duoc du lieu!

Một vài nhận xét:

– Kĩ thuật async/await hoạt động trên nền tảng promise

– Từ khóa async đặt trước một hàm sẽ làm cho hàm đó thực thi theo cơ chế bất đồng bộ

– Từ khóa await chỉ được sử dụng trong các hàm có từ khóa async

– Với async/await, cách viết đoạn chương trình chạy bất đồng bộ nhưng trông có vẻ giống như lập trình đồng bộ

– Từ khóa await đứng trước một promise (một hàm), nghĩa là chương trình sẽ chờ cho promise đó được xử lý xong và trả về giá trị, trước khi thực thi các lệnh kế tiếp

1.1.3       Xem và đọc thêm

– Dùng các từ khóa sau để tìm kiếm trên mạng và đọc thêm: lập trình đồng bộ, lập trình bất đồng bộ, synchronous, asynchronous, promise, fetch API, async/await.

– Synchronous and Asynchronous: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

– Asynchronous videos: https://www.youtube.com/watch?v=ZcQyJ-gxke0&list=PL4cUxeGkcC9jx2TTZk3IGWKSbtugYdrlu&index=1

– XMLHttpRequest.readyState: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState

– HTTP response status codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

– Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise?retiredLocale=vi

– async/await: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

– [1] Alex Banks, Eve Porcello, Learning React – Mordern Patterns for Developing React Apps, O’Reilly Media, 2020, p23 – p

– [2]  Darren Jones, JavaScript Novice To Ninja, Sitepoint, 2017, p149 – p160

1.1.4       Bài tập và thực hành

Bài tập 1. Viết lại các đoạn mã trong phần lý thuyết để chạy và kiểm tra lại kết quả.

Bài tập 2. Fetch và REST

https://www.youtube.com/watch?v=TRjVXmk8q8I

1.1.5       Câu hỏi ôn tập

Câu 1. Các kĩ thuật lập trình bất đồng bộ trong JavaScript gồm?

A. callback, promise, callback hell

B. promise, callback, fetch

C. async/await, callback, promise

D. fetch, async/await, callback

Câu 2. Trong JavaScript, callback được hiểu là?

A. Kĩ thuật khai báo biến

B. Xuất một chuỗi theo mẫu có sẵn

C. Truyền hàm cho hàm khác dưới dạng một đối số.

D. Kĩ thuật nối chuỗi

Câu 3. Trong JavaScript, khi sử dụng đối tượng XMLHttpRequest để giao tiếp giữa client và server, các trạng thái của gói request tính từ lúc được tạo ra tới khi thực hiện xong pha truyền dữ liệu, theo thứ tự sẽ là:

A. UNSENT > OPENED > HEADER_RECEIVED > LOADING > DONE

B. UNSENT > LOADING > HEADER_RECEIVED > OPENED > DONE

C. LOADING > UNSENT > HEADER_RECEIVED > OPENED > DONE

D. UNSENT > OPENED > LOADING > HEADER_RECEIVED > DONE

Đáp án: 1 (C), 2 (C), 3 (A)

-----

Cập nhật: 5/5/2022

-----

Bài tiếp: Web nâng cao (11) - JavaScript cho React (10)