XMLHttpRequest là gì? Cách sử dụng XHR trong AJAX

XMLHttpRequest (XHR) là một đối tượng JavaScript cho phép trình duyệt gửi và nhận dữ liệu với máy chủ mà không cần tải lại toàn bộ trang. Đây là một trong những nền tảng quan trọng tạo nên cách hoạt động của AJAX, đặc biệt phổ biến trong các website sử dụng JavaScript để cập nhật dữ liệu động.

Điểm đáng chú ý của XHR không nằm ở việc nó chỉ gửi một HTTP request. Giá trị thực sự của nó là khả năng để JavaScript chủ động giao tiếp với máy chủ, chờ phản hồi và xử lý dữ liệu ngay trong trang hiện tại. Nhờ vậy, một khu vực nhỏ trên website có thể được cập nhật mà người dùng không phải rời khỏi trang hoặc thực hiện thao tác tải lại.

XHR xuất hiện từ khá sớm trong quá trình phát triển ứng dụng web hiện đại nhưng vẫn có vai trò quan trọng trong nhiều hệ thống thực tế. Hiểu XHR cũng giúp lập trình viên nắm được bản chất của AJAX thay vì chỉ sử dụng các thư viện hoặc API cấp cao mà không biết request đang diễn ra như thế nào.

XMLHttpRequest là gì? Cách sử dụng XHR trong AJAX
XMLHttpRequest là gì? Cách sử dụng XHR trong AJAX

XHR thực chất là gì trong trình duyệt?

XHR là viết tắt của XMLHttpRequest. Đây là một Web API được cung cấp bởi trình duyệt, cho phép mã JavaScript thực hiện các HTTP request tới máy chủ và nhận dữ liệu trả về.

Tên gọi XMLHttpRequest có chữ XML vì XML từng là định dạng dữ liệu quan trọng trong thời kỳ API này được phát triển. Tuy nhiên, XHR không bị giới hạn ở XML. Trong các website hiện nay, XHR thường được sử dụng để nhận dữ liệu JSON, văn bản, HTML hoặc các loại dữ liệu khác tùy vào nhu cầu của ứng dụng.

Có thể hình dung đơn giản rằng trang web đang hiển thị trước mặt người dùng là một giao diện đã được tải xuống trình duyệt. Khi cần thêm dữ liệu, JavaScript có thể tạo một đối tượng XHR và yêu cầu máy chủ cung cấp phần dữ liệu đó. Máy chủ xử lý request rồi gửi response trở lại. JavaScript tiếp tục lấy dữ liệu nhận được và cập nhật giao diện.

Điều này khác với cách hoạt động truyền thống khi trình duyệt thường gửi request, nhận một tài liệu mới rồi thay thế toàn bộ trang hiện tại bằng tài liệu đó.

XHR đứng ở đâu trong một request AJAX?

AJAX không phải là một ngôn ngữ lập trình hay một giao thức riêng. Đây là cách kết hợp JavaScript với cơ chế trao đổi dữ liệu HTTP để tạo ra trải nghiệm tương tác linh hoạt hơn.

Trong mô hình quen thuộc, XHR đóng vai trò là công cụ giúp JavaScript thực hiện việc trao đổi dữ liệu với máy chủ. Một luồng xử lý đơn giản có thể được hiểu như sau:

  1. Người dùng thực hiện một hành động trên giao diện.
  2. JavaScript nhận biết hành động đó.
  3. Mã JavaScript tạo một đối tượng XMLHttpRequest.
  4. XHR thiết lập request tới máy chủ.
  5. Máy chủ tiếp nhận và xử lý request.
  6. Máy chủ gửi response về trình duyệt.
  7. JavaScript đọc dữ liệu phản hồi.
  8. Giao diện được cập nhật bằng dữ liệu mới.

Điểm quan trọng là bước cuối không bắt buộc phải tải lại toàn bộ tài liệu HTML. JavaScript có thể chỉ thay đổi đúng khu vực cần thiết, chẳng hạn danh sách sản phẩm, kết quả tìm kiếm, nội dung bình luận hoặc thông báo trạng thái.

Vì sao cơ chế này giúp website tương tác mượt hơn?

Trong một website truyền thống, khi người dùng gửi một biểu mẫu, trình duyệt thường chuyển request đến máy chủ và nhận về một trang mới. Toàn bộ tài liệu có thể được tải lại dù người dùng chỉ cần thay đổi một phần nhỏ trên giao diện.

Với AJAX sử dụng XHR, ứng dụng có thể yêu cầu riêng phần dữ liệu cần thiết. JavaScript nhận response và cập nhật khu vực tương ứng. Ví dụ, nếu người dùng chọn một danh mục sản phẩm, website có thể chỉ lấy danh sách sản phẩm của danh mục đó thay vì tải lại toàn bộ trang.

Cách tiếp cận này đem lại một số lợi ích rõ ràng:

  • Giảm nhu cầu tải lại toàn bộ trang.
  • Giảm lượng HTML phải gửi về trong những trường hợp chỉ cần dữ liệu nhỏ.
  • Phản hồi giao diện nhanh hơn trong nhiều tình huống.
  • Cho phép tạo các thành phần tương tác động.
  • Giúp trải nghiệm sử dụng gần với ứng dụng web hơn.

Tuy nhiên, không nên hiểu rằng XHR luôn làm website nhanh hơn. Tổng thời gian còn phụ thuộc vào máy chủ, cơ sở dữ liệu, mạng, kích thước response, JavaScript và cách lập trình. XHR chủ yếu thay đổi cách ứng dụng trao đổi và cập nhật dữ liệu, chứ không tự động tối ưu toàn bộ hệ thống.

