Buổi 13: JavaScript cho Website (2): biểu mẫu, đồ họa, JSON, giao tiếp máy chủ
Bạn đang làm trang đăng ký thành viên cho một cửa hàng trực tuyến. Khách điền họ tên, email, số điện thoại rồi bấm "Gửi". Nếu để trống ô bắt buộc, trang phải báo lỗi ngay chứ không bắt khách chờ máy chủ trả lời. Sau khi gửi, trang lại cần lấy danh sách sản phẩm từ máy chủ và hiển thị mà không phải tải lại toàn bộ trang. Dữ liệu qua lại giữa trình duyệt và máy chủ thường được đóng gói dưới dạng JSON, và nếu muốn có biểu đồ hay hiệu ứng chuyển động thì bạn sẽ dùng phần tử <canvas>.
Buổi này nối tiếp Buổi 12 (window, document, sự kiện, DOM). Bạn sẽ học bốn kỹ thuật đưa trang Web từ "tĩnh" sang "tương tác thực sự": xác thực biểu mẫu, đồ họa canvas, JSON và AJAX/fetch.
Mục tiêu buổi học
- Sau buổi này bạn có thể kiểm tra dữ liệu biểu mẫu bằng
onsubmit,checkValidity(),setCustomValidity()và các thuộc tính của đối tượngvalidity. - Sau buổi này bạn có thể giải thích vì sao xác thực phía trình duyệt chỉ cải thiện trải nghiệm và vẫn phải kiểm tra lại ở phía máy chủ.
- Sau buổi này bạn có thể dùng phần tử
<canvas>và đối tượng bối cảnh vẽ (context) để vẽ đường thẳng, hình tròn, văn bản, gradient, hình ảnh và tạo chuyển động đơn giản. - Sau buổi này bạn có thể đọc/viết dữ liệu JSON, dùng
JSON.parse()vàJSON.stringify(), phân biệt JSON với đối tượng JavaScript và với XML. - Sau buổi này bạn có thể giải thích AJAX là gì và thực hiện một yêu cầu bằng
XMLHttpRequest(các bước,readyState,status) cũng như bằngfetch()vớiasync/await. - Sau buổi này bạn có thể hiểu chính sách cùng nguồn (same-origin policy) và CORS để đọc được thông báo lỗi trong Console.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1. Xác thực biểu mẫu | onsubmit, checkValidity(), setCustomValidity(), validity, lưu ý an toàn |
30 phút |
| 2. Đồ họa trong JavaScript | <canvas>, context 2D, đường, hình tròn, văn bản, gradient, ảnh |
20 phút |
| 3. Làm việc với JSON | Khái niệm, JSON và XML, kiểu dữ liệu JSON | 25 phút |
| 4. Giao tiếp với máy chủ | AJAX, XMLHttpRequest, fetch(), async/await, CORS |
40 phút |
| Thực hành theo giáo trình | Bài 6.1 – 6.5 (chuyển động, Firebase, Postman, thêm/xóa/đọc/lưu dữ liệu) | 35 phút |
| Ôn tập và bài tập tự thực hành | Câu hỏi 6.9 – 6.15, Bài 6.6 – 6.8, bài tập trên Codelab | 30 phút |
Khoảng một nửa thời gian là thực hành: bạn sẽ chạy thử ví dụ ngay trong bài, làm các nhóm bài tập sau mỗi phần và hoàn thành bài tập tự thực hành.
Lưu ý: Trong bài này, mọi ví dụ gọi máy chủ đều được mô phỏng bằng một "máy chủ giả" viết ngay trong mã (hàm
fakeFetch, lớpFakeXHR) để chạy được ngay trong trình duyệt của bạn. Mã gọi API thật (Firebase, jsonplaceholder) vẫn được trình bày nhưng không có nút chạy.
1. Xác thực biểu mẫu
Giáo trình: Chương 6 — mục "Xác thực biểu mẫu".
Biểu mẫu (form) HTML được dùng để người dùng nhập liệu, và dữ liệu nhập vào thường được kiểm tra lại bằng JavaScript. Bạn đã biết các ô nhập ở Buổi 5; hôm nay là phần "hành vi" của chúng.
1.1. Kiểm tra khi gửi biểu mẫu bằng onsubmit
Giáo trình đưa ra một biểu mẫu có thuộc tính onsubmit="return validateForm()". Khi người dùng bấm nút gửi, trình duyệt chạy hàm validateForm(); nếu hàm trả về false thì việc gửi bị hủy, nếu trả về true (hoặc không chặn) thì dữ liệu được gửi đi.
<form name="myForm" action="/action_page.php" onsubmit="return validateForm()" method="post">
Name: <input type="text" name="fname">
<input type="submit" value="Submit">
</form>function validateForm() {
if (document.forms["myForm"]["fname"].value == "") {
alert("Name must be filled out");
return false;
}
}Hãy chạy thử phiên bản đầy đủ dưới đây. Ví dụ tự gửi một sự kiện submit để bạn thấy kết quả trong bảng console; bạn cũng có thể gõ tên rồi bấm nút trong khung xem trước.
Lưu ý: Trong khung chạy thử của bài học, việc gửi form thật bị tắt nên hàm bên dưới luôn trả về
false. Trong dự án thật, khi dữ liệu hợp lệ bạn phảireturn trueđể trình duyệt gửi dữ liệu đi.
<form name="myForm" action="#" onsubmit="return validateForm()" method="post">
Name: <input type="text" name="fname">
<input type="submit" value="Submit">
</form>
<script>
function validateForm() {
if (document.forms["myForm"].elements["fname"].value == "") {
alert("Name must be filled out");
return false;
}
alert("Hợp lệ: dữ liệu sẽ được gửi đi");
return false; // trong dự án thật: return true
}
// Mô phỏng người dùng bấm Submit khi chưa nhập gì
const form = document.forms["myForm"];
const ev = new Event("submit", { cancelable: true });
form.dispatchEvent(ev);
console.log("Việc gửi bị chặn?", ev.defaultPrevented);
// Mô phỏng sau khi đã nhập tên
form.elements["fname"].value = "An";
form.dispatchEvent(new Event("submit", { cancelable: true }));
</script>Cách đọc: document.forms["myForm"]["fname"].value (trong ví dụ chạy thử viết thành .elements["fname"], cách viết tường minh hơn) đi từ tập hợp các form của tài liệu, chọn form theo name, rồi chọn ô nhập theo name. Giá trị rỗng ("") nghĩa là người dùng chưa nhập gì.
Xác thực biểu mẫu: validity & checkValidity()
gõ vào ô, xem các cờ của đối tượng validity| validity.… | giá trị |
|---|---|
| valueMissing | true |
| typeMismatch | false |
| patternMismatch | false |
| tooShort | false |
| tooLong | false |
| rangeUnderflow | false |
| rangeOverflow | false |
| stepMismatch | false |
| customError | false |
| valid | false |
checkValidity()→false🔐 Xác thực phía trình duyệt chỉ để báo lỗi sớm, không phải bảo mật — máy chủ luôn phải kiểm tra lại.
Thử mô phỏng trên: đổi các ràng buộc (bắt buộc, độ dài, khuôn mẫu...) rồi xem ô nhập báo hợp lệ hay không. Bạn sẽ thấy cùng một dữ liệu có thể vi phạm nhiều loại ràng buộc khác nhau.
1.2. Các phương thức checkValidity() và setCustomValidity()
Các phần tử nhập liệu của biểu mẫu có hai phương thức liên quan tới kiểm tra dữ liệu:
checkValidity()cho biết phần tử có thỏa mãn các ràng buộc hay không (trả vềtrue/false).setCustomValidity()đặt một thông báo lỗi tùy chỉnh cho phần tử.
Hai phương thức này kết hợp với các thuộc tính ràng buộc dữ liệu:
| Thuộc tính | Mô tả |
|---|---|
disabled |
Đối tượng nhập liệu không thể sử dụng |
max |
Giá trị lớn nhất cho phép |
min |
Giá trị nhỏ nhất cho phép |
pattern |
Khuôn mẫu của dữ liệu |
required |
Bắt buộc nhập dữ liệu |
type |
Kiểu dữ liệu |
Khi các thành phần nhập liệu đã được quy định các thuộc tính trên, trong hàm kiểm tra bằng JavaScript bạn chỉ cần gọi checkValidity(): dữ liệu hợp lệ thì trả về true, không hợp lệ thì trả về false. Đó là cơ sở để đưa ra cảnh báo hoặc nhắc nhở cần thiết.
<form id="f">
Họ tên: <input id="ten" name="ten" required minlength="3"><br>
Tuổi: <input id="tuoi" name="tuoi" type="number" min="18" max="60" required><br>
Mã SV: <input id="ma" name="ma" pattern="[0-9]{8}" required>
</form>
<script>
const ten = document.getElementById("ten");
const tuoi = document.getElementById("tuoi");
const ma = document.getElementById("ma");
console.log("Bỏ trống:", ten.checkValidity(), tuoi.checkValidity(), ma.checkValidity());
ten.value = "Lan"; tuoi.value = "20"; ma.value = "11223344";
console.log("Điền đúng:", ten.checkValidity(), tuoi.checkValidity(), ma.checkValidity());
tuoi.value = "70"; ma.value = "abc";
console.log("Tuổi 70, mã 'abc':", tuoi.checkValidity(), ma.checkValidity());
console.log("Cả form hợp lệ?", document.getElementById("f").checkValidity());
</script>Cách đọc: không có dòng if nào kiểm tra độ dài hay khoảng giá trị; trình duyệt tự làm việc đó dựa trên các thuộc tính required, minlength, min, max, pattern. Mã JavaScript chỉ hỏi kết quả. Phần tử form cũng có checkValidity() để kiểm tra toàn bộ biểu mẫu một lần.
setCustomValidity(thongBao) đặt thông báo lỗi riêng. Khi truyền một chuỗi khác rỗng, phần tử bị coi là không hợp lệ (validity.customError bằng true); để xóa lỗi, truyền chuỗi rỗng "".
<form>
Mật khẩu: <input id="mk1" type="password" minlength="6">
Nhập lại: <input id="mk2" type="password">
</form>
<script>
const mk1 = document.getElementById("mk1");
const mk2 = document.getElementById("mk2");
function kiemTraKhop() {
if (mk1.value !== mk2.value) {
mk2.setCustomValidity("Hai mật khẩu chưa khớp");
} else {
mk2.setCustomValidity(""); // xóa lỗi tùy chỉnh
}
return mk2.checkValidity();
}
mk1.value = "abc123"; mk2.value = "abc124";
console.log("Khác nhau:", kiemTraKhop(), "| lỗi:", mk2.validationMessage);
mk2.value = "abc123";
console.log("Giống nhau:", kiemTraKhop(), "| lỗi:", JSON.stringify(mk2.validationMessage));
</script>Cảnh báo: Quên gọi
setCustomValidity("")để xóa lỗi là sai lầm rất hay gặp: ô nhập sẽ bị "kẹt" ở trạng thái không hợp lệ ngay cả khi người dùng đã sửa đúng.
1.3. Đối tượng validity
Với các kiểu dữ liệu phức tạp hơn, bạn có thể dùng thêm các thuộc tính của đối tượng validity (có trong mọi phần tử nhập liệu) để biết chính xác ràng buộc nào bị vi phạm:
| Thuộc tính | Ý nghĩa |
|---|---|
customError |
true nếu được thiết lập cảnh báo tùy chỉnh |
patternMismatch |
true nếu dữ liệu nhập sai với khuôn mẫu |
rangeOverflow |
true nếu dữ liệu lớn hơn giá trị lớn nhất cho phép (thuộc tính max) |
rangeUnderflow |
true nếu dữ liệu nhỏ hơn giá trị nhỏ nhất cho phép (thuộc tính min) |
stepMismatch |
true nếu dữ liệu sai thuộc tính bước (step) |
tooLong |
true nếu giá trị vượt quá chiều dài cho phép |
typeMismatch |
true nếu dữ liệu bị sai kiểu |
valueMissing |
true nếu chưa có dữ liệu |
valid |
true nếu dữ liệu là hợp lệ |
Ví dụ của giáo trình kiểm tra số người dùng nhập có nằm trong khoảng cho phép (từ 20 đến 100) hay không. Bên dưới là phiên bản chạy được; đoạn mã gọi myFunction() sau khi gán thử giá trị.
Number: <input id="id1" type="number" max="100" min="20">
<input type="submit" value="Submit" onclick="myFunction()">
<p id="demo"></p>
<script>
function myFunction() {
var txt = "";
if (document.getElementById("id1").validity.rangeOverflow) {
txt = "Value too large";
}
else if (document.getElementById("id1").validity.rangeUnderflow) {
txt = "Value too small";
}
document.getElementById("demo").innerHTML = txt;
}
for (const v of ["150", "5", "50"]) {
document.getElementById("id1").value = v;
myFunction();
console.log(v, "→", JSON.stringify(document.getElementById("demo").innerHTML));
}
</script>Với giá trị 50 thì txt giữ nguyên chuỗi rỗng: không quá lớn, cũng không quá nhỏ. Ví dụ sau liệt kê toàn bộ cờ của validity cho một ô email, giúp bạn hình dung từng thuộc tính:
const form = document.createElement("form");
form.innerHTML = '<input id="e" type="email" required maxlength="20" pattern=".+@neu\\.edu\\.vn">';
document.body.appendChild(form);
const e = form.elements.e;
const cacCo = ["valueMissing", "typeMismatch", "patternMismatch", "customError", "valid"];
function inCo(nhan) {
console.log(nhan + " [" + JSON.stringify(e.value) + "]:",
cacCo.filter(k => e.validity[k]).join(", "));
}
inCo("Rỗng");
e.value = "khongphaiemail"; inCo("Sai kiểu");
e.value = "[email protected]"; inCo("Sai khuôn mẫu");
e.value = "[email protected]"; inCo("Đúng");
e.setCustomValidity("Email này đã tồn tại"); inCo("Có lỗi tùy chỉnh");Cách đọc: valid chỉ bằng true khi mọi cờ lỗi đều false. Một ô có thể vi phạm cùng lúc nhiều ràng buộc (ví dụ email sai khuôn mẫu và đồng thời bị setCustomValidity), vì vậy hãy kiểm tra từng cờ khi cần nói rõ cho người dùng biết lỗi nằm ở đâu.
Ngoài kiểm tra bằng thuộc tính HTML, bạn vẫn có thể tự viết hàm kiểm tra bằng JavaScript thuần với biểu thức chính quy. Cách này linh hoạt hơn khi quy tắc phức tạp (ví dụ số điện thoại Việt Nam, email của trường):
function kiemTraEmail(s) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
}
function kiemTraSDT(s) {
return /^0\d{9}$/.test(s);
}
const mau = [["[email protected]", "0912345678"], ["lan@neu", "912345678"], ["", "09123456789"]];
for (const [email, sdt] of mau) {
console.log(JSON.stringify(email), kiemTraEmail(email), "|", JSON.stringify(sdt), kiemTraSDT(sdt));
}1.4. Lưu ý về an toàn
Mọi cách xác thực trình bày ở trên đều chạy trên trình duyệt của người dùng, nên chỉ có tác dụng cải thiện trải nghiệm — báo lỗi ngay mà không phải chờ gửi dữ liệu lên máy chủ. Chúng không phải là biện pháp bảo mật: người dùng có thể tắt JavaScript, sửa mã HTML bằng công cụ dành cho nhà phát triển (DevTools), hoặc gửi thẳng một yêu cầu HTTP mà không qua trang Web. Vì vậy, mọi dữ liệu nhận được đều phải được kiểm tra lại một lần nữa ở phía máy chủ trước khi lưu vào cơ sở dữ liệu.
// Mô phỏng "kẻ tấn công" bỏ qua form, gửi thẳng dữ liệu xấu tới máy chủ
function mayChuNhanDangKy(payload) {
// Máy chủ KHÔNG tin trình duyệt: kiểm tra lại toàn bộ
const loi = [];
if (typeof payload.name !== "string" || payload.name.trim() === "") loi.push("name rỗng");
if (!Number.isInteger(payload.age) || payload.age < 0) loi.push("age không hợp lệ");
return loi.length ? { status: 400, loi } : { status: 201 };
}
console.log(mayChuNhanDangKy({ name: "An", age: 20 }));
console.log(mayChuNhanDangKy({ name: " ", age: -5 }));Cảnh báo: Thuộc tính
requiredhaypatterncó thể bị xóa chỉ bằng vài thao tác trong DevTools. Đừng bao giờ coi kiểm tra phía trình duyệt là hàng rào bảo vệ dữ liệu.
Mẹo: Quy tắc thực tế: kiểm tra ở trình duyệt để thân thiện, kiểm tra ở máy chủ để an toàn. Hai lớp kiểm tra bổ sung cho nhau chứ không thay thế nhau.
Giáo trình có công cụ minh họa "Xác thực biểu mẫu" tại fit.neu.edu.vn/smartdoc/congcu/web-xac-thuc-bieu-mau.html để bạn thử thêm.
Vừa học xong xác thực dữ liệu, hãy làm các bài sau để luyện kiểm tra email, số điện thoại và dữ liệu JSON gửi lên theo quy tắc cho trước.
2. Đồ họa trong JavaScript
Giáo trình: Chương 6 — mục "Đồ họa trong JavaScript".
Phần tử <canvas> được sử dụng để vẽ đồ họa. Nó chỉ là một vùng chứa (container) để hiển thị các đối tượng đồ họa; bạn phải dùng mã JavaScript để vẽ. Đối tượng bối cảnh vẽ (context) lấy từ canvas cung cấp các phương thức để vẽ đường thẳng, hình chữ nhật, hình tròn, văn bản và hình ảnh. Hãy luôn chỉ định thuộc tính id và xác định kích thước của khung vẽ (width, height).
Ví dụ về một <canvas> cơ bản, chưa chứa đối tượng nào (chỉ có viền để nhìn thấy khung):
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;">
</canvas>
<script>
const c = document.getElementById("myCanvas");
console.log("Kích thước khung vẽ:", c.width + "x" + c.height);
</script>Lưu ý: Kích thước đặt bằng thuộc tính
width/heightlà kích thước vùng vẽ. Nếu chỉ đặt bằng CSS, canvas bị co giãn như một bức ảnh và hình vẽ bị mờ hoặc méo.
Mở rộng: Hệ tọa độ của canvas có gốc
(0, 0)ở góc trên bên trái, trụcxhướng sang phải, trụcyhướng xuống dưới. Điểm(200, 100)của canvas trên là góc dưới bên phải.
Trong các ví dụ dưới đây, đoạn mã luôn kiểm tra if (ctx) trước khi vẽ: trình duyệt thật vẽ bình thường; môi trường kiểm tra tự động không có đồ họa thật nên chỉ báo đã tạo được phần tử.
2.1. Vẽ đường thẳng
Lấy canvas bằng getElementById, lấy bối cảnh vẽ bằng getContext("2d"), sau đó moveTo đặt điểm bắt đầu, lineTo đặt điểm kết thúc và stroke() để vẽ nét.
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;"></canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
if (ctx) {
ctx.moveTo(0, 0);
ctx.lineTo(200, 100);
ctx.stroke();
console.log("Đã vẽ đường chéo từ (0,0) tới (200,100)");
} else {
console.log("Môi trường này không có đồ họa canvas thật; trình duyệt sẽ vẽ đường chéo.");
}
</script>2.2. Vẽ hình tròn
Dùng beginPath() để bắt đầu một đường mới, arc(x, y, bánKính, gócĐầu, gócCuối) để vẽ cung tròn (góc tính bằng radian; cả đường tròn là 0 tới 2 * Math.PI) và stroke() để vẽ nét.
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;"></canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
if (ctx) {
ctx.beginPath();
ctx.arc(95, 50, 40, 0, 2 * Math.PI);
ctx.stroke();
console.log("Đã vẽ hình tròn tâm (95,50), bán kính 40");
} else {
console.log("Không có canvas thật; tâm (95,50), bán kính 40, chu vi ≈", (2 * Math.PI * 40).toFixed(1));
}
</script>2.3. Vẽ văn bản và văn bản có viền
font đặt phông chữ, fillText(chuỗi, x, y) vẽ chữ đặc, còn strokeText(chuỗi, x, y) vẽ chữ chỉ có viền (rỗng bên trong). Tọa độ (x, y) là điểm bắt đầu của đường cơ sở dòng chữ.
<canvas id="c1" width="200" height="70" style="border:1px solid #000"></canvas>
<canvas id="c2" width="200" height="70" style="border:1px solid #000"></canvas>
<script>
function ve(id, cheDo) {
var ctx = document.getElementById(id).getContext("2d");
if (!ctx) { console.log(id + ": không có canvas thật (" + cheDo + ")"); return; }
ctx.font = "30px Arial";
if (cheDo === "fill") ctx.fillText("Hello World", 10, 50);
else ctx.strokeText("Hello World", 10, 50);
console.log(id + ": đã vẽ chữ " + cheDo);
}
ve("c1", "fill");
ve("c2", "stroke");
</script>2.4. Vẽ gradient tuyến tính và gradient tròn
Gradient (chuyển màu) được tạo trước bằng createLinearGradient(x0, y0, x1, y1) hoặc createRadialGradient(x0, y0, r0, x1, y1, r1), thêm các điểm màu bằng addColorStop(vịTrí, màu) (vị trí từ 0 đến 1), rồi gán vào fillStyle và vẽ hình chữ nhật bằng fillRect(x, y, rộng, cao).
<canvas id="lin" width="200" height="100" style="border:1px solid #000"></canvas>
<canvas id="rad" width="200" height="100" style="border:1px solid #000"></canvas>
<script>
var a = document.getElementById("lin").getContext("2d");
if (a) {
var grd = a.createLinearGradient(0, 0, 200, 0);
grd.addColorStop(0, "red");
grd.addColorStop(1, "white");
a.fillStyle = grd;
a.fillRect(10, 10, 150, 80);
}
var b = document.getElementById("rad").getContext("2d");
if (b) {
var grd2 = b.createRadialGradient(75, 50, 5, 90, 60, 100);
grd2.addColorStop(0, "red");
grd2.addColorStop(1, "white");
b.fillStyle = grd2;
b.fillRect(10, 10, 150, 80);
}
console.log("Gradient tuyến tính:", a ? "đã vẽ" : "bỏ qua (không có canvas thật)");
console.log("Gradient tròn:", b ? "đã vẽ" : "bỏ qua (không có canvas thật)");
</script>Cách đọc: gradient tuyến tính chạy từ điểm (0, 0) tới (200, 0), tức là từ trái sang phải, màu đỏ ở đầu và trắng ở cuối. Gradient tròn lan từ một vòng tròn nhỏ (bán kính 5) sang một vòng tròn lớn (bán kính 100).
2.5. Vẽ hình ảnh
drawImage(ảnh, x, y) vẽ một ảnh lên canvas. Ảnh phải tải xong trước khi vẽ, vì vậy đoạn mã giáo trình (ảnh có sẵn trong HTML) thường được đặt trong sự kiện load.
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000"></canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var img = new Image();
img.onload = function () {
if (ctx) ctx.drawImage(img, 10, 10);
console.log("Ảnh đã tải xong, kích thước", img.width + "x" + img.height);
};
img.src = "data:image/svg+xml;utf8," + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="60" height="40"><rect width="60" height="40" fill="orange"/></svg>');
console.log("Đã yêu cầu tải ảnh; vẽ sẽ được thực hiện trong img.onload");
</script>Mẹo: Nếu gọi
drawImagengay khi ảnh chưa tải xong, bạn sẽ không thấy gì và cũng không có lỗi. Luôn đặt lệnh vẽ ảnh trongonload.
Tóm tắt các lệnh vẽ vừa học:
| Việc cần làm | Lệnh |
|---|---|
| Lấy bối cảnh vẽ | canvas.getContext("2d") |
| Đường thẳng | moveTo, lineTo, stroke |
| Hình tròn | beginPath, arc, stroke |
| Văn bản | font, fillText, strokeText |
| Gradient | createLinearGradient, createRadialGradient, addColorStop, fillStyle, fillRect |
| Hình ảnh | drawImage |
Ví dụ thực tế: Các biểu đồ doanh thu trên trang quản trị cửa hàng, ô xoay "chọn ảnh đại diện", trò chơi nhỏ trên Web hay chữ ký điện tử trên màn hình cảm ứng đều được vẽ bằng canvas.
Phần chuyển động (animation) trên Web kết hợp canvas hoặc CSS với bộ đếm giờ setInterval; bạn sẽ làm trong Bài 6.1 ở phần "Thực hành theo giáo trình".
3. Làm việc với JSON
Giáo trình: Chương 6 — mục "Làm việc với JSON" (Khái niệm về JSON; JSON và XML; Kiểu dữ liệu JSON).
3.1. Khái niệm về JSON
JSON là viết tắt của JavaScript Object Notation. Đây là một định dạng dùng để trao đổi và lưu trữ dữ liệu. Cú pháp JSON có nguồn gốc từ cú pháp ký hiệu đối tượng JavaScript, nhưng định dạng JSON chỉ là văn bản. Mã để đọc và tạo dữ liệu JSON có thể được viết bằng bất kỳ ngôn ngữ lập trình nào (JavaScript, Python, Java, C#, PHP...).
JSON rất giống cách viết đối tượng trong JavaScript, nhưng chặt chẽ hơn: khóa bắt buộc đặt trong nháy kép, chuỗi bắt buộc dùng nháy kép, không có hàm, không có undefined và không có chú thích. Do sự giống nhau này, một chương trình JavaScript có thể dễ dàng chuyển dữ liệu JSON thành các đối tượng JavaScript gốc.
Cú pháp JSON có các quy tắc sau:
- Dữ liệu nằm trong cặp tên/giá trị.
- Dữ liệu được phân tách bằng dấu phẩy.
- Dấu ngoặc nhọn
{}để khai báo đối tượng. - Dấu ngoặc vuông
[]để khai báo mảng.
Ví dụ một tệp JSON:
{
"employees":[
{"firstName":"John", "lastName":"Doe"},
{"firstName":"Anna", "lastName":"Smith"},
{"firstName":"Peter", "lastName":"Jones"}
]
}Phòng thí nghiệm JSON: parse & stringify
văn bản JSON ⇄ đối tượng JavaScriptMô phỏng trên cho bạn gõ chuỗi JSON rồi xem đối tượng thu được sau JSON.parse() (và ngược lại với JSON.stringify()). Hãy thử cố tình gõ sai (dùng nháy đơn, thừa dấu phẩy cuối) để thấy thông báo lỗi.
Cách sử dụng phổ biến của JSON là đọc dữ liệu từ máy chủ Web, sau đó dùng phương thức JSON.parse() để chuyển chuỗi thành đối tượng. Đây là ví dụ của giáo trình (chuỗi JSON được ghép từ nhiều đoạn):
let text = '{ "employees" : [' +
'{ "firstName":"John" , "lastName":"Doe" },' +
'{ "firstName":"Anna" , "lastName":"Smith" },' +
'{ "firstName":"Peter" , "lastName":"Jones" } ]}';
let obj = JSON.parse(text);
console.log(typeof text, "→", typeof obj);
console.log("Số nhân viên:", obj.employees.length);
console.log(obj.employees[1].firstName + " " + obj.employees[1].lastName);Hiển thị dữ liệu lên trang bằng DOM (đoạn mã trong giáo trình):
<p id="demo"></p>
<script>
let text = '{ "employees" : [' +
'{ "firstName":"John" , "lastName":"Doe" },' +
'{ "firstName":"Anna" , "lastName":"Smith" },' +
'{ "firstName":"Peter" , "lastName":"Jones" } ]}';
let obj = JSON.parse(text);
document.getElementById("demo").innerHTML =
obj.employees[1].firstName + " " + obj.employees[1].lastName;
console.log(document.getElementById("demo").innerHTML);
</script>Chiều ngược lại: nếu dữ liệu được lưu trong một đối tượng JavaScript, bạn có thể chuyển nó thành chuỗi JSON bằng JSON.stringify() rồi gửi lên máy chủ. Giáo trình gửi bằng cách ghép vào địa chỉ (window.location = "demo_json.php?x=" + myJSON); ví dụ dưới đây chỉ in địa chỉ ra để bạn xem (có mã hóa ký tự đặc biệt bằng encodeURIComponent):
var myObj = { name: "John", age: 31, city: "New York" };
var myJSON = JSON.stringify(myObj);
console.log(typeof myJSON, myJSON);
// Giáo trình: window.location = "demo_json.php?x=" + myJSON;
var url = "demo_json.php?x=" + encodeURIComponent(myJSON);
console.log(url);JSON.stringify() còn nhận tham số thứ ba để in đẹp (thụt lề), rất tiện khi bạn gỡ lỗi hoặc ghi tệp cấu hình:
const sv = { ten: "Lan", lop: "CNTT 63A", diem: [8, 9.5] };
console.log(JSON.stringify(sv));
console.log(JSON.stringify(sv, null, 2));Mẹo: Thông báo lỗi
Unexpected token ... in JSON at position Nkhi gọiJSON.parse()gần như luôn do chuỗi không phải JSON hợp lệ (nháy đơn, thừa dấu phẩy, khóa không có nháy, hoặc máy chủ trả về trang HTML thay vì JSON). Hãy bọcJSON.parse()trongtry/catch.
const chuoiThu = [
'{"ten": "An"}', // đúng
"{'ten': 'An'}", // sai: nháy đơn
'{ten: "An"}', // sai: khóa không có nháy
'{"ten": "An",}', // sai: thừa dấu phẩy cuối
'{"ten": undefined}' // sai: JSON không có undefined
];
for (const s of chuoiThu) {
try {
JSON.parse(s);
console.log("Hợp lệ: ", s);
} catch (e) {
console.log("Không hợp lệ:", s, "→", e.name);
}
}Bảng sau tóm tắt khác biệt giữa đối tượng JavaScript và JSON (mục "JSON và đối tượng JavaScript" của giáo trình):
| Điểm so sánh | Đối tượng JavaScript | JSON |
|---|---|---|
| Bản chất | Cấu trúc dữ liệu trong bộ nhớ | Chuỗi văn bản |
| Khóa | Có thể không có nháy | Bắt buộc nháy kép |
| Chuỗi | Nháy đơn hoặc kép | Chỉ nháy kép |
Hàm, undefined |
Được phép | Không được phép |
| Chú thích | Được phép | Không được phép |
Bạn vừa học cách đọc/ghi JSON, hãy làm các bài sau để kiểm tra chuỗi JSON, định dạng lại, tính toán trên dữ liệu JSON.
3.2. JSON và XML
Cả JSON và XML đều có thể dùng với công dụng tương tự nhau nhưng cú pháp khác nhau. Ví dụ dưới đây dùng cả hai để lưu thông tin của ba nhân viên.
JSON:
{"employees":[
{ "firstName":"John", "lastName":"Doe" },
{ "firstName":"Anna", "lastName":"Smith" },
{ "firstName":"Peter", "lastName":"Jones" }
]}XML:
<employees>
<employee>
<firstName>John</firstName> <lastName>Doe</lastName>
</employee>
<employee>
<firstName>Anna</firstName> <lastName>Smith</lastName>
</employee>
<employee>
<firstName>Peter</firstName> <lastName>Jones</lastName>
</employee>
</employees>So sánh:
- JSON giống XML: con người đều có thể đọc hiểu; đều lưu trữ dữ liệu theo kiểu phân cấp; đều có thể phân tích và được rất nhiều ngôn ngữ lập trình sử dụng.
- JSON không giống XML: JSON không dùng thẻ kết thúc; JSON ngắn hơn; JSON đọc và ghi nhanh hơn; JSON có thể dùng mảng.
JSON được ưa dùng hơn XML trong các ứng dụng AJAX vì XML khó phân tích hơn, còn JSON có cú pháp gần với cú pháp đối tượng của JavaScript nên xử lý nhanh và đơn giản hơn.
Ví dụ sau đọc cùng một dữ liệu từ XML (phải duyệt cây DOM) và từ JSON (truy cập thuộc tính trực tiếp) để bạn thấy sự khác biệt về độ phức tạp:
const xml = "<employees><employee><firstName>John</firstName><lastName>Doe</lastName></employee>" +
"<employee><firstName>Anna</firstName><lastName>Smith</lastName></employee></employees>";
const json = '{"employees":[{"firstName":"John","lastName":"Doe"},{"firstName":"Anna","lastName":"Smith"}]}';
// XML: phải phân tích thành cây DOM rồi tìm từng thẻ
const doc = new DOMParser().parseFromString(xml, "application/xml");
const dsXml = doc.getElementsByTagName("employee");
console.log("XML :", dsXml[1].getElementsByTagName("firstName")[0].textContent);
// JSON: một lệnh parse rồi dùng như đối tượng bình thường
const dsJson = JSON.parse(json).employees;
console.log("JSON:", dsJson[1].firstName);
console.log("Độ dài chuỗi — XML:", xml.length, "| JSON:", json.length);3.3. Kiểu dữ liệu JSON
Trong JSON, giá trị phải là một trong các kiểu: Chuỗi (string), Số (number), Đối tượng (object), Mảng (array), Boolean, hoặc Rỗng (null). Khác với đối tượng JavaScript, giá trị trong JSON không thể là một hàm và không thể là undefined.
| Kiểu | Quy tắc | Ví dụ |
|---|---|---|
| Chuỗi | Phải viết trong dấu ngoặc kép | { "name":"John" } |
| Số | Số nguyên hoặc số thực dấu chấm động | { "age":30 } |
| Đối tượng | Cũng phải theo đúng quy tắc của đối tượng JSON | { "employee":{ "name":"John", "age":30, "city":"New York" } } |
| Mảng | Dùng dấu ngoặc vuông | { "employees":[ "John", "Anna", "Peter" ] } |
| Boolean | true hoặc false |
{ "sale":true } |
| Rỗng (null) | Giá trị null |
{ "middlename":null } |
42 3.14 NaN"Minh" 'NEU'true falsenullundefinedSymbol("id")9007199254740993n{ ten: "Minh", tuoi: 21 }[ 10, 20, 30 ](a, b) => a + bKiểu nguyên thủy lưu trực tiếp giá trị (sao chép là tạo bản mới); kiểu đối tượng lưu tham chiếu tới cùng một đối tượng. typeof cho biết kiểu.
Hình trên nhắc lại các kiểu dữ liệu của JavaScript (Buổi 10–11). So sánh với bảng: JSON không có undefined, hàm, ngày giờ hay NaN; chúng được chuyển đổi hoặc bị bỏ khi stringify.
const dl = {
name: "John", // chuỗi
age: 30, // số
employee: { city: "New York" }, // đối tượng
employees: ["John", "Anna", "Peter"], // mảng
sale: true, // boolean
middlename: null, // rỗng
// Những giá trị JSON KHÔNG hỗ trợ:
chao: function () { return "hi"; },
chuaCo: undefined,
hong: NaN,
ngay: new Date(0)
};
const chuoi = JSON.stringify(dl);
console.log(chuoi);
const lai = JSON.parse(chuoi);
console.log("Có hàm chao?", "chao" in lai, "| có chuaCo?", "chuaCo" in lai);
console.log("NaN thành:", lai.hong, "| Date thành:", typeof lai.ngay, lai.ngay);
for (const k of ["name", "age", "employee", "employees", "sale", "middlename"]) {
console.log(k, "→", lai[k] === null ? "null" : Array.isArray(lai[k]) ? "array" : typeof lai[k]);
}Cách đọc: hàm và undefined bị bỏ hẳn khỏi chuỗi JSON, NaN thành null, ngày giờ thành chuỗi. Vì vậy sau khi stringify rồi parse, bạn không nhận lại đúng đối tượng ban đầu.
Cảnh báo: Đừng dùng
JSON.parse(JSON.stringify(x))để "sao chép sâu" đối tượng có chứa hàm, ngày giờ hoặcundefined: dữ liệu bị mất hoặc đổi kiểu.
Mở rộng:
JSON.parse(chuoi, reviver)nhận thêm hàmreviverđể biến đổi từng giá trị khi đọc (ví dụ đổi chuỗi ngày thànhDate);JSON.stringify(doiTuong, replacer, thuLe)nhậnreplacerđể lọc thuộc tính vàthuLeđể định dạng.
Một cách dùng phổ biến khác: lưu danh sách vào bộ nhớ trình duyệt dưới dạng chuỗi JSON và đọc lại. Ví dụ sau tự định nghĩa một "kho" đơn giản thay cho localStorage để chạy được trong bài:
const kho = {}; // thay cho localStorage trong ví dụ
const gioHang = [{ ma: "A1", sl: 2 }, { ma: "B7", sl: 1 }];
kho.gioHang = JSON.stringify(gioHang); // chỉ lưu được chuỗi
console.log("Đã lưu:", kho.gioHang);
const doc = JSON.parse(kho.gioHang); // đọc lại thành mảng
doc.push({ ma: "C3", sl: 5 });
console.log("Tổng số lượng:", doc.reduce((t, x) => t + x.sl, 0));4. Giao tiếp với máy chủ
Giáo trình: Chương 6 — mục "Giao tiếp với máy chủ" (Khái niệm về AJAX; Các bước để thực hiện AJAX; Các thuộc tính và phương thức; Sử dụng
fetch()vàasync/await; Chính sách cùng nguồn và CORS).
4.1. Khái niệm về AJAX
AJAX (Asynchronous JavaScript And XML) là một kỹ thuật cho phép mã JavaScript chạy trên trình duyệt có thể:
- Đọc dữ liệu từ một máy chủ Web — sau khi trang đã được tải về.
- Cập nhật một trang Web mà không cần tải lại trang.
- Gửi dữ liệu đến một máy chủ Web — ở chế độ nền.
async function layBaiViet() { try { const res = await fetch(url) // ② ③ if (!res.ok) throw new Error(res.status) const data = await res.json() // ④ ⑤ demo.innerHTML = data.title // ⑥ } catch (err) { console.log(err.message) } }
{
"id": 1,
"title": "Xin chào Web"
}Access-Control-Allow-Origin (CORS), nếu không trình duyệt chặn kết quả.fetch() gửi yêu cầu ở chế độ nền và trả về Promise ngay; trong lúc chờ, trang vẫn dùng được. Khi máy chủ phản hồi JSON, mã chỉ cập nhật đúng phần cần đổi của trang.
Sơ đồ trên mô tả luồng bất đồng bộ: trang gửi yêu cầu, tiếp tục làm việc khác (người dùng vẫn cuộn, bấm được), và khi máy chủ trả lời thì một hàm được gọi lại để cập nhật phần tương ứng của trang.
Lưu ý: AJAX là một cái tên dễ gây hiểu nhầm. Ứng dụng AJAX có thể dùng XML để vận chuyển dữ liệu, nhưng ngày nay dữ liệu thường được truyền dưới dạng văn bản thuần hoặc văn bản JSON. AJAX cho phép trang Web cập nhật không đồng bộ bằng cách trao đổi dữ liệu với máy chủ ở chế độ nền, nhờ đó có thể cập nhật một phần của trang mà không tải lại toàn bộ.
Ví dụ của giáo trình: một nút "Change Content" thay nội dung thẻ div bằng dữ liệu tải từ máy chủ (ở đây là cơ sở dữ liệu Firebase). Mã gốc gọi máy chủ thật nên không chạy trong bài này:
<!DOCTYPE html>
<html>
<body>
<div id="demo">
<h2>Let AJAX change this text</h2>
<button type="button" onclick="loadData()">Change Content</button>
</div>
<script>
function loadData() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("demo").innerHTML = this.responseText;
}
};
xhttp.open("GET", "https://myWebsite-3db42-default-rtdb.firebaseio.com/Website.json", true);
xhttp.send();
}
</script>
</body>
</html>Để bạn chạy được ngay, bên dưới là cùng cấu trúc nhưng máy chủ giả: lớp FakeXHR có đủ các thành viên mà giáo trình dùng (open, send, readyState, status, responseText, onreadystatechange) và trả lời từ một "bảng đường dẫn" thay vì mạng thật. Trong dự án thật bạn chỉ việc thay FakeXHR bằng XMLHttpRequest.
<div id="demo">
<h2>Let AJAX change this text</h2>
<button type="button" onclick="loadData()">Change Content</button>
</div>
<script>
// ===== Máy chủ giả (chỉ để chạy thử trong bài) =====
const DATA = {
"/Website.json": { status: 200, statusText: "OK", body: '{"title":"Thiết kế Web","chuong":6}' },
"/ajax_info.txt": { status: 200, statusText: "OK", body: "Nội dung tải bằng AJAX" }
};
class FakeXHR {
constructor() { this.readyState = 0; this.status = 0; this.statusText = ""; this.responseText = ""; }
open(method, url) { this.method = method; this.url = url; this._doi(1); }
send(body) {
this._doi(2);
setTimeout(() => {
const r = DATA[this.url] || { status: 404, statusText: "Not Found", body: "" };
this.status = r.status; this.statusText = r.statusText; this.responseText = r.body;
this._doi(3); this._doi(4);
}, 20);
}
_doi(n) { this.readyState = n; if (this.onreadystatechange) this.onreadystatechange(); }
}
// ===== Mã giống giáo trình =====
function loadData() {
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("demo").innerHTML = this.responseText;
console.log("Đã nhận:", this.responseText);
}
};
xhttp.open("GET", "/Website.json", true);
xhttp.send();
}
loadData(); // mô phỏng người dùng bấm nút
</script>Cách đọc: sau khoảng 20 ms, onreadystatechange được gọi với readyState chạy từ 2 → 3 → 4; khi readyState == 4 và status == 200, nội dung thẻ div được thay bằng chuỗi nhận về. Trang không tải lại.
4.2. Các bước để thực hiện AJAX
Bước 1: Tạo một đối tượng XMLHttpRequest.
let xhttp = new XMLHttpRequest();Bước 2: Gửi một yêu cầu đến máy chủ bằng hai phương thức open() và send() của đối tượng này.
xhttp.open("GET", "ajax_info.txt", true);
xhttp.send();Tham số của open(phươngThức, địaChỉ, bấtĐồngBộ): phương thức HTTP (GET, POST...), địa chỉ cần gọi, và true để chạy bất đồng bộ. Một yêu cầu POST đơn giản:
xhttp.open("POST", "demo_post.asp", true);
xhttp.send();Để dữ liệu POST được gửi đi giống dữ liệu tạo ra từ một biểu mẫu HTML, ta khai báo Content-type là application/x-www-form-urlencoded bằng setRequestHeader(), sau đó truyền dữ liệu vào send():
xhttp.open("POST", "ajax_test.asp", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("fname=Henry&lname=Ford");Bước 3: Định nghĩa một hàm được thực thi khi yêu cầu nhận được câu trả lời, trong thuộc tính onreadystatechange của đối tượng:
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("demo").innerHTML = this.responseText;
}
};
xhttp.open("GET", "ajax_info.txt", true);
xhttp.send();Bước 3 phải được đặt trước send() để không bỏ lỡ sự kiện. Dưới đây là ví dụ mô phỏng POST dạng biểu mẫu; máy chủ giả tách chuỗi fname=Henry&lname=Ford bằng URLSearchParams và trả lời:
class FakeXHR {
constructor() { this.readyState = 0; this.status = 0; this.responseText = ""; this._h = {}; }
open(m, u) { this.method = m; this.url = u; this.readyState = 1; }
setRequestHeader(k, v) { this._h[k.toLowerCase()] = v; }
send(body) {
setTimeout(() => {
const kieu = this._h["content-type"] || "";
if (this.method === "POST" && this.url === "ajax_test.asp" && kieu.includes("x-www-form-urlencoded")) {
const p = new URLSearchParams(body);
this.status = 200;
this.responseText = "Xin chào " + p.get("fname") + " " + p.get("lname");
} else { this.status = 400; this.responseText = "Yêu cầu sai"; }
this.readyState = 4; this.onreadystatechange();
}, 10);
}
}
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) console.log("Phản hồi:", this.responseText);
else console.log("Mã", this.status);
};
xhttp.open("POST", "ajax_test.asp", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("fname=Henry&lname=Ford");Các thuộc tính readyState, status và statusText cho biết trạng thái của đối tượng XMLHttpRequest:
| Thuộc tính | Mô tả |
|---|---|
readyState |
Giữ trạng thái của XMLHttpRequest: 0 yêu cầu chưa được khởi tạo; 1 đã gọi open(), kết nối tới máy chủ được thiết lập; 2 đã gọi send(), máy chủ đã nhận yêu cầu; 3 đang xử lý yêu cầu, dữ liệu phản hồi đang được tải về; 4 kết thúc yêu cầu và phản hồi sẵn sàng |
status |
200: "OK"; 403: "Forbidden"; 404: "Not Found" |
statusText |
Trả về trạng thái dưới dạng văn bản (ví dụ "OK" hoặc "Not Found") |
Hàm gán cho onreadystatechange được gọi mỗi khi readyState thay đổi. Khi readyState bằng 4 và status bằng 200 thì phản hồi đã sẵn sàng và ta có thể truy cập dữ liệu trả về. Ví dụ sau in từng thay đổi trạng thái, kể cả trường hợp lỗi 404:
const DATA = { "ajax_info.txt": [200, "OK", "Xin chào từ máy chủ"] };
class FakeXHR {
constructor() { this.readyState = 0; this.status = 0; this.statusText = ""; this.responseText = ""; }
open(m, u) { this.url = u; this._doi(1); }
send() {
this._doi(2);
setTimeout(() => {
const r = DATA[this.url] || [404, "Not Found", ""];
[this.status, this.statusText, this.responseText] = r;
this._doi(3); this._doi(4);
}, 5);
}
_doi(n) { this.readyState = n; this.onreadystatechange(); }
}
function loadDoc(url) {
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
console.log(url, "readyState =", this.readyState, "| status =", this.status);
if (this.readyState == 4 && this.status == 200) console.log("→ dữ liệu:", this.responseText);
if (this.readyState == 4 && this.status != 200) console.log("→ lỗi:", this.status, this.statusText);
};
xhttp.open("GET", url, true);
xhttp.send();
}
loadDoc("ajax_info.txt");
loadDoc("khong_co.txt");Cũng có thể truyền hàm xử lý vào như một tham số (hàm gọi lại — callback), để khi dữ liệu sẵn sàng thì hàm đó được gọi. Nhờ vậy một hàm loadDoc dùng lại được cho nhiều địa chỉ:
class FakeXHR {
constructor() { this.readyState = 0; this.status = 0; this.responseText = ""; }
open(m, u) { this.url = u; }
send() {
setTimeout(() => {
this.status = 200; this.readyState = 4;
this.responseText = "dữ liệu của " + this.url;
this.onreadystatechange();
}, 5);
}
}
loadDoc("url-1", myFunction1);
loadDoc("url-2", myFunction2);
function loadDoc(url, cFunction) {
var xhttp;
xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
cFunction(this);
}
};
xhttp.open("GET", url, true);
xhttp.send();
}
function myFunction1(xhttp) { console.log("myFunction1 nhận:", xhttp.responseText); }
function myFunction2(xhttp) { console.log("myFunction2 nhận:", xhttp.responseText); }Mẹo: Luôn kiểm tra cả hai điều kiện
readyState == 4vàstatus == 200. Chỉ kiểm trareadyState == 4thì khi máy chủ trả 404 bạn vẫn xử lý "dữ liệu" là trang báo lỗi.
Vừa học xong cách gửi yêu cầu kèm dữ liệu (địa chỉ, chuỗi truy vấn, JSON), hãy làm các bài sau để thành thạo việc đọc/dựng các thành phần của yêu cầu.
4.3. Các thuộc tính và phương thức
Thuộc tính dùng để lấy dữ liệu trả về từ máy chủ:
responseText: lấy dữ liệu phản hồi dưới dạng chuỗi.responseXML: lấy dữ liệu phản hồi dưới dạng dữ liệu XML.
Phương thức dùng để lấy thông tin tiêu đề (header) trả về từ máy chủ:
getResponseHeader(): trả về giá trị của một tiêu đề cụ thể trong phản hồi.getAllResponseHeaders(): trả về toàn bộ các tiêu đề trong phản hồi.
Thuộc tính responseText trả về chuỗi nên ta có thể gán nội dung cho phần tử: document.getElementById("demo").innerHTML = xhttp.responseText;. Đối tượng XMLHttpRequest còn có thể phân tích cú pháp XML qua responseXML. Mã trong giáo trình duyệt các thẻ ARTIST của tệp cd_catalog.xml:
xmlDoc = xhttp.responseXML;
txt = "";
x = xmlDoc.getElementsByTagName("ARTIST");
for (i = 0; i < x.length; i++) {
txt += x[i].childNodes[0].nodeValue + "<br>";
}
document.getElementById("demo").innerHTML = txt;
xhttp.open("GET", "cd_catalog.xml", true);
xhttp.send();Phiên bản chạy được (lấy XML từ một chuỗi, phân tích bằng DOMParser — cùng loại đối tượng tài liệu mà responseXML trả về):
const xmlText = "<CATALOG><CD><TITLE>Empire Burlesque</TITLE><ARTIST>Bob Dylan</ARTIST></CD>" +
"<CD><TITLE>Hide your heart</TITLE><ARTIST>Bonnie Tyler</ARTIST></CD></CATALOG>";
const xmlDoc = new DOMParser().parseFromString(xmlText, "text/xml"); // giống xhttp.responseXML
let txt = "";
const x = xmlDoc.getElementsByTagName("ARTIST");
for (let i = 0; i < x.length; i++) {
txt += x[i].childNodes[0].nodeValue + "<br>";
}
console.log(txt);Hai phương thức đọc tiêu đề phản hồi — giáo trình dùng getAllResponseHeaders() và getResponseHeader("Last-Modified") trong onreadystatechange. Ví dụ mô phỏng:
class FakeXHR {
constructor() { this.readyState = 0; this.status = 0; this._rh = {}; }
open(m, u) { this.url = u; }
send() {
setTimeout(() => {
this._rh = { "content-type": "text/plain", "last-modified": "Tue, 06 Oct 2026 08:00:00 GMT" };
this.status = 200; this.readyState = 4; this.onreadystatechange();
}, 5);
}
getResponseHeader(k) { return this._rh[k.toLowerCase()] ?? null; }
getAllResponseHeaders() { return Object.entries(this._rh).map(([k, v]) => k + ": " + v).join("\r\n"); }
}
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
console.log(this.getAllResponseHeaders());
console.log("Last-Modified =", this.getResponseHeader("Last-Modified"));
}
};
xhttp.open("GET", "ajax_info.txt", true);
xhttp.send();Tiêu đề (header), mã trạng thái (status) và thân yêu cầu là những thứ bạn vừa đọc từ XMLHttpRequest; hãy làm các bài sau để tập phân tích và phân loại chúng.
4.4. Sử dụng fetch() và async/await
Đối tượng XMLHttpRequest ra đời cùng AJAX và hiện vẫn gặp trong nhiều mã nguồn cũ. Từ năm 2015, trình duyệt cung cấp hàm fetch() với cú pháp ngắn gọn hơn hẳn, và đây là cách được dùng phổ biến hiện nay:
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(function (res) { return res.json(); })
.then(function (data) {
document.getElementById("demo").innerHTML = data.title;
})
.catch(function (err) { console.log("Lỗi: " + err); });Hàm fetch() trả về một Promise — đối tượng đại diện cho kết quả sẽ có trong tương lai. Phương thức then() nhận kết quả khi thành công, catch() nhận lỗi khi thất bại. Một Promise ở một trong ba trạng thái: đang chờ (pending), đã thành công (fulfilled) hoặc đã thất bại (rejected).
fetch() và Promise: lời gọi bất đồng bộ
chọn kịch bản rồi bấm ▶ Gọi fetch()💡 fetch() chỉ bị từ chối khi KHÔNG nhận được phản hồi (mất mạng, CORS). Mã 404/500 vẫn là phản hồi hợp lệ — hãy kiểm tra res.ok.
Mô phỏng trên cho bạn bấm "gọi API" với các kịch bản khác nhau (thành công, chậm, lỗi 404, lỗi mạng) và xem thứ tự các bước then/catch.
Vì sandbox của bài học không có mạng thật, ví dụ dưới định nghĩa fakeFetch trả về Promise giống fetch() (có ok, status, json()), để bạn chạy được nguyên văn cấu trúc then/catch:
const BAI_VIET = { 1: { id: 1, title: "Giới thiệu AJAX", userId: 7 } };
function fakeFetch(url) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const id = Number(url.split("/").pop());
if (url.startsWith("loi://")) return reject(new TypeError("Failed to fetch"));
const body = BAI_VIET[id];
resolve({
ok: !!body,
status: body ? 200 : 404,
json: function () { return Promise.resolve(body); }
});
}, 10);
});
}
fakeFetch("https://api.example.com/posts/1")
.then(function (res) { return res.json(); })
.then(function (data) { console.log("Tiêu đề:", data.title); })
.catch(function (err) { console.log("Lỗi: " + err); });
fakeFetch("loi://khong-ket-noi/posts/1")
.then(function (res) { return res.json(); })
.catch(function (err) { console.log("Lỗi: " + err); });Với cú pháp async/await, đoạn mã trên được viết lại theo lối tuần tự, dễ đọc hơn. await tạm dừng hàm async cho tới khi Promise có kết quả; try/catch bắt lỗi như mã đồng bộ. Hàm fetch() không tự báo lỗi khi máy chủ trả 404 hoặc 500, vì vậy phải kiểm tra res.ok (hoặc res.status) rồi tự throw.
async function layBaiViet() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
if (!res.ok) throw new Error("Máy chủ trả về mã " + res.status);
const data = await res.json();
document.getElementById("demo").innerHTML = data.title;
} catch (err) {
console.log("Lỗi: " + err.message);
}
}Bản chạy được với fakeFetch (thử các bài viết có và không có, và lỗi mạng):
const BAI_VIET = { 1: { id: 1, title: "Giới thiệu AJAX" } };
function fakeFetch(url) {
return new Promise((resolve, reject) => setTimeout(() => {
if (url.startsWith("loi://")) return reject(new TypeError("Failed to fetch"));
const body = BAI_VIET[Number(url.split("/").pop())];
resolve({ ok: !!body, status: body ? 200 : 404, json: () => Promise.resolve(body) });
}, 10));
}
async function layBaiViet(url) {
try {
const res = await fakeFetch(url);
if (!res.ok) throw new Error("Máy chủ trả về mã " + res.status);
const data = await res.json();
console.log("OK:", data.title);
} catch (err) {
console.log("Lỗi:", err.message);
}
}
(async () => {
await layBaiViet("https://api.example.com/posts/1");
await layBaiViet("https://api.example.com/posts/99");
await layBaiViet("loi://khong-ket-noi/posts/1");
})();Để gửi dữ liệu lên máy chủ, truyền thêm tham số thứ hai cho fetch(): phương thức, tiêu đề Content-Type và thân yêu cầu (body) là chuỗi JSON.
await fetch("https://example.com/api/sinhvien", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hoten: "Nguyễn Văn A", lop: "CNTT 63A" })
});// Máy chủ giả nhận POST JSON và trả về bản ghi kèm id
const dsSinhVien = [];
function fakeFetch(url, opts = {}) {
return new Promise((resolve) => setTimeout(() => {
if (opts.method === "POST" && opts.headers["Content-Type"] === "application/json") {
const sv = JSON.parse(opts.body);
sv.id = dsSinhVien.length + 1;
dsSinhVien.push(sv);
resolve({ ok: true, status: 201, json: async () => sv });
} else resolve({ ok: false, status: 400, json: async () => ({}) });
}, 5));
}
(async () => {
const res = await fakeFetch("https://example.com/api/sinhvien", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hoten: "Nguyễn Văn A", lop: "CNTT 63A" })
});
console.log("Mã:", res.status);
console.log("Bản ghi nhận lại:", await res.json());
console.log("Số bản ghi trên máy chủ:", dsSinhVien.length);
})();Bảng so sánh hai cách làm:
| Tiêu chí | XMLHttpRequest |
fetch() |
|---|---|---|
| Ra đời | Cùng AJAX, mã nguồn cũ vẫn dùng nhiều | Từ năm 2015 |
| Cú pháp | Dài: tạo đối tượng, open, send, onreadystatechange |
Ngắn gọn, trả về Promise |
| Xử lý kết quả | Hàm gọi lại, kiểm tra readyState + status |
then()/catch() hoặc async/await |
| Lỗi HTTP 404/500 | Đọc status |
Không tự lỗi, phải kiểm tra res.ok |
Cảnh báo:
awaitchỉ dùng được bên trong hàmasync(hoặc mô-đun). Nếu bạn quênasyncở trướcfunction, trình duyệt báo lỗi cú pháp.
4.5. Chính sách cùng nguồn và CORS
Vì lý do an toàn, trình duyệt áp dụng chính sách cùng nguồn (same-origin policy): mã JavaScript của một trang chỉ được đọc dữ liệu từ những địa chỉ có cùng giao thức, cùng tên miền và cùng cổng với trang đó. Khi gọi sang một tên miền khác, trình duyệt sẽ chặn kết quả và báo lỗi "blocked by CORS policy" trong cửa sổ Console.
Để cho phép, máy chủ được gọi phải trả về tiêu đề Access-Control-Allow-Origin kèm địa chỉ được phép truy cập (CORS — Cross-Origin Resource Sharing). Đây là thiết lập ở phía máy chủ, người lập trình phía máy khách không thể tự khắc phục. Các dịch vụ công khai như Firebase hay các API mở đã bật sẵn tiêu đề này nên các ví dụ trong chương hoạt động bình thường.
function cungNguon(trang, apiUrl) {
const a = new URL(trang), b = new URL(apiUrl);
return a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
}
const trang = "https://fit.neu.edu.vn/codelab/index.html";
const thu = [
"https://fit.neu.edu.vn/codelab/api/lop", // cùng nguồn
"http://fit.neu.edu.vn/codelab/api/lop", // khác giao thức
"https://api.neu.edu.vn/lop", // khác tên miền
"https://fit.neu.edu.vn:8443/codelab/api/lop" // khác cổng
];
for (const u of thu) console.log(cungNguon(trang, u) ? "CÙNG NGUỒN " : "KHÁC NGUỒN ", u);Mẹo: Khi thấy "blocked by CORS policy", đừng cố sửa mã JavaScript. Hãy kiểm tra xem máy chủ có gửi
Access-Control-Allow-Originhay không, hoặc nhờ người quản trị máy chủ cấu hình.
Mở rộng: Với yêu cầu "phức tạp" (ví dụ gửi JSON bằng
PUT/DELETE), trình duyệt gửi trước một yêu cầu thăm dòOPTIONS(preflight) để hỏi máy chủ có cho phép hay không.
Thực hành theo giáo trình
Giáo trình: Chương 6 — mục "Thực hành" (Bài 6.1 đến Bài 6.5).
Các bài 6.2 – 6.5 của giáo trình dùng dịch vụ Firebase Realtime Database và công cụ Postman, cần mạng thật và tài khoản. Ở đây bạn nắm đúng các bước, rồi chạy phần mã bằng "Firebase giả" để thấy được kết quả.
Bài 6.1. Tạo một đối tượng chuyển động
Mục tiêu: làm một ô vuông đỏ chạy chéo trong khung vàng khi bấm nút, bằng setInterval() và clearInterval().
Ý tưởng: cứ mỗi 5 mili giây hàm frame() tăng biến pos lên 1 rồi đặt style.top và style.left; khi pos đạt 350 thì clearInterval(id) để dừng. Khung chứa dùng position: relative, phần tử chuyển động dùng position: absolute (Buổi 8).
<style>
#container { width: 400px; height: 400px; position: relative; background: yellow; }
#animate { width: 50px; height: 50px; position: absolute; background-color: red; }
</style>
<p><button onclick="myMove()">Click Me</button></p>
<div id="container">
<div id="animate"></div>
</div>
<script>
function myMove() {
var elem = document.getElementById("animate");
var pos = 0;
var id = setInterval(frame, 5);
function frame() {
if (pos == 350) {
clearInterval(id);
console.log("Dừng ở", elem.style.left, elem.style.top);
} else {
pos++;
elem.style.top = pos + "px";
elem.style.left = pos + "px";
}
}
}
console.log("Bấm nút 'Click Me' để ô vuông chạy từ góc trên bên trái xuống góc dưới bên phải");
</script>Cách đọc: setInterval(frame, 5) trả về một mã định danh id; mã này dùng để dừng bộ đếm bằng clearInterval(id). Nếu quên dừng, hàm frame cứ chạy mãi.
Mẹo: Hãy thử đổi
5thành20(chậm hơn), đổi350thành100, hoặc chỉ tăngleft(chạy ngang). Với hoạt hình mượt hơn, trình duyệt cung cấprequestAnimationFrame(), và đây cũng là cách vẽ chuyển động trên<canvas>.
(Chương trình không in gì.)
Chạy trong khung cách ly ngay trên trình duyệt của bạn; không có mạng.
Hãy mở khung soạn thảo trên, dán mã Bài 6.1 và thử các biến thể ở trên: đổi màu, kích thước, hướng chạy.
Bài 6.2. Thực hành với cơ sở dữ liệu JSON trên Firebase
Các bước của giáo trình (không chạy được trong bài, bạn làm trên máy của mình):
- Truy cập Firebase Console tại
https://console.firebase.google.comvà đăng nhập bằng tài khoản Google. - Nhấn nút "Add project" (Thêm dự án) để tạo dự án mới.
- Khởi tạo cơ sở dữ liệu Realtime Database cho dự án.
- Chọn Test mode để dữ liệu được mở tự do trong một khoảng thời gian. Hết thời gian này, nếu muốn tiếp tục truy cập dữ liệu thì phải vào cấu hình lại quy tắc bảo mật.
- Chọn dấu + để thêm dữ liệu. Trong ví dụ, thêm 3 trường thông tin về Website.
- Truy cập dữ liệu bằng đường dẫn có đuôi
.json, ví dụ:https://mywebsite-bee49-default-rtdb.firebaseio.com/website.json. - Kiểm tra kết quả trả về trên trình duyệt.
https://mywebsite-bee49-default-rtdb.firebaseio.com/website.jsonFirebase Realtime Database lưu dữ liệu thành một cây JSON: thêm .json vào cuối địa chỉ của một nút thì máy chủ trả về chính nhánh đó dưới dạng JSON — nên bạn đọc được nó bằng JSON.parse() hoặc res.json().
Cảnh báo: Chế độ Test mode cho phép bất kỳ ai ghi đọc dữ liệu của bạn. Chỉ dùng để học; dự án thật phải cấu hình quy tắc bảo mật và kiểm tra lại dữ liệu ở máy chủ.
Bài 6.3. Sử dụng Postman
Postman là công cụ dùng để gửi thử các yêu cầu HTTP tới một API và xem kết quả mà không cần viết mã. Với Firebase, các phương thức HTTP có ý nghĩa như sau:
| Phương thức | Việc làm | Ghi chú |
|---|---|---|
GET |
Lấy dữ liệu | Ví dụ lấy thông tin Website đã thêm ở Bài 6.2 |
PUT |
Cập nhật, thay thế toàn bộ dữ liệu | Dữ liệu gửi trong phần Body, định dạng JSON |
PATCH |
Chỉ thay một phần dữ liệu | Dùng khi không muốn ghi đè toàn bộ |
POST |
Thêm dữ liệu mới | Firebase tự sinh một khóa cho bản ghi mới |
DELETE |
Xóa dữ liệu | Hãy vào Firebase kiểm tra lại sau khi xóa |
Khi thêm dữ liệu bằng POST, Firebase sinh một khóa cho bản ghi mới; khóa này dùng để thao tác về sau với bản ghi đó (GET, PUT, DELETE). Ví dụ sau mô phỏng cách Firebase giả hoạt động để bạn thấy sự khác nhau giữa PUT, PATCH và POST:
const db = { website: { ten: "MyWebsite", chuDe: "Blog", nam: 2026 }, products: {} };
let demKhoa = 0;
function firebaseGia(method, path, body) {
const noi = path.split("/");
const cha = noi.slice(0, -1).reduce((o, k) => o[k], db);
const khoa = noi[noi.length - 1];
if (method === "GET") return cha[khoa];
if (method === "PUT") { cha[khoa] = body; return body; }
if (method === "PATCH") { Object.assign(cha[khoa], body); return body; }
if (method === "DELETE") { delete cha[khoa]; return null; }
if (method === "POST") { // thêm bản ghi, tự sinh khóa
const k = "-key" + (++demKhoa);
cha[khoa][k] = body;
return { name: k };
}
}
console.log("GET :", JSON.stringify(firebaseGia("GET", "website")));
console.log("PATCH :", JSON.stringify(firebaseGia("PATCH", "website", { nam: 2027 })));
console.log("Sau PATCH:", JSON.stringify(db.website));
console.log("PUT :", JSON.stringify(firebaseGia("PUT", "website", { ten: "Moi" })));
console.log("Sau PUT :", JSON.stringify(db.website));
const kq = firebaseGia("POST", "products", { name: "Samsung S22", price: 999 });
console.log("POST : khóa mới =", kq.name);
firebaseGia("POST", "products", { name: "iPhone 14", price: 1099 });
console.log("Danh sách:", JSON.stringify(db.products));
firebaseGia("DELETE", "products/" + kq.name);
console.log("Sau DELETE:", JSON.stringify(db.products));Cách đọc: PUT làm mất các trường không có trong dữ liệu mới (trường chuDe và nam biến mất), còn PATCH chỉ ghi đè trường được gửi. POST trả về { "name": "<khóa mới>" }.
Bài 6.4. Thực hành thêm hoặc xóa dữ liệu trên Firebase
Thêm dữ liệu. Nút "Add Product" gửi POST tới .../products.json, thân yêu cầu là chuỗi JSON của sản phẩm. Mã gốc (không chạy):
<div id="demo">
<h2>Let AJAX add a Product</h2>
<button type="button" onclick="addProduct()">Add Product</button>
</div>
<script>
function addProduct() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("demo").innerHTML = this.responseText;
}
};
xhttp.open("POST", "https://myWebsite-3db42-default-rtdb.firebaseio.com/products.json", true);
let data = JSON.stringify({ "name": "Samsung S22", "price": 999 })
xhttp.send(data);
}
</script>Lấy dữ liệu. Hàm getProducts() gửi GET, dùng JSON.parse() rồi duyệt từng sản phẩm bằng for...in, tạo các phần tử div bằng createElement, appendChild (đã học ở Buổi 12). Bản chạy được với "Firebase giả" (cả thêm lẫn lấy):
<style>
.product { border: 1px solid red; padding: 6px 10px; margin: 4px; }
</style>
<div id="demo" style="display:flex"></div>
<script>
// ===== Firebase giả =====
const db = { products: {} };
class FakeXHR {
constructor() { this.readyState = 0; this.status = 0; this.responseText = ""; }
open(m, u) { this.m = m; this.u = u; }
send(body) {
setTimeout(() => {
if (this.m === "POST") {
const k = "-k" + (Object.keys(db.products).length + 1);
db.products[k] = JSON.parse(body);
this.responseText = JSON.stringify({ name: k });
} else {
this.responseText = JSON.stringify(db.products);
}
this.status = 200; this.readyState = 4; this.onreadystatechange();
}, 5);
}
}
// ===== Mã theo giáo trình =====
function addProduct(name, price, xong) {
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) { console.log("Đã thêm, khóa:", this.responseText); xong(); }
};
xhttp.open("POST", "products.json", true);
xhttp.send(JSON.stringify({ "name": name, "price": price }));
}
function getProducts() {
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
let products = JSON.parse(this.responseText);
for (let p in products) {
const nodeProduct = document.createElement("div");
nodeProduct.setAttribute("class", "product");
const nodeName = document.createElement("div");
nodeName.appendChild(document.createTextNode(products[p].name));
nodeProduct.append(nodeName);
const nodePrice = document.createElement("div");
nodePrice.appendChild(document.createTextNode(products[p].price));
nodeProduct.append(nodePrice);
document.getElementById("demo").appendChild(nodeProduct);
}
console.log("Đã hiển thị", document.querySelectorAll(".product").length, "sản phẩm");
}
};
xhttp.open("GET", "products.json", true);
xhttp.send();
}
addProduct("Samsung S22", 999, function () {
addProduct("iPhone 14", 1099, getProducts);
});
</script>Xóa dữ liệu: dùng phương thức DELETE tới địa chỉ của bản ghi (có khóa), ví dụ .../products/-k1.json. Hãy thử tự viết hàm deleteProduct(khoa) theo mẫu addProduct và kiểm tra lại danh sách.
Bài 6.5. Đọc và lưu dữ liệu vào Minh Blogs
Đây là bài tích hợp: trang cá nhân "Minh Blogs" đọc thông tin liên hệ từ Firebase và lưu dữ liệu biểu mẫu lên Firebase.
- Tạo thư mục
jsvới tệpmain.js, liên kết bằng<script src="js/main.js"></script>, đặt ngay trước thẻ đóng</body>. - Tạo dữ liệu thông tin liên hệ trên Firebase (nút
infogồmemail,phone,address). - Sửa phần liên hệ trong HTML, gán
idcho ba phần tửa:info-email,info-phone,info-address. - Bổ sung mã JS gọi
GETtới.../info.json,JSON.parse()rồi gán vào từng phần tử. - Kiểm tra kết quả bảo đảm dữ liệu đã được tải.
- Viết hàm
saveFormData()gửiPOSTtới.../register.json, rồi sửa để khi bấm nút gửi thì dữ liệu của biểu mẫu được đẩy lên Firebase.
<div class="contact">
<h4>Hòm thư</h4>
<a href="mailto:[email protected]" id="info-email"></a>
<h4>Điện thoại</h4>
<a href="tel:+1255-568-6523" id="info-phone"></a>
<h4>Địa chỉ</h4>
<a href="https://www.google.com/maps" target="_blank" id="info-address"></a>
</div>
<script>
// Firebase giả: trả JSON giống dữ liệu đã tạo ở bước 2
class FakeXHR {
open(m, u) { this.u = u; }
send() {
setTimeout(() => {
this.responseText = JSON.stringify({ email: "[email protected]", phone: "0912 345 678", address: "Hà Nội" });
this.status = 200; this.readyState = 4; this.onreadystatechange();
}, 5);
}
}
function getInfo() {
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
let info = JSON.parse(this.responseText);
document.getElementById("info-email").innerText = info.email;
document.getElementById("info-phone").innerText = info.phone;
document.getElementById("info-address").innerText = info.address;
console.log("Đã điền:", info.email, "|", info.phone, "|", info.address);
}
};
xhttp.open("GET", "info.json", true);
xhttp.send();
}
getInfo();
</script>Hàm saveFormData() của giáo trình chưa gửi dữ liệu gì (send() rỗng) — đó chính là phần bạn phải sửa. Lời giải gợi ý: đọc các ô của form, đóng gói thành JSON, gửi qua send(json), và chỉ hiển thị "Done!" khi nhận phản hồi thành công:
<form id="register-form">
Tên: <input id="ten" value="Minh"><br>
Email: <input id="mail" value="[email protected]" type="email" required><br>
<button type="submit">Gửi</button>
</form>
<script>
class FakeXHR {
open(m, u) { this.m = m; this.u = u; }
send(body) {
setTimeout(() => {
console.log("Máy chủ nhận", this.m, this.u, body);
this.status = 200; this.readyState = 4; this.onreadystatechange();
}, 5);
}
}
function saveFormData() {
var xhttp = new FakeXHR();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("register-form").innerHTML = "Done!";
console.log("Giao diện:", document.getElementById("register-form").innerHTML);
}
};
xhttp.open("POST", "register.json", true);
const data = JSON.stringify({
ten: document.getElementById("ten").value,
email: document.getElementById("mail").value
});
xhttp.send(data);
}
const form = document.getElementById("register-form");
form.addEventListener("submit", function (e) {
e.preventDefault(); // chặn gửi form kiểu cũ
if (form.checkValidity()) saveFormData(); // kết hợp xác thực ở mục 1
});
form.dispatchEvent(new Event("submit", { cancelable: true })); // mô phỏng bấm Gửi
</script>Ví dụ thực tế: Đây chính là kiến trúc của rất nhiều trang Web hiện nay: giao diện (HTML/CSS/JS) tách khỏi dữ liệu (một dịch vụ lưu trữ JSON), hai bên nói chuyện với nhau bằng AJAX/fetch.
Câu hỏi ôn tập
Giáo trình: Chương 6 — mục "Câu hỏi ôn tập lý thuyết". Các câu 6.1 – 6.8 (nhúng JS,
window, sự kiện, DOM) thuộc nội dung Buổi 12; dưới đây là các câu 6.9 – 6.15 thuộc buổi này (JSON và AJAX).
6.9. JSON là viết tắt của từ gì? A. JavaScript Object Node · B. JavaScript Object Notation · C. JavaScript Object Naming · D. JavaScript Object Numbering
Đáp án: B — JavaScript Object Notation.
6.10. Cú pháp JSON bao gồm những ký tự nào? A. Dấu ngoặc nhọn {} · B. Dấu ngoặc vuông [] · C. Dấu ngoặc đơn () · D. Cả A và B
Đáp án: D — ngoặc nhọn khai báo đối tượng, ngoặc vuông khai báo mảng; dấu ngoặc đơn không thuộc cú pháp JSON.
6.11. Các giá trị JSON có thể là kiểu dữ liệu gì? A. Number · B. Boolean · C. String · D. Tất cả đều đúng
Đáp án: D — chuỗi, số, đối tượng, mảng, boolean và null.
6.12. Sử dụng phương thức nào để chuyển một chuỗi JSON thành đối tượng JavaScript? A. JSON.parse() · B. JSON.stringify() · C. JSON.convert() · D. JSON.load()
Đáp án: A — JSON.parse() (chuỗi → đối tượng); JSON.stringify() làm chiều ngược lại.
6.13. AJAX có thể truyền dữ liệu giữa trang Web và máy chủ dưới định dạng nào? A. XML · B. JSON · C. Cả A và B đều đúng · D. Tất cả các phương án đều sai
Đáp án: C — AJAX có thể dùng XML hoặc JSON (và cả văn bản thuần); ngày nay JSON phổ biến hơn.
6.14. Phương thức nào của đối tượng XMLHttpRequest thực sự gửi yêu cầu đi? A. open() · B. send() · C. setRequestHeader() · D. connect()
Đáp án: B — send(). open() chỉ khai báo phương thức, địa chỉ, chế độ bất đồng bộ.
6.15. Để kiểm tra tiến trình xử lý của yêu cầu AJAX, ta dùng thuộc tính nào của XMLHttpRequest? A. status · B. responseText · C. readyState · D. responseStatus
Đáp án: C — readyState (0 → 4); status chỉ là mã kết quả HTTP.
Thêm vài câu tự luận ngắn để ôn kỹ:
- Vì sao xác thực biểu mẫu ở trình duyệt vẫn cần xác thực lại ở máy chủ? Đáp án: vì mã chạy ở trình duyệt có thể bị tắt, sửa hoặc bỏ qua (gửi thẳng yêu cầu HTTP); nó chỉ cải thiện trải nghiệm, không phải biện pháp bảo mật.
- Hãy kể ba điểm khiến JSON khác đối tượng JavaScript. Đáp án: khóa và chuỗi bắt buộc nháy kép; không có hàm và
undefined; không có chú thích (JSON chỉ là văn bản). - Khi nào hàm gán cho
onreadystatechangecoi là "đã nhận được dữ liệu hợp lệ"? Đáp án: khireadyState == 4vàstatus == 200. - Lỗi "blocked by CORS policy" do đâu và ai khắc phục? Đáp án: do chính sách cùng nguồn; máy chủ được gọi phải gửi tiêu đề
Access-Control-Allow-Origin, phía máy khách không tự khắc phục được.
Bài tập tự thực hành
Giáo trình: Chương 6 — mục "Bài tập tự thực hành" (Bài 6.6, 6.7, 6.8).
Bài 6.6. Xử lý biểu mẫu
Đề: tạo trang có một ô nhập liệu và nút gửi (submit). Khi người dùng nhập một chuỗi và bấm submit, dùng JavaScript hiển thị chuỗi đó lên trang. Kiểm tra và thông báo nếu người dùng không nhập.
<form id="f">
<input id="o" placeholder="Nhập một chuỗi">
<button type="submit">Gửi</button>
</form>
<p id="kq"></p>
<script>
const form = document.getElementById("f");
form.addEventListener("submit", function (e) {
e.preventDefault();
const v = document.getElementById("o").value.trim();
const kq = document.getElementById("kq");
if (v === "") { kq.textContent = "Bạn chưa nhập gì!"; }
else { kq.textContent = "Bạn đã nhập: " + v; }
console.log(kq.textContent);
});
// Mô phỏng: gửi khi trống, rồi gửi sau khi nhập
form.dispatchEvent(new Event("submit", { cancelable: true }));
document.getElementById("o").value = "Xin chào";
form.dispatchEvent(new Event("submit", { cancelable: true }));
</script>Bài 6.7. Website giới thiệu sản phẩm
Đề: tạo trang có danh sách sản phẩm; khi người dùng bấm vào một sản phẩm, hiển thị thông tin chi tiết lấy từ một chuỗi JSON.
<ul id="ds"></ul>
<div id="chitiet"></div>
<script>
const json = '[{"id":1,"ten":"Laptop A","gia":15000000,"mota":"RAM 16GB"},' +
'{"id":2,"ten":"Chuột B","gia":250000,"mota":"Không dây"}]';
const sanPham = JSON.parse(json);
const ds = document.getElementById("ds");
sanPham.forEach(function (sp) {
const li = document.createElement("li");
li.textContent = sp.ten;
li.style.cursor = "pointer";
li.addEventListener("click", function () {
document.getElementById("chitiet").textContent =
sp.ten + " — " + sp.gia.toLocaleString("en-US") + " đ — " + sp.mota;
console.log(document.getElementById("chitiet").textContent);
});
ds.appendChild(li);
});
ds.children[1].click(); // mô phỏng bấm vào sản phẩm thứ hai
</script>Bài 6.8. Tạo một trò chơi đơn giản
Đề: tạo trò chơi đố vui nhiều câu hỏi: hiển thị câu hỏi và các phương án, kiểm tra người chơi chọn đúng hay sai; khi chọn sai thì đưa ra gợi ý.
const cauHoi = [
{ hoi: "JSON là viết tắt của?", dapAn: ["JavaScript Object Notation", "Java Source Object Name"], dung: 0, goiY: "Chữ N cuối là Notation (ký hiệu)." },
{ hoi: "Phương thức đổi chuỗi JSON thành đối tượng?", dapAn: ["JSON.stringify", "JSON.parse"], dung: 1, goiY: "Parse = phân tích cú pháp." }
];
let diem = 0;
function traLoi(i, chon) {
const c = cauHoi[i];
if (chon === c.dung) { diem++; console.log("Câu", i + 1, ": Đúng!"); }
else console.log("Câu", i + 1, ": Sai. Gợi ý:", c.goiY);
}
traLoi(0, 0);
traLoi(1, 0); // chọn sai
traLoi(1, 1);
console.log("Điểm:", diem + "/" + cauHoi.length);Hãy mở rộng ba bài trên: Bài 6.6 kiểm tra thêm bằng checkValidity(); Bài 6.7 tải JSON bằng fetch giả; Bài 6.8 hiển thị câu hỏi lên DOM và đếm điểm.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
| Chỉ kiểm tra dữ liệu bằng JavaScript ở trình duyệt | Kẻ xấu bỏ qua kiểm tra, dữ liệu rác hoặc độc hại vào cơ sở dữ liệu | Kiểm tra lại toàn bộ ở máy chủ |
Quên setCustomValidity("") khi dữ liệu đã đúng |
Ô nhập bị kẹt ở trạng thái không hợp lệ | Xóa lỗi tùy chỉnh bằng chuỗi rỗng khi hợp lệ |
| Vẽ canvas khi ảnh chưa tải xong | Không thấy ảnh, không có thông báo lỗi | Gọi drawImage trong onload |
| Đặt kích thước canvas chỉ bằng CSS | Hình vẽ bị mờ, méo | Đặt thuộc tính width, height của canvas |
| Viết JSON bằng nháy đơn, khóa không nháy, thừa dấu phẩy cuối | JSON.parse() ném lỗi SyntaxError |
Dùng nháy kép, bọc try/catch |
Dùng JSON.stringify rồi parse để sao chép đối tượng có hàm, ngày |
Mất hàm, ngày thành chuỗi | Chỉ dùng cho dữ liệu thuần JSON |
Chỉ kiểm tra readyState == 4 |
Xử lý nhầm trang lỗi 404/500 như dữ liệu | Kiểm tra cả status == 200 |
Gọi send() rồi mới gán onreadystatechange |
Bỏ lỡ sự kiện phản hồi | Gán hàm xử lý trước send() |
Quên kiểm tra res.ok khi dùng fetch |
Lỗi 404/500 không bị bắt trong catch |
if (!res.ok) throw new Error(...) |
| Cố "sửa mã" khi gặp lỗi CORS | Mất thời gian, không hết lỗi | Cấu hình Access-Control-Allow-Origin trên máy chủ |
Tóm tắt
- Xác thực biểu mẫu: dùng
onsubmit+return falseđể chặn gửi;checkValidity()kết hợp các thuộc tínhrequired,min,max,pattern,type;setCustomValidity()đặt lỗi riêng; các cờ trongvaliditycho biết lỗi cụ thể. Luôn kiểm tra lại ở máy chủ. - Canvas:
<canvas>chỉ là vùng chứa; vẽ bằng context 2D (getContext("2d")) vớimoveTo/lineTo/stroke,arc,fillText/strokeText, gradient,drawImage. - JSON: định dạng văn bản trao đổi dữ liệu;
JSON.parse()(chuỗi → đối tượng),JSON.stringify()(đối tượng → chuỗi); giá trị là chuỗi, số, đối tượng, mảng, boolean hoặc null; ngắn gọn và dễ xử lý hơn XML. - AJAX: trao đổi dữ liệu với máy chủ ở chế độ nền, không tải lại trang.
XMLHttpRequest:open(),send(),onreadystatechange,readyState0–4,status200/403/404,responseText,responseXML,getResponseHeader().fetch()+async/awaitgọn hơn. - Cùng nguồn và CORS: trình duyệt chặn đọc dữ liệu khác nguồn trừ khi máy chủ gửi
Access-Control-Allow-Origin.