Cấu trúc cơ bản của một yêu cầu bằng XHR

Để sử dụng XHR, JavaScript thường trải qua một chuỗi thao tác tương đối rõ ràng: tạo đối tượng, cấu hình request, gửi request và xử lý response.

Một ví dụ cơ bản:

const xhr = new XMLHttpRequest();

xhr.open("GET", "/du-lieu.php", true);

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
    }
};

xhr.onerror = function () {
    console.log("Không thể kết nối đến máy chủ.");
};

xhr.send();

Ở đây, new XMLHttpRequest() tạo ra một đối tượng XHR mới. Phương thức open() cấu hình request, còn send() thực sự gửi request đến máy chủ.

Khi máy chủ trả dữ liệu về, sự kiện load có thể được sử dụng để xử lý kết quả. Trong ví dụ trên, chương trình kiểm tra HTTP status trước khi lấy dữ liệu từ responseText.

Cách viết này tuy ngắn nhưng chứa những thành phần quan trọng nhất mà lập trình viên cần hiểu trước khi xây dựng các request phức tạp hơn.

Đối tượng XMLHttpRequest

Đối tượng XMLHttpRequest là trung tâm của quá trình trao đổi dữ liệu. Mỗi request thường có một đối tượng riêng để quản lý trạng thái, thông tin response và các thiết lập liên quan.

Ví dụ:

const xhr = new XMLHttpRequest();

Sau khi tạo, đối tượng này có thể được cấu hình bằng nhiều thuộc tính và phương thức khác nhau. Một số thành phần thường gặp gồm:

Thành phần Vai trò
open() Xác định phương thức HTTP, URL và cách thực hiện request.
send() Gửi request tới máy chủ.
responseText Lấy response dưới dạng chuỗi văn bản.
response Lấy dữ liệu phản hồi theo kiểu dữ liệu đã cấu hình.
status Cho biết mã trạng thái HTTP mà máy chủ trả về.
readyState Cho biết trạng thái hiện tại của request.

Không nhất thiết phải ghi nhớ tất cả thuộc tính ngay từ đầu. Quan trọng hơn là hiểu được vòng đời của một request và biết sử dụng đúng thuộc tính ở đúng thời điểm.

Phương thức open() quyết định cách request được thiết lập

Phương thức open() thường là bước cấu hình đầu tiên sau khi tạo đối tượng XHR.

xhr.open("GET", "/du-lieu.php", true);

Tham số đầu tiên là phương thức HTTP, chẳng hạn GET hoặc POST. Tham số thứ hai là URL mà request sẽ gửi tới. Tham số thứ ba xác định request có được thực hiện theo cơ chế bất đồng bộ hay không.

Trong AJAX, request bất đồng bộ thường là lựa chọn phù hợp vì JavaScript không cần dừng toàn bộ quá trình xử lý giao diện để chờ máy chủ phản hồi.

Ví dụ với URL có tham số:

xhr.open("GET", "/tim-kiem.php?tu-khoa=ajax", true);

Trong trường hợp này, dữ liệu tìm kiếm được truyền trực tiếp trong URL. Với các request có lượng dữ liệu lớn hơn hoặc cần gửi dữ liệu trong phần thân request, POST thường phù hợp hơn.

Request bất đồng bộ thay đổi cách JavaScript chờ dữ liệu

Khái niệm asynchronous là phần rất quan trọng khi tìm hiểu AJAX. Khi request được thực hiện bất đồng bộ, JavaScript có thể tiếp tục xử lý các công việc khác thay vì phải đứng chờ cho tới khi máy chủ trả response.

Ví dụ, người dùng nhấn nút tải danh sách sản phẩm. JavaScript gửi request rồi tiếp tục duy trì hoạt động của trang. Khi response xuất hiện, một hàm xử lý được gọi để đưa dữ liệu vào giao diện.

Điều này đặc biệt hữu ích với những request có thể mất thời gian như:

  • Lấy dữ liệu từ cơ sở dữ liệu.
  • Tìm kiếm thông tin trên máy chủ.
  • Kiểm tra dữ liệu biểu mẫu.
  • Tải thêm danh sách sản phẩm.
  • Gửi bình luận hoặc đánh giá.
  • Cập nhật một khu vực nhỏ của giao diện.

Nếu xử lý không hợp lý, việc gửi quá nhiều request vẫn có thể khiến website chậm hoặc tạo tải lớn cho máy chủ. Vì vậy, AJAX không đồng nghĩa với việc cứ dùng nhiều request là tốt. Request cần được thiết kế phù hợp với dữ liệu và hành vi thực tế của người dùng.

Các trạng thái cần hiểu khi theo dõi vòng đời request

XHR có cơ chế readyState để biểu thị request đang ở giai đoạn nào. Đây là một khái niệm quan trọng nếu muốn hiểu sâu cách XHR hoạt động thay vì chỉ sao chép đoạn mã có sẵn.

Giá trị Ý nghĩa
0 Đối tượng XHR vừa được tạo nhưng chưa được gọi open().
1 Request đã được thiết lập bằng open().
2 Request đã được gửi và phần header phản hồi đã được nhận.
3 Đang nhận dữ liệu phản hồi.
4 Request đã hoàn tất và toàn bộ response đã được nhận.

Trong cách lập trình cũ, onreadystatechange thường được sử dụng để theo dõi những thay đổi này.

xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

Cách này vẫn có giá trị về mặt học tập và trong một số hệ thống cũ. Tuy nhiên, khi xây dựng code mới, lập trình viên có thể sử dụng các sự kiện như load, error, timeoutabort để làm code dễ đọc hơn.

HTTP status quan trọng hơn việc request có chạy xong

Một lỗi thường gặp khi mới làm việc với XHR là chỉ kiểm tra xem request đã hoàn thành hay chưa. readyState === 4 chỉ cho biết quá trình trao đổi đã kết thúc, không có nghĩa máy chủ đã xử lý thành công.

Ví dụ, máy chủ có thể trả về mã 404, 403, 500 hoặc một mã HTTP khác. Request vẫn hoàn tất nhưng dữ liệu nhận được không phải kết quả mà ứng dụng mong muốn.

Do đó, khi xử lý response, nên xem xét cả HTTP status:

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log("Request thành công");
    } else {
        console.log("Máy chủ trả về lỗi:", xhr.status);
    }
};

Cách kiểm tra theo khoảng mã 2xx thường linh hoạt hơn việc chỉ kiểm tra duy nhất mã 200, bởi máy chủ có thể sử dụng những mã thành công khác tùy vào thiết kế API.

responseText và dữ liệu máy chủ trả về

Khi máy chủ trả về nội dung dạng văn bản, XHR có thể lấy dữ liệu thông qua responseText.

xhr.onload = function () {
    if (xhr.status === 200) {
        const data = xhr.responseText;
        console.log(data);
    }
};

Nếu máy chủ trả về JSON, responseText vẫn thường là chuỗi JSON. Khi đó JavaScript cần chuyển chuỗi này thành dữ liệu có thể thao tác bằng JSON.parse().

xhr.onload = function () {
    if (xhr.status === 200) {
        const data = JSON.parse(xhr.responseText);

        console.log(data);
    }
};

Điểm này rất quan trọng trong thực tế. Máy chủ có thể tạo JSON hoàn toàn hợp lệ nhưng nếu response bị lỗi định dạng, JSON.parse() sẽ phát sinh exception. Vì vậy, những ứng dụng thực tế nên có chiến lược xử lý dữ liệu không hợp lệ thay vì giả định response lúc nào cũng đúng.

XHR chỉ đảm nhiệm việc trao đổi dữ liệu. Cách dữ liệu được thiết kế, kiểm tra và đưa lên giao diện vẫn là trách nhiệm của ứng dụng.

Gửi dữ liệu bằng GET với XMLHttpRequest

GET là phương thức phù hợp khi JavaScript cần lấy dữ liệu từ máy chủ mà không làm thay đổi trạng thái dữ liệu trên máy chủ. Đây là trường hợp thường gặp khi tải danh sách sản phẩm, lấy thông tin bài viết, tìm kiếm, lấy dữ liệu phân trang hoặc kiểm tra một thông tin nào đó.

Một request GET đơn giản có thể được thực hiện như sau:

const xhr = new XMLHttpRequest();

xhr.open("GET", "/san-pham.php", true);

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
    } else {
        console.log("Request thất bại:", xhr.status);
    }
};

xhr.onerror = function () {
    console.log("Có lỗi khi kết nối máy chủ.");
};

xhr.send();

Ở ví dụ này, trình duyệt gửi request đến /san-pham.php. Khi máy chủ trả kết quả, chương trình kiểm tra status trước khi sử dụng dữ liệu.

Nếu cần truyền tham số tìm kiếm, tham số có thể được đặt trong query string:

const xhr = new XMLHttpRequest();

xhr.open(
    "GET",
    "/tim-kiem.php?keyword=javascript&page=2",
    true
);

xhr.onload = function () {
    if (xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.send();

Trong ứng dụng thực tế, dữ liệu đưa vào URL nên được mã hóa đúng cách thay vì nối chuỗi một cách tùy tiện. JavaScript cung cấp URLSearchParams để xử lý trường hợp này rõ ràng hơn.

const params = new URLSearchParams();

params.set("keyword", "XMLHttpRequest");
params.set("page", "2");

const xhr = new XMLHttpRequest();

xhr.open("GET", "/tim-kiem.php?" + params.toString(), true);

xhr.onload = function () {
    if (xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.send();

Cách này đặc biệt hữu ích khi số lượng tham số tăng lên, bởi chương trình không phải tự xử lý dấu ?, & và việc mã hóa giá trị.

Gửi dữ liệu bằng POST thay vì đưa tất cả lên URL

POST thường được sử dụng khi trình duyệt cần gửi dữ liệu lên máy chủ để xử lý. Ví dụ điển hình là đăng nhập, đăng ký, gửi biểu mẫu, thêm dữ liệu hoặc thực hiện một thao tác trên hệ thống.

Với XHR, request POST cơ bản có thể viết như sau:

const xhr = new XMLHttpRequest();

xhr.open("POST", "/dang-nhap.php", true);

xhr.setRequestHeader(
    "Content-Type",
    "application/x-www-form-urlencoded"
);

xhr.onload = function () {
    if (xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.send("username=admin&password=123456");

Trong ví dụ trên, setRequestHeader() cho máy chủ biết dữ liệu được gửi theo dạng URL encoded. Tuy nhiên, không nên tự nối chuỗi dữ liệu khi ứng dụng có nhiều trường. URLSearchParams có thể giúp việc tạo dữ liệu gửi đi an toàn và dễ quản lý hơn.

const data = new URLSearchParams();

data.set("username", "admin");
data.set("password", "123456");

const xhr = new XMLHttpRequest();

xhr.open("POST", "/dang-nhap.php", true);

xhr.setRequestHeader(
    "Content-Type",
    "application/x-www-form-urlencoded"
);

xhr.onload = function () {
    if (xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.send(data.toString());

Điều quan trọng cần phân biệt là POST không phải cơ chế bảo mật dữ liệu. Dữ liệu gửi bằng POST vẫn cần được bảo vệ bằng HTTPS và máy chủ vẫn phải kiểm tra, xác thực và xử lý dữ liệu một cách an toàn.

Gửi JSON giữa JavaScript và máy chủ

JSON là định dạng rất phổ biến khi xây dựng AJAX và API. Thay vì gửi từng trường theo dạng form truyền thống, JavaScript có thể đóng gói dữ liệu thành một đối tượng rồi chuyển thành chuỗi JSON.

const data = {
    name: "Nguyen Van A",
    email: "example@example.com"
};

const xhr = new XMLHttpRequest();

xhr.open("POST", "/api/khach-hang", true);

xhr.setRequestHeader(
    "Content-Type",
    "application/json"
);

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.responseText);
    }
};

xhr.send(JSON.stringify(data));

Ở đây, JSON.stringify() chuyển object JavaScript thành chuỗi JSON để gửi qua HTTP. Phía máy chủ cần đọc request body và giải mã JSON theo ngôn ngữ lập trình đang sử dụng.

Nếu máy chủ trả JSON về, JavaScript có thể sử dụng JSON.parse() để chuyển chuỗi phản hồi thành object:

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        try {
            const result = JSON.parse(xhr.responseText);

            console.log(result);
        } catch (error) {
            console.log("Response không phải JSON hợp lệ.");
        }
    }
};

Cách xử lý bằng try...catch đặc biệt hữu ích khi API có thể trả về nội dung lỗi không phải JSON. Nếu gọi JSON.parse() trực tiếp mà dữ liệu không đúng định dạng, chương trình có thể phát sinh lỗi JavaScript.

Nhận JSON trực tiếp bằng responseType

XHR còn cho phép chỉ định kiểu dữ liệu mà JavaScript mong muốn nhận thông qua responseType. Khi làm việc với JSON, có thể đặt giá trị này thành json.

const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/san-pham", true);

xhr.responseType = "json";

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log(xhr.response);
    }
};

xhr.send();

Khi response được xử lý theo kiểu JSON, chương trình có thể sử dụng xhr.response thay vì tự gọi JSON.parse() trên responseText.

Cách này giúp code gọn hơn và thể hiện rõ ý định của request. Tuy nhiên, lập trình viên vẫn cần kiểm tra status HTTP và xử lý trường hợp dữ liệu không đúng như API đã cam kết.

Gửi dữ liệu biểu mẫu bằng FormData

XHR không chỉ phù hợp với JSON hoặc dữ liệu URL encoded. Khi làm việc với biểu mẫu, đặc biệt là biểu mẫu có tải tệp, FormData là lựa chọn rất hữu ích.

Ví dụ, một biểu mẫu HTML có thể có các trường như sau:

<form id="contactForm">
    <input type="text" name="name">
    <input type="email" name="email">
    <textarea name="message"></textarea>
    <button type="submit">Gửi</button>
</form>

JavaScript có thể lấy toàn bộ dữ liệu của biểu mẫu:

const form = document.getElementById("contactForm");

form.addEventListener("submit", function (event) {
    event.preventDefault();

    const data = new FormData(form);

    const xhr = new XMLHttpRequest();

    xhr.open("POST", "/xu-ly-lien-he.php", true);

    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 300) {
            console.log(xhr.responseText);
        }
    };

    xhr.send(data);
});

Điểm đáng chú ý là khi gửi FormData, không nên tự đặt Content-Type thành multipart/form-data. Trình duyệt cần tự tạo Content-Type cùng boundary phù hợp để máy chủ có thể phân tách các phần dữ liệu.

Đây cũng là một trong những lý do FormData thuận tiện khi xử lý upload file bằng AJAX.

Xử lý phản hồi thành công và phản hồi lỗi

Một request AJAX hoàn chỉnh không nên chỉ có nhánh thành công. Trong môi trường thực tế, mạng có thể mất kết nối, máy chủ có thể quá tải, API có thể trả mã lỗi hoặc dữ liệu nhận được có thể không đúng định dạng.

Có thể xây dựng phần xử lý cơ bản như sau:

const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/data", true);

xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log("Thành công:", xhr.responseText);
        return;
    }

    console.log("Máy chủ trả lỗi:", xhr.status);
};

xhr.onerror = function () {
    console.log("Không thể thực hiện request.");
};

xhr.ontimeout = function () {
    console.log("Request đã quá thời gian chờ.");
};

xhr.send();

Ba tình huống trên cần được phân biệt:

  • HTTP error: máy chủ có phản hồi nhưng status cho biết request không thành công.
  • Network error: trình duyệt không thể hoàn tất việc giao tiếp với máy chủ theo cách mong muốn.
  • Timeout: request không hoàn thành trong khoảng thời gian đã cấu hình.

Việc phân biệt các tình huống này giúp giao diện đưa ra thông báo phù hợp. Chẳng hạn, lỗi 404 có thể là tài nguyên không tồn tại, trong khi lỗi kết nối lại có thể liên quan đến mạng hoặc máy chủ không thể truy cập.

Thiết lập timeout để request không chờ vô hạn

Trong một ứng dụng thực tế, không nên để một request chờ quá lâu mà không có giới hạn. XHR hỗ trợ thuộc tính timeout để xác định thời gian tối đa mà request được phép chờ.

const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/san-pham", true);

xhr.timeout = 10000;

xhr.onload = function () {
    if (xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

xhr.ontimeout = function () {
    console.log("Máy chủ phản hồi quá chậm.");
};

xhr.send();

Giá trị 10000 tương ứng với 10.000 mili giây, tức khoảng 10 giây.

Timeout không nên được đặt quá ngắn vì mạng chậm hoặc máy chủ đang xử lý tác vụ hợp lệ có thể bị coi là lỗi. Ngược lại, nếu đặt quá dài, người dùng có thể phải chờ trong trạng thái không rõ ràng. Thời gian phù hợp phụ thuộc vào loại request.

Hủy request khi không còn cần thiết

XHR có phương thức abort() để hủy một request đang thực hiện. Điều này có ích khi request trở nên không còn cần thiết.

Một ví dụ điển hình là ô tìm kiếm. Người dùng liên tục thay đổi từ khóa, trong khi request của từ khóa trước đó vẫn chưa hoàn thành. Nếu không kiểm soát, nhiều response có thể quay về theo thứ tự không mong muốn.

let xhr = null;

function search(keyword) {
    if (xhr) {
        xhr.abort();
    }

    xhr = new XMLHttpRequest();

    xhr.open(
        "GET",
        "/tim-kiem.php?keyword=" + encodeURIComponent(keyword),
        true
    );

    xhr.onload = function () {
        if (xhr.status === 200) {
            console.log(xhr.responseText);
        }
    };

    xhr.send();
}

Trong trường hợp này, request cũ được hủy trước khi tạo request mới. Tuy nhiên, với tính năng tìm kiếm theo thời gian thực, thường nên kết hợp thêm cơ chế debounce để không gửi request sau mỗi lần người dùng gõ một ký tự.

Thay đổi giao diện sau khi nhận dữ liệu

AJAX chỉ thực sự có ý nghĩa khi dữ liệu nhận được được đưa trở lại giao diện. XHR không tự động thay đổi nội dung trang; JavaScript phải quyết định dữ liệu nào được sử dụng và cập nhật phần tử nào.

Ví dụ, giả sử máy chủ trả về HTML:

<div>
    <strong>Sản phẩm mới</strong>
</div>

JavaScript có thể đưa nội dung vào một khu vực trên trang:

xhr.onload = function () {
    if (xhr.status === 200) {
        document.getElementById("ketQua").innerHTML =
            xhr.responseText;
    }
};

Cách làm này tiện lợi nhưng cần đặc biệt cẩn thận nếu response có nguồn gốc từ dữ liệu người dùng hoặc nguồn không đáng tin cậy. Đưa chuỗi chưa được kiểm soát trực tiếp vào innerHTML có thể tạo ra nguy cơ XSS.

Trong trường hợp chỉ cần hiển thị văn bản thuần túy, textContent thường an toàn hơn:

document.getElementById("ketQua").textContent =
    xhr.responseText;

Nếu API trả JSON, cách tốt hơn thường là lấy từng giá trị cần thiết rồi tạo hoặc cập nhật phần tử giao diện thay vì đưa nguyên response vào innerHTML.

Thiết lập header khi giao tiếp với API

HTTP header giúp trình duyệt và máy chủ trao đổi thêm thông tin về request. Với XHR, header có thể được thiết lập bằng setRequestHeader().

Ví dụ, khi API nhận JSON:

xhr.setRequestHeader(
    "Content-Type",
    "application/json"
);

Trong một số API, máy chủ cũng yêu cầu thông tin xác thực hoặc các header riêng. Tuy nhiên, không nên tùy tiện gửi các header nhạy cảm từ phía trình duyệt. Cơ chế xác thực phải được thiết kế đồng bộ giữa frontend, backend, HTTPS và chính sách CORS.

CORS ảnh hưởng thế nào đến XHR?

Một trường hợp thường gây khó hiểu là XHR hoạt động bình thường khi gọi API cùng website nhưng gặp lỗi khi gọi sang một domain khác. Nguyên nhân có thể liên quan đến Same-Origin PolicyCORS.

Ví dụ, trang web tại một origin có thể gửi request đến một origin khác. Việc request có được trình duyệt cho phép đọc response hay không phụ thuộc vào chính sách CORS mà máy chủ đích cung cấp.

Phần kiểm soát này chủ yếu nằm ở phía máy chủ. JavaScript phía trình duyệt không thể đơn giản bỏ qua giới hạn bảo mật bằng cách thêm một header tùy ý.

Một API có thể phản hồi các header CORS phù hợp, chẳng hạn:

Access-Control-Allow-Origin: https://example.com

Nếu request thuộc trường hợp cần preflight, trình duyệt có thể gửi trước một request OPTIONS để hỏi máy chủ xem request thực tế có được phép hay không. Vì vậy, lỗi CORS không nên được xử lý bằng cách tắt bảo mật trình duyệt hoặc tìm cách né cơ chế bảo vệ. Cần cấu hình origin, phương thức và header được phép ở phía máy chủ một cách chính xác.

Tổ chức một request XHR để dễ bảo trì

Khi mới học AJAX, việc viết toàn bộ request trong một đoạn JavaScript ngắn có thể khá thuận tiện. Nhưng khi website phát triển, cách viết này nhanh chóng trở nên khó quản lý. Một dự án có thể có hàng chục hoặc hàng trăm request khác nhau cho sản phẩm, tìm kiếm, đăng nhập, bình luận, phân trang và quản trị dữ liệu.

Thay vì lặp lại toàn bộ logic ở nhiều nơi, có thể xây dựng một hàm dùng chung để tạo request. Mục tiêu không phải làm code ngắn nhất mà là tách những phần có tính lặp lại ra khỏi logic nghiệp vụ.

function request(method, url, data, callback) {
    const xhr = new XMLHttpRequest();

    xhr.open(method, url, true);

    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 300) {
            callback(null, xhr);
            return;
        }

        callback(new Error("HTTP " + xhr.status), xhr);
    };

    xhr.onerror = function () {
        callback(new Error("Network error"), xhr);
    };

    xhr.send(data);
}

Sau đó, những phần khác của ứng dụng chỉ cần gọi hàm này với thông tin cần thiết. Trong hệ thống lớn hơn, có thể tiếp tục tách riêng lớp giao tiếp API, xử lý dữ liệu và phần cập nhật giao diện.

Cách tổ chức này giúp khi cần thay đổi timeout, cách kiểm tra lỗi hoặc một số thiết lập chung, lập trình viên không phải sửa hàng chục đoạn code giống nhau.

Không nên xem mọi response là dữ liệu hợp lệ

Một request thành công về mặt HTTP chưa chắc đồng nghĩa dữ liệu ứng dụng nhận được là đúng. Máy chủ có thể trả HTTP 200 nhưng nội dung JSON thiếu trường, sai kiểu dữ liệu hoặc chứa trạng thái nghiệp vụ không thành công.

Ví dụ, API có thể trả:

{
    "success": false,
    "message": "Không tìm thấy sản phẩm"
}

Trong trường hợp này, HTTP request đã hoàn thành và có thể vẫn nhận status 200, nhưng nghiệp vụ lại không thành công.

Vì vậy, ứng dụng nên phân biệt ít nhất ba lớp:

  • Trạng thái HTTP: request có được máy chủ xử lý theo cách mong muốn hay không.
  • Định dạng dữ liệu: response có đúng cấu trúc mà frontend mong đợi hay không.
  • Trạng thái nghiệp vụ: thao tác thực tế có thành công theo quy tắc của ứng dụng hay không.

Cách phân tách này giúp giao diện hiển thị thông báo chính xác hơn và tránh việc coi mọi response 200 là thành công tuyệt đối.

Bảo mật khi sử dụng XHR trong website

XHR không phải là một lớp bảo mật. Nó chỉ là phương tiện để JavaScript giao tiếp với máy chủ. Các quy tắc bảo mật vẫn phải được triển khai ở cả frontend và backend.

Không tin dữ liệu nhận từ trình duyệt

Mọi dữ liệu được gửi bằng XHR đều có thể bị người dùng thay đổi trước khi tới máy chủ. Vì vậy, việc JavaScript kiểm tra dữ liệu ở trình duyệt chỉ giúp cải thiện trải nghiệm chứ không thay thế validation phía server.

Ví dụ, nếu frontend yêu cầu giá sản phẩm phải lớn hơn 0, người dùng vẫn có thể tự sửa request và gửi một giá trị khác. Máy chủ phải tự kiểm tra lại dữ liệu trước khi lưu hoặc thực hiện nghiệp vụ.

Cẩn thận khi đưa response vào HTML

Việc sử dụng innerHTML với dữ liệu chưa được kiểm soát có thể tạo ra nguy cơ XSS. Nếu dữ liệu chỉ cần hiển thị dưới dạng văn bản, nên cân nhắc sử dụng textContent.

const result = document.getElementById("ketQua");

result.textContent = xhr.responseText;

Nếu ứng dụng thực sự cần đưa HTML động vào giao diện, dữ liệu phải được kiểm soát và xử lý phù hợp trước khi chèn vào DOM.

HTTPS vẫn là yếu tố nền tảng

Việc sử dụng XHR không mã hóa dữ liệu một cách tự động. Nếu ứng dụng truyền thông tin nhạy cảm, kết nối cần được bảo vệ bằng HTTPS. Đây là trách nhiệm của toàn bộ hệ thống chứ không phải một tùy chọn riêng của XHR.

Đặc biệt với đăng nhập, thông tin tài khoản, dữ liệu cá nhân hoặc các request có quyền thay đổi dữ liệu, không nên dựa vào việc "ẩn" dữ liệu trong JavaScript để coi đó là biện pháp bảo mật.

Kiểm soát request để tránh gửi quá nhiều lần

Một trong những vấn đề thực tế của AJAX là số lượng request có thể tăng rất nhanh nếu JavaScript phản ứng với mọi thao tác của người dùng.

Ví dụ, người dùng nhập từ khóa XMLHttpRequest. Nếu gửi request ngay sau mỗi ký tự, trình duyệt có thể tạo nhiều request liên tiếp:

  • X
  • XM
  • XML
  • XMLH
  • XMLHt
  • ...

Trong khi người dùng chưa chắc cần kết quả của tất cả những từ khóa trung gian đó.

Giải pháp phổ biến là debounce: đợi người dùng ngừng thao tác trong một khoảng thời gian ngắn rồi mới gửi request.

let timer;

function search(keyword) {
    clearTimeout(timer);

    timer = setTimeout(function () {
        console.log("Gửi request:", keyword);
    }, 400);
}

Khi kết hợp debounce với abort(), ứng dụng có thể giảm đáng kể số request không cần thiết và hạn chế tình trạng response cũ cập nhật giao diện sau response mới.

Tránh để response cũ ghi đè kết quả mới

Đây là một lỗi khá khó nhận ra trong các tính năng tìm kiếm hoặc lọc dữ liệu. Người dùng có thể tạo request A trước request B, nhưng máy chủ lại trả B trước rồi mới trả A.

Nếu JavaScript cứ nhận response nào thì cập nhật giao diện, kết quả cũ của A có thể ghi đè kết quả mới của B.

Có nhiều cách giải quyết vấn đề này. Một cách đơn giản là hủy request cũ khi tạo request mới. Một cách khác là gắn một mã hoặc số thứ tự cho từng request và chỉ chấp nhận response của request mới nhất.

let requestId = 0;

function search(keyword) {
    const currentId = ++requestId;

    const xhr = new XMLHttpRequest();

    xhr.open(
        "GET",
        "/tim-kiem.php?keyword=" +
        encodeURIComponent(keyword),
        true
    );

    xhr.onload = function () {
        if (currentId !== requestId) {
            return;
        }

        if (xhr.status === 200) {
            console.log(xhr.responseText);
        }
    };

    xhr.send();
}

Cách này đặc biệt hữu ích khi request không thể hoặc không nên bị hủy trong một số kiến trúc ứng dụng.

Những lỗi thường gặp khi lập trình với XHR

Phần lớn lỗi khi làm AJAX không đến từ bản thân XMLHttpRequest mà đến từ việc hiểu chưa đầy đủ về HTTP, JavaScript bất đồng bộ hoặc dữ liệu trả về.

Chỉ kiểm tra readyState

Kiểm tra readyState === 4 cho biết request đã kết thúc nhưng không đảm bảo request thành công. Cần kết hợp với HTTP status và tùy trường hợp là kiểm tra cả nội dung response.

Chỉ kiểm tra status 200

Không phải mọi response thành công đều nhất thiết phải sử dụng mã 200. Với nhiều API, các mã 2xx khác cũng có thể biểu thị request đã được xử lý thành công.

Quên xử lý lỗi mạng

Một đoạn code chỉ có onload sẽ thiếu trường hợp kết nối gặp vấn đề. Với request quan trọng, nên có chiến lược xử lý lỗi mạng, timeout và hủy request.

Không kiểm tra JSON trước khi sử dụng

Nếu frontend mong đợi JSON nhưng server trả về HTML lỗi, chuỗi rỗng hoặc dữ liệu sai định dạng, việc parse trực tiếp có thể khiến JavaScript phát sinh lỗi.

Đưa dữ liệu không đáng tin cậy vào innerHTML

Đây là lỗi có thể dẫn đến vấn đề bảo mật nghiêm trọng. Dữ liệu từ API, cơ sở dữ liệu hoặc người dùng không nên mặc nhiên được coi là HTML an toàn.

Gửi request cho mọi thao tác nhỏ

Việc gửi quá nhiều request có thể làm tăng tải máy chủ và khiến giao diện hoạt động kém ổn định. Debounce, throttle, cache hoặc gộp request có thể phù hợp tùy từng trường hợp.

XHR có thể tải dữ liệu ngoài JSON và văn bản

Mặc dù AJAX hiện nay thường gắn với JSON, XHR không bị giới hạn ở định dạng này. Tùy thuộc vào responseType, XHR có thể xử lý nhiều loại dữ liệu khác nhau.

responseType Mục đích phổ biến
text Nhận dữ liệu dạng văn bản.
json Nhận dữ liệu JSON.
blob Nhận dữ liệu nhị phân như tệp hoặc tài nguyên.
arraybuffer Làm việc với dữ liệu nhị phân ở mức thấp hơn.

Ví dụ, nếu API trả về dữ liệu JSON, có thể đặt:

xhr.responseType = "json";

Với tệp nhị phân, có thể sử dụng:

xhr.responseType = "blob";

Việc lựa chọn responseType phù hợp giúp JavaScript xử lý response đúng với bản chất dữ liệu thay vì coi mọi thứ là chuỗi.

So sánh XHR với Fetch API

XHR là nền tảng lâu đời của AJAX, nhưng JavaScript hiện đại có thêm Fetch API với cú pháp thường dễ đọc hơn khi xây dựng các request bất đồng bộ.

Tiêu chí XMLHttpRequest Fetch API
Cú pháp Dựa nhiều vào object, event và callback. Dựa trên Promise, thường dễ tổ chức hơn.
AJAX truyền thống Rất phổ biến. Được sử dụng rộng rãi trong JavaScript hiện đại.
Theo dõi tiến trình tải Có cơ chế progress phù hợp với nhiều tình huống. Cách xử lý phụ thuộc vào ReadableStream và nhu cầu cụ thể.
Hủy request Sử dụng abort(). Có thể kết hợp AbortController.
Code bất đồng bộ Có thể dùng event handler hoặc callback. Phù hợp với Promise và async/await.

Không nên hiểu rằng Fetch làm XHR trở nên vô dụng. Hai API có khả năng và cách sử dụng khác nhau. Những hệ thống cũ đang phụ thuộc vào XHR vẫn có thể tiếp tục sử dụng nó. Ngược lại, khi bắt đầu một dự án JavaScript mới, Fetch thường là lựa chọn đáng cân nhắc nếu các yêu cầu của ứng dụng phù hợp.

Khi nào XHR vẫn là lựa chọn hợp lý?

XHR vẫn có giá trị trong những dự án cần khả năng tương thích với kiến trúc AJAX cũ, hệ thống đang sử dụng nhiều thư viện dựa trên XMLHttpRequest hoặc các trường hợp cần cơ chế sự kiện và theo dõi tiến trình mà đội ngũ đã quen thuộc.

Nó cũng rất hữu ích về mặt học tập. Hiểu XHR giúp lập trình viên nhìn rõ những thành phần cơ bản của một request HTTP phía trình duyệt: phương thức, URL, header, body, trạng thái, response và các tình huống lỗi.

Đặc biệt, khi làm việc với một hệ thống PHP cũ hoặc một website được xây dựng từ lâu, gặp XHR là chuyện hoàn toàn bình thường. Biết đọc và sửa code XHR giúp quá trình bảo trì dễ dàng hơn nhiều.

AJAX không chỉ là một đoạn code gửi request

Một hiểu lầm phổ biến là coi AJAX đồng nghĩa với XMLHttpRequest. Trên thực tế, XHR chỉ là một trong những công cụ có thể được sử dụng để thực hiện mô hình giao tiếp bất đồng bộ giữa trình duyệt và máy chủ.

Điều quan trọng hơn là hiểu toàn bộ luồng dữ liệu:

  1. Giao diện tạo ra một hành động.
  2. JavaScript xác định dữ liệu cần gửi.
  3. Request được tạo và gửi đến máy chủ.
  4. Backend xác thực và xử lý dữ liệu.
  5. Máy chủ tạo response.
  6. JavaScript kiểm tra response.
  7. Dữ liệu được chuyển thành trạng thái mà giao diện có thể sử dụng.
  8. Chỉ phần cần thiết của giao diện được cập nhật.

Nếu chỉ học thuộc cú pháp new XMLHttpRequest() mà không hiểu luồng trên, việc xử lý những tình huống thực tế sẽ trở nên khó khăn. Ngược lại, khi nắm được bản chất, việc chuyển từ XHR sang Fetch hoặc một thư viện HTTP khác cũng trở nên đơn giản hơn.

Những nguyên tắc quan trọng khi xây dựng AJAX bằng XHR

  • Chỉ gửi request khi thực sự cần dữ liệu.
  • Kiểm tra HTTP status thay vì chỉ kiểm tra request đã hoàn thành.
  • Luôn có phương án xử lý lỗi mạng đối với request quan trọng.
  • Thiết lập timeout cho những request có thể chờ lâu.
  • Hủy hoặc bỏ qua request cũ khi người dùng tạo request mới.
  • Kiểm tra cấu trúc response trước khi sử dụng.
  • Không tin tưởng dữ liệu do trình duyệt gửi lên.
  • Không đưa dữ liệu chưa được kiểm soát trực tiếp vào HTML.
  • Sử dụng HTTPS cho dữ liệu cần bảo vệ.
  • Cấu hình CORS ở phía máy chủ khi ứng dụng cần giao tiếp khác origin.
  • Hạn chế request dư thừa bằng debounce, throttle hoặc cache khi phù hợp.
  • Tách logic gọi API khỏi logic xử lý giao diện khi dự án bắt đầu lớn.

Nắm XHR để hiểu sâu hơn về AJAX

XMLHttpRequest là một phần quan trọng trong lịch sử phát triển ứng dụng web tương tác. Nó cho phép JavaScript trao đổi dữ liệu với máy chủ mà không cần tải lại toàn bộ tài liệu, từ đó tạo nền tảng cho cách tiếp cận AJAX được sử dụng rộng rãi.

Điều cần ghi nhớ không chỉ là cách tạo một đối tượng XHR mà còn là cách một request thực sự vận hành: chọn phương thức HTTP, xác định URL, gửi dữ liệu, theo dõi trạng thái, kiểm tra response, xử lý lỗi và cập nhật giao diện.

Trong các hệ thống hiện đại, Fetch API thường mang lại cú pháp thuận tiện hơn cho nhiều trường hợp. Tuy nhiên, XHR vẫn đáng học vì nó xuất hiện trong rất nhiều mã nguồn thực tế và giúp làm rõ những nguyên lý nền tảng của giao tiếp giữa trình duyệt với máy chủ.

Khi hiểu được những nguyên lý này, AJAX không còn là một kỹ thuật khó nhớ. Nó trở thành một quy trình có thể phân tích từng bước, từ hành động của người dùng cho tới request HTTP, xử lý backend và kết quả cuối cùng được phản ánh lên giao diện.

  • 0 Bình luận
CEO Bùi Tấn Lực | Founder Web Mới
Bùi Tấn Lực
Tìm hiểu về CEO Bùi Tấn Lực, Founder Web Mới với nhiều năm kinh nghiệm trong lĩnh vực phát triển website, SEO và chia sẻ kiến thức công nghệ
Đánh giá
Chia sẻ nội dung đánh giá của bạn về XMLHttpRequest là gì? Cách sử dụng XHR trong AJAX
Email, Điện thoại của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *
Đánh giá của bạn
Tên *
Email
Số điện thoại *
Bình luận, Hỏi đáp
Yêu Cầu Báo Giá
Gửi trang web mẫu cần làm theo, chúng tôi sẽ báo giá đến bạn từ Email (tanlucit09@gmail.com - Bùi Tấn Lực) hoặc Zalo (Lực IT - 0398259259) !