Buổi 14: Các công cụ hỗ trợ: Emmet, Git/GitHub, SASS, Bootstrap, jQuery
Hình dung bạn nhận làm trang Web giới thiệu sản phẩm cho một cửa hàng nhỏ. Bạn phải gõ hàng chục thẻ <li> cho menu, viết lại cùng một bộ màu ở ba chục chỗ trong CSS, dựng lưới sao cho đẹp trên cả điện thoại lẫn máy tính, thêm vài hiệu ứng ẩn/hiện, và còn phải lưu lại từng phiên bản để lỡ sửa hỏng thì quay về được. Nếu làm tất cả bằng tay thuần HTML, CSS và JavaScript thì được, nhưng chậm và dễ sai.
Các nhà phát triển Web chuyên nghiệp không làm như vậy. Họ dùng thêm những công cụ hỗ trợ do bên thứ ba cung cấp: từ một plugin sinh mã HTML tự động từ các từ viết tắt, đến các khung làm việc (framework) cho phép dựng giao diện nhanh hơn. Buổi này giới thiệu năm công cụ như vậy theo đúng thứ tự của Chương 7 trong giáo trình: Emmet, Git và GitHub, SASS, Bootstrap và jQuery.
Lưu ý: Khung chạy thử trên Codelab là môi trường cách ly, không có kết nối mạng, nên các ví dụ chạy thử trong bài này không nạp Bootstrap hay jQuery từ CDN. Thay vào đó, bạn sẽ chạy những bản tự dựng thu nhỏ (mini) để hiểu cách công cụ hoạt động, còn mã dùng thư viện thật được trình bày dưới dạng mã minh họa để bạn chép vào dự án của mình.
Mục tiêu buổi học
- Sau buổi này bạn có thể dùng cú pháp viết tắt của Emmet (
!,tag*n,>,#id,.class,[attr],{text},$) để sinh nhanh mã HTML và CSS. - Sau buổi này bạn có thể giải thích Git và GitHub là gì, phân biệt kho cục bộ với kho trên GitHub và dùng các lệnh
init,add,commit,push,pull,branch,merge. - Sau buổi này bạn có thể viết SCSS với biến, lồng quy tắc và mixin, rồi biên dịch ra CSS bằng lệnh
sass. - Sau buổi này bạn có thể dùng hệ thống lưới 12 cột của Bootstrap (
container,row,col-*,col-md-*) để dựng bố cục đáp ứng. - Sau buổi này bạn có thể dùng jQuery để chọn phần tử, đọc/ghi giá trị, duyệt cây DOM, gán sự kiện, tạo hiệu ứng và gửi AJAX, đồng thời biết mã JavaScript thuần tương đương.
- Sau buổi này bạn có thể nhận xét được khi nào nên dùng, khi nào không cần dùng jQuery trong các dự án hiện nay.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1. Emmet | Cú pháp viết tắt HTML, ví dụ nav>ul>li*8, Emmet cho CSS |
15 phút |
| 2. Git và GitHub | Git, GitHub, GitHub Desktop, các lệnh Git cơ bản | 20 phút |
| 3. SASS | Biến, lồng quy tắc, mixin, biên dịch | 20 phút |
| 4. Bootstrap | Mô-đun, lưới 12 cột, thành phần, biểu tượng | 25 phút |
| 5. jQuery | Bộ chọn, đọc/ghi, duyệt DOM, kiểu, sự kiện, hiệu ứng, AJAX | 40 phút |
| Thực hành theo giáo trình | Bài 7.1 (DataTables) và Bài 7.2 (SASS) | 30 phút |
| Ôn tập và bài tập tự thực hành | Câu hỏi 7.1 – 7.13, Bài 7.3, 7.4, bài tập trên Codelab | 30 phút |
Khoảng một nửa thời lượng dành cho thực hành: sau mỗi phần lý thuyết có một nhóm bài tập trên Codelab để bạn kiểm tra ngay.
1. Emmet
Giáo trình: Chương 7 — mục "Emmet" (Bảng 7-1, Hình 7-1).
Emmet là một công cụ mạnh mẽ dùng để tăng tốc quá trình viết mã HTML và CSS. Công cụ này cung cấp các phím tắt và biểu thức ngắn gọn để tạo ra mã HTML và CSS một cách tự động. Nhờ vậy lập trình viên tiết kiệm được thời gian và công sức viết, định dạng mã, nhất là ở các dự án lớn hoặc khi cần dùng lại mã. Hiệu quả cuối cùng là năng suất cao hơn và quy trình phát triển Web linh hoạt hơn.
nav>ul>li*3>a[href="target.html"]#item${Link}▌
<nav><ul><li><a href="target.html" id="item1">Link</a></li><li><a href="target.html" id="item2">Link</a></li><li><a href="target.html" id="item3">Link</a></li></ul></nav>
nav>ul>liDấu > lồng phần tử con vào bên trong phần tử cha.*3Nhân bản phần tử đứng trước 3 lần; đổi thành *8 sẽ ra 8 dòng <li>.[href="target.html"]Thuộc tính và giá trị; thuộc tính bắt buộc như href luôn được thêm sẵn.#item$Thuộc tính id; ký hiệu $ tự đánh số 1, 2, 3…{Link}Văn bản nằm giữa thẻ mở và thẻ đóng.!<!DOCTYPE html><html lang="en">…a<a href=""></a>li*3<li></li> × 3a[href="x.html"]<a href="x.html"></a>ul>li<ul><li></li></ul>div#main<div id="main"></div>li#m$*2<li id="m1"></li><li id="m2"></li>p.note<p class="note"></p>h1{Xin chào}<h1>Xin chào</h1>bd+border: 1px solid #000;m10margin: 10px;p5padding: 5px;fsz12font-size: 12px;bgc#f00background-color: #f00;Emmet biến một biểu thức ngắn thành mã HTML/CSS đầy đủ; có sẵn trong Visual Studio Code, Sublime Text, WebStorm…
Hình trên cho thấy ý tưởng cốt lõi: bạn gõ một cụm viết tắt (abbreviation) rất ngắn, bấm phím kích hoạt, và trình soạn thảo mở rộng (expand) nó thành mã đầy đủ. Hãy đọc hình từ trái sang phải: vế trái là thứ bạn gõ, vế phải là mã HTML nhận được.
Sinh khung trang với dấu chấm than
Ví dụ nhỏ nhất và hay dùng nhất: để sinh khung HTML khởi đầu cho một trang Web, bạn gõ dấu ! rồi chọn Emmet > Expand Abbreviation hoặc nhấn Enter. Trong một số trình viết mã, bạn nhấn phím Tab để thực thi. Mã sau sẽ được tạo ra:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>Đây chính là cấu trúc tài liệu HTML bạn đã học ở Buổi 3 (DOCTYPE, html, head, body). Thay vì gõ lại cả chục dòng, bạn chỉ gõ một ký tự.
Bảng cú pháp viết tắt
Một số cú pháp viết tắt do Emmet cung cấp được tổng hợp trong Bảng 7-1 của giáo trình:
| Cú pháp | Tác dụng |
|---|---|
! |
Thêm mã bắt đầu cho trang Web, gồm khai báo loại tài liệu và phần khung của tài liệu HTML |
tag |
Thêm phần tử, cùng với các thuộc tính bắt buộc |
tag*number |
Thêm phần tử theo số lần đã chỉ định |
tag[attribute="value"] |
Thêm phần tử với thuộc tính và giá trị |
tag1>tag2 |
Lồng phần tử bên trong một phần tử khác |
tag#id |
Thêm phần tử có thuộc tính id được chỉ định |
tag#id$ |
Thêm phần tử có thuộc tính id được chỉ định kèm theo đánh số |
tag.classname |
Thêm phần tử có thuộc tính lớp |
tag{text} |
Bọc văn bản bằng một thẻ |
Mở rộng: Ngoài Bảng 7-1, Emmet còn có toán tử
+(thẻ anh em cùng cấp, ví dụh1+p) và dấu ngoặc()để nhóm (ví dụ(dt+dd)*3). Nếu bạn chỉ viết#mainhoặc.cardmà không ghi tên thẻ, Emmet tự hiểu là thẻdiv. Số dấu$liên tiếp quy định độ rộng đệm số 0:li.item$$*3choitem01,item02,item03. Các quy tắc này đều xuất hiện trong bài tập Codelab của buổi.
Để thấy bảng cú pháp vận hành thế nào, ví dụ dưới đây là một bộ khai triển Emmet thu nhỏ (khoảng 30 dòng) do lớp tự dựng, hỗ trợ các cú pháp >, +, *, #, ., [], {} và $. Nó không thay thế Emmet thật, nhưng cho bạn thấy "phép màu" thực chất chỉ là quy tắc thay thế chuỗi.
// Bộ khai triển Emmet thu nhỏ (chỉ để học)
const VOID = ["img", "input", "br", "hr", "meta", "link"];
function parseOne(s) {
const tag = (s.match(/^[a-z][a-z0-9]*/i) || ["div"])[0];
const attrs = {}, classes = [];
let text = "", n = 1, rest = s.replace(/^[a-z][a-z0-9]*/i, "");
const re = /^(?:#([\w$-]+)|\.([\w$-]+)|\[([^\]]*)\]|\{([^}]*)\}|\*(\d+))/;
while (rest) {
const m = rest.match(re);
if (!m) throw new Error("Không hiểu: " + s);
if (m[1]) attrs.id = m[1];
else if (m[2]) classes.push(m[2]);
else if (m[3] !== undefined) for (const a of m[3].match(/[\w-]+="[^"]*"/g) || []) { const i = a.indexOf("="); attrs[a.slice(0, i)] = a.slice(i + 2, -1); }
else if (m[4] !== undefined) text = m[4];
else n = +m[5];
rest = rest.slice(m[0].length);
}
if (classes.length) attrs.class = classes.join(" ");
return { tag, attrs, text, n };
}
function build(abbr, ctx) {
const [head, ...rest] = abbr.split(">");
const items = head.split("+"), out = [];
items.forEach((item, k) => {
const node = parseOne(item);
const kids = k === items.length - 1 && rest.length ? rest.join(">") : null;
for (let i = 1; i <= node.n; i++) {
const idx = node.n > 1 ? i : ctx;
const at = Object.entries(node.attrs).map(([a, v]) => ` ${a}="${v.replace(/\$/g, idx)}"`).join("");
const text = node.text.replace(/\$/g, idx);
if (kids) {
const inner = build(kids, idx);
if (inner.length === 1) out.push(`<${node.tag}${at}>${inner[0]}</${node.tag}>`);
else out.push(`<${node.tag}${at}>`, ...inner.map((l) => " " + l), `</${node.tag}>`);
} else if (VOID.includes(node.tag)) out.push(`<${node.tag}${at}>`);
else out.push(`<${node.tag}${at}>${text}</${node.tag}>`);
}
});
return out;
}
const expand = (abbr) => build(abbr, 1).join("\n");
console.log(expand("h1{Cửa hàng}+p.intro{Xin chào}"));
console.log(expand("ul>li.item$*3"));
console.log(expand("#main"));
console.log(expand("img[src=\"a.png\" alt=\"Ảnh\"]"));Hãy đoán trước: dòng thứ hai (ul>li.item$*3) sẽ sinh ra những lớp nào? Chạy thử để kiểm tra: ký hiệu $ được thay bằng số thứ tự 1, 2, 3 của từng bản sao, nên bạn nhận item1, item2, item3.
Ví dụ: nav, ul và 8 thẻ li
Giáo trình đưa ra một ví dụ điển hình: muốn tạo một nav bên trong có thẻ ul và 8 thẻ li, trong mỗi thẻ li có một liên kết được đặt id và có nội dung kèm theo, ta viết bằng cú pháp viết tắt như sau:
nav>ul>li*8>a[href="target.html"]#item${Link}Mã HTML tự động được tạo ra (rút gọn còn 3 mục để dễ đọc, bản đầy đủ có item1 đến item8):
<nav>
<ul>
<li><a href="target.html" id="item1">Link</a></li>
<li><a href="target.html" id="item2">Link</a></li>
<li><a href="target.html" id="item3">Link</a></li>
...
</ul>
</nav>Bạn có thể kiểm chứng bằng bộ khai triển thu nhỏ ở trên: ví dụ tiếp theo nhúng lại bộ khai triển đó vào một trang, khai triển đúng cụm viết tắt của giáo trình (với 8 mục) và hiển thị kết quả thành danh sách liên kết thật.
<style>
nav ul { display: flex; gap: 8px; list-style: none; padding: 0; flex-wrap: wrap; }
nav a { padding: 4px 10px; background: #e8f0fe; border-radius: 4px; text-decoration: none; color: #1a56db; }
pre { background: #f6f8fa; padding: 8px; font-size: 12px; max-height: 120px; overflow: auto; }
</style>
<div id="kq"></div>
<pre id="ma"></pre>
<script>
const VOID = ["img", "input", "br", "hr"];
function build(abbr, ctx) {
const [head, ...rest] = abbr.split(">");
const out = [];
head.split("+").forEach((item, k, all) => {
const m = item.match(/^([a-z][a-z0-9]*)(.*)$/i);
const tag = m[1], href = (m[2].match(/href="([^"]*)"/) || [])[1];
const opts = m[2].replace(/\[[^\]]*\]/g, "");
const n = +((opts.match(/\*(\d+)/) || [])[1] || 1);
const id = (opts.match(/#([\w$-]+)/) || [])[1];
const cls = (opts.match(/\.([\w$-]+)/) || [])[1];
const text = (opts.match(/\{([^}]*)\}/) || [])[1] || "";
const kids = k === all.length - 1 && rest.length ? rest.join(">") : null;
for (let i = 1; i <= n; i++) {
const idx = n > 1 ? i : ctx;
let at = "";
if (href) at += ` href="${href}"`;
if (id) at += ` id="${id.replace(/\$/g, idx)}"`;
if (cls) at += ` class="${cls.replace(/\$/g, idx)}"`;
if (kids) {
const inner = build(kids, idx);
out.push(inner.length === 1 ? `<${tag}${at}>${inner[0]}</${tag}>` : `<${tag}${at}>\n ${inner.join("\n ")}\n</${tag}>`);
} else out.push(VOID.includes(tag) ? `<${tag}${at}>` : `<${tag}${at}>${text}</${tag}>`);
}
});
return out;
}
const html = build('nav>ul>li*8>a[href="target.html"]#item${Link}', 1).join("\n");
document.getElementById("kq").innerHTML = html;
document.getElementById("ma").textContent = html;
console.log("Số liên kết:", document.querySelectorAll("nav a").length);
console.log("id cuối:", document.querySelector("nav li:last-child a").id);
</script>Hãy mở tab console của khung chạy thử: có 8 liên kết, liên kết cuối có id="item8". Đúng như giáo trình mô tả.
Thuộc tính bắt buộc
Trong các phần tử <a> ở ví dụ trên, thuộc tính href được đặt thành "target.html". Tuy nhiên, ngay cả khi bạn không chỉ định href cho phần tử <a>, Emmet vẫn tạo thuộc tính này vì đây là thuộc tính bắt buộc; nhà phát triển chỉ cần điền giá trị. Điều tương tự áp dụng với các thuộc tính bắt buộc khác (chẳng hạn src và alt của thẻ img).
Ngoài href, thuộc tính id có giá trị "item" kèm chỉ số được thêm vào mỗi thẻ <a> để giá trị id khác nhau ở từng mục (nhờ ký hiệu $). Cuối cùng, văn bản của mỗi <a> được đặt thành "Link" nhờ {Link}. Nếu bạn thử nghiệm thêm vài lệnh khác, bạn sẽ hiểu rõ hơn Emmet giúp tăng tốc đến mức nào.
Mẹo: Muốn khai triển nhanh trong VS Code, gõ cụm viết tắt rồi nhấn Tab (hoặc Enter nếu gợi ý đang hiện). Nếu không thấy gợi ý, kiểm tra tệp đang mở có đúng ngôn ngữ HTML hay CSS không.
Emmet hoạt động trên nhiều nền tảng
Emmet có thể hoạt động trên nhiều nền tảng khác nhau. Emmet thường được tích hợp sẵn vào các trình soạn thảo mã như Visual Studio Code, Sublime Text, WebStorm,... (Hình 7-1 trong giáo trình liệt kê các công cụ có thể cài Emmet). Với các trình soạn thảo khác, Emmet được cài đặt qua các gói mở rộng và thiết lập tùy chỉnh. Tài liệu đầy đủ của Emmet nằm tại docs.emmet.io.
Emmet cho CSS
Emmet cũng được dùng khi viết CSS. Một số cú pháp viết tắt thường dùng:
| Viết tắt | Khai triển thành |
|---|---|
bd+ |
border: 1px solid #000; |
m10 |
margin: 10px; |
p5 |
padding: 5px; |
fsz12 |
font-size: 12px; |
bgc#f00 |
background-color: #f00; |
Ví dụ sau mô phỏng bảng này bằng một từ điển nhỏ để bạn thấy quy luật: chữ cái đầu là viết tắt của thuộc tính (m = margin, p = padding, fsz = font-size, bgc = background-color) còn số phía sau là giá trị tính bằng pixel.
const dict = { m: "margin", p: "padding", fsz: "font-size", bgc: "background-color" };
function css(abbr) {
if (abbr === "bd+") return "border: 1px solid #000;";
const m = abbr.match(/^([a-z]+)(#?[\w]+)$/);
const prop = dict[m[1]];
const val = m[2][0] === "#" ? m[2] : m[2] + "px";
return `${prop}: ${val};`;
}
["bd+", "m10", "p5", "fsz12", "bgc#f00"].forEach((a) => console.log(a.padEnd(8), "→", css(a)));Ví dụ thực tế: Khi dựng menu có 6 mục, bạn gõ
nav>ul>li*6>arồi Tab: 6 dòng<li><a href=""></a></li>xuất hiện ngay. Với 30 sản phẩm trong lưới,div.card*30tiết kiệm bạn cả chục phút gõ lặp.
Hãy thử ngay với mô phỏng tương tác dưới đây: gõ một cụm viết tắt bất kỳ trong Bảng 7-1 và xem HTML được sinh ra.
Emmet: gõ tắt, bung thành mã
biểu thức ngắn → HTML/CSS đầy đủBên trong mô phỏng, bạn thấy kết quả thay đổi theo từng ký tự gõ. Hãy thử ul>li*3, div#main.box, a[href="x.html"]{Xem} và so sánh với bảng cú pháp.
Vừa học xong Emmet, hãy làm nhóm bài sau để luyện đọc và khai triển cụm viết tắt: các bài này yêu cầu bạn tự viết chương trình khai triển nhân bản thẻ, đánh số $, lồng thẻ và nhóm.
2. Git và GitHub
Giáo trình: Chương 7 — mục "Git và GitHub" (Hình 7-2) và mục con "Các lệnh Git cơ bản".
Git, kho lưu trữ và kiểm soát mã nguồn
Git là một hệ thống kiểm soát mã nguồn (version control system), cho phép quản lý các dự án được lưu trong các kho mã nguồn. Git giúp các thao tác như hoàn tác lỗi, quay về các phiên bản trước, hay chia sẻ mã nguồn với nhà phát triển khác mà không gây xung đột trở nên dễ dàng hơn. Với Git, dự án được lưu dưới dạng kho lưu trữ (repository). Mỗi kho có thể chứa nhiều nhánh (branch), mỗi nhánh đại diện cho một trạng thái của dự án theo thời gian. Người dùng có thể chuyển qua lại giữa các nhánh và bất cứ lúc nào cũng có thể dùng mã từ một nhánh để ghi đè các tệp trên nhánh chính (main).
Git là một ví dụ của hệ thống kiểm soát mã nguồn phân tán (distributed). Với kiểu này, người dùng không cần luôn nối với máy chủ chứa mã nguồn. Các thay đổi được thực hiện ngay trên máy (cục bộ), thậm chí được chia sẻ giữa các máy trạm trước khi chuyển lên máy chủ trung tâm.
Ví dụ thực tế: Nhóm ba bạn làm Website cho cửa hàng: An làm trang chủ, Bình làm trang sản phẩm, Chi sửa CSS chung. Không có Git, cả ba gửi tệp ZIP qua lại và ghi đè lên nhau. Có Git, mỗi người làm trên một nhánh riêng, xong việc thì gộp (merge) vào nhánh chính; sai thì quay lại mốc cũ.
GitHub và GitHub Desktop
GitHub là một nền tảng quản lý và lưu trữ mã nguồn của dự án phần mềm, xây dựng trên nền Git. Trên GitHub, các nhà phát triển có thể tạo kho lưu trữ miễn phí để lưu mã nguồn, theo dõi lịch sử thay đổi (commits), tạo và quản lý các nhánh (branches) và thực hiện thao tác hợp nhất (merge) giữa các nhánh. GitHub còn có các tính năng quản lý vấn đề (issue tracking), wiki và hệ thống dự án để theo dõi công việc.
GitHub Desktop là ứng dụng chạy trên máy tính, giúp làm việc với GitHub dễ dàng và trực quan hơn. Với GitHub Desktop, bạn thao tác với kho lưu trữ mà không cần dùng dòng lệnh: giao diện đồ họa cho phép xem và quản lý các thay đổi mã nguồn, thực hiện commit, push, pull và merge thuận tiện (Hình 7-2 trong giáo trình là giao diện của GitHub Desktop).
Cách tạo kho lưu trữ Git cục bộ bằng GitHub Desktop:
- Chọn File > New Repository để hiện hộp thoại Create a New Repository, rồi nhập tên và mô tả cho kho.
- Sao chép các tệp dự án vào thư mục của kho.
Thông tin thêm có tại github.com và desktop.github.com.
Ba khái niệm trên được tóm tắt trong sơ đồ dòng chảy dưới đây: tệp được sửa ở thư mục làm việc, được đưa vào vùng chờ, được ghi thành commit trong kho cục bộ rồi được đẩy lên GitHub.
git add .git commita1f3c9ePhiên bản đầu tiênb72d4a0Thêm menuc5e81f6Sửa giao diệngit pusha1f3c9ePhiên bản đầu tiênb72d4a0Thêm menuc5e81f6Sửa giao diệngit pulllấy mốc mới từ GitHub về máy ·git clone <địa chỉ>tải cả kho về lần đầugit initgit add .git commit -m "Phiên bản đầu tiên"git branch -M maingit remote add origin https://github.com/<tên tài khoản>/<tên kho>.gitgit push -u origin mainThay đổi đi qua ba chặng: vùng chờ (git add), kho cục bộ (git commit) rồi GitHub (git push); git pull đi theo chiều ngược lại.
Hãy đọc sơ đồ theo chiều mũi tên: git add chuyển từ thư mục làm việc sang vùng chờ, git commit ghi một mốc vào kho cục bộ, git push đưa các mốc lên GitHub, còn git pull đi chiều ngược lại.
Các lệnh Git cơ bản
Ngoài giao diện đồ họa như GitHub Desktop, Git chủ yếu được dùng qua dòng lệnh. Dưới đây là các lệnh cần thiết cho một dự án thiết kế Web thông thường.
| Lệnh | Tác dụng |
|---|---|
git init |
Khởi tạo một kho lưu trữ mới trong thư mục hiện tại |
git clone <địa chỉ> |
Tải một kho đã có trên GitHub về máy |
git status |
Xem tệp nào đã thay đổi, tệp nào đang chờ commit |
git add <tệp> hoặc git add . |
Đưa các thay đổi vào vùng chờ (staging area) |
git commit -m "nội dung" |
Ghi một mốc thay đổi kèm lời mô tả |
git log --oneline |
Xem lại lịch sử các mốc đã ghi |
git push origin main |
Đẩy các mốc từ máy lên kho trên GitHub |
git pull |
Lấy về các thay đổi mới nhất từ kho trên GitHub |
git branch <tên> |
Tạo một nhánh mới |
git checkout <tên> (hoặc git switch <tên> từ Git 2.23) |
Chuyển sang nhánh đó |
git merge <tên nhánh> |
Gộp nhánh đã chỉ định vào nhánh đang làm việc |
Quy trình đưa một dự án mới lên GitHub gồm các bước sau:
git init
git add .
git commit -m "Phiên bản đầu tiên"
git branch -M main
git remote add origin https://github.com/<tên tài khoản>/<tên kho>.git
git push -u origin mainLưu ý: Từ tháng 10 năm 2020, GitHub đặt tên nhánh mặc định là main; các kho tạo trước thời điểm đó dùng tên master. Hãy tạo thêm tệp
.gitignoreở thư mục gốc để loại các thư mục không cần đưa lên kho, chẳng hạnnode_moduleshay tệp cấu hình cá nhân.
Các lệnh Git cần một cửa sổ dòng lệnh thật nên không chạy được trong trình duyệt. Để bạn hình dung, ví dụ dưới đây là một mô hình Git thu nhỏ viết bằng JavaScript: nó mô phỏng thư mục làm việc, vùng chờ, các commit, nhánh và việc gộp nhánh với đúng thứ tự lệnh của quy trình trên. Hãy đọc kết quả in ra và đối chiếu từng dòng với lệnh Git tương ứng.
// Mô hình Git thu nhỏ: thư mục làm việc → vùng chờ → kho (commit) → GitHub
const repo = { work: {}, staged: {}, branches: { main: [] }, head: "main", remote: [] };
let counter = 0;
const git = {
edit(file, text) { repo.work[file] = text; },
status() {
const changed = Object.keys(repo.work).filter((f) => !(f in repo.staged) || repo.staged[f] !== repo.work[f]);
console.log("status → chưa add:", changed.join(", ") || "(không)", "| đã add:", Object.keys(repo.staged).join(", ") || "(không)");
},
add(file) { if (file === ".") Object.assign(repo.staged, repo.work); else repo.staged[file] = repo.work[file]; },
commit(msg) {
const id = "c" + ++counter;
repo.branches[repo.head].push({ id, msg, files: { ...repo.staged } });
console.log(`[${repo.head} ${id}] ${msg}`);
},
log() { console.log("log:", repo.branches[repo.head].map((c) => c.id + " " + c.msg).reverse().join(" | ")); },
branch(name) { repo.branches[name] = repo.branches[repo.head].slice(); },
checkout(name) { repo.head = name; console.log("Đang ở nhánh", name); },
merge(name) {
const cur = repo.branches[repo.head];
for (const c of repo.branches[name]) if (!cur.some((x) => x.id === c.id)) cur.push(c);
console.log(`Đã gộp ${name} vào ${repo.head}`);
},
push() { repo.remote = repo.branches[repo.head].map((c) => c.id); console.log("push → GitHub có:", repo.remote.join(", ")); },
};
git.edit("index.html", "<h1>Xin chào</h1>");
git.status();
git.add(".");
git.commit("Phiên bản đầu tiên");
git.branch("giao-dien");
git.checkout("giao-dien");
git.edit("style.css", "h1{color:navy}");
git.add("style.css");
git.commit("Thêm CSS cho tiêu đề");
git.checkout("main");
git.merge("giao-dien");
git.log();
git.push();Chuỗi lệnh trong ví dụ tương ứng với Git thật như sau (mỗi dòng mô phỏng có một lệnh thật đi kèm):
git status
git add .
git commit -m "Phiên bản đầu tiên"
git branch giao-dien
git checkout giao-dien
git add style.css
git commit -m "Thêm CSS cho tiêu đề"
git checkout main
git merge giao-dien
git log --oneline
git push origin mainCảnh báo: Không đưa mật khẩu, khóa API hay tệp
.envvào kho Git, nhất là kho công khai trên GitHub. Một khi đã commit, bí mật nằm trong lịch sử kho và có thể bị lấy lại ngay cả khi bạn xóa tệp ở commit sau.
Mở rộng: Khi hai nhánh cùng sửa một dòng,
git mergebáo xung đột (conflict): bạn mở tệp, chọn nội dung giữ lại, rồigit addvàgit commitlại. Mô hình thu nhỏ ở trên chỉ có trường hợp gộp suôn sẻ, nên không có xung đột.
Phần Git và GitHub không có nhóm bài chấm tự động trên Codelab (vì cần dòng lệnh thật); bạn sẽ thực hành bằng Bài 7.3 ở cuối bài.
3. SASS
Giáo trình: Chương 7 — mục "SASS", gồm các mục con "Giới thiệu về SASS", "Sử dụng biến", "Cách sử dụng SASS để lồng các quy tắc kiểu" và "Sử dụng mixins của SASS".
Giới thiệu về SASS
SASS là viết tắt của Syntactically Awesome Style Sheets. Đây là một ngôn ngữ mở rộng của CSS, dùng để biên dịch và tạo ra mã CSS hiệu quả hơn. SASS cung cấp các tính năng mạnh như biến (variables), lồng (nesting), mixin, kế thừa (inheritance) và nhiều tính năng khác, giúp tái sử dụng mã và quản lý kiểu trình bày của trang Web dễ dàng. Nhờ vậy nhà phát triển viết CSS có cấu trúc hơn, giảm lặp lại và tăng tính linh hoạt.
Mã SASS được viết trong các tệp .sass hoặc .scss, sau đó biên dịch (compile) thành CSS thông thường để dùng trong dự án. Các công cụ như Node.js hoặc trình biên dịch SASS độc lập có thể làm việc này. Trình duyệt không hiểu SCSS, nó chỉ hiểu CSS đã biên dịch.
sass --watch sass:csstheo dõi thư mục: lưu tệp là tự biên dịch lại$font: Arial, Helvetica, sans-serif;$color-2: navy;body {font-family: $font;}h1 {color: $color-2;}
body {font-family: Arial, Helvetica, sans-serif;}h1 {color: navy;}
nav {ul { margin-left: 1cm; }li { border: 2px solid red; }a { color: red; }}
nav ul { margin-left: 1cm; }nav li { border: 2px solid red; }nav a { color: red; }
@mixin box($bgcolor) {width: 80px;height: 90px;background-color: $bgcolor;}.box1 { @include box(blue); }.box2 { @include box(yellow); }
.box1 {width: 80px;height: 90px;background-color: blue;}.box2 {width: 80px;height: 90px;background-color: yellow;}
Trình biên dịch SASS đọc tệp .scss (có biến, lồng quy tắc, mixin) rồi sinh ra CSS thuần cho trình duyệt; vùng cùng màu là phần tương ứng.
Hình trên là toàn bộ vòng đời của SASS: bạn viết style.scss (có biến, lồng, mixin), trình biên dịch đọc nó và ghi ra style.css thuần, rồi trang HTML nhúng style.css như bình thường.
Một tệp SCSS có thể như sau:
$primary-color: #3498db;
$secondary-color: #2ecc71;
.button {
background-color: $primary-color;
color: $secondary-color;
}Tệp SCSS này khai báo hai biến màu $primary-color và $secondary-color có giá trị lần lượt là #3498db và #2ecc71, rồi dùng hai biến để tô màu cho phần tử có lớp .button. Bạn biên dịch tệp SASS thành CSS bằng lệnh:
npx sass style.scss style.cssCó thể thêm tham số tùy chọn, ví dụ:
sass --watch sass:cssLệnh này khiến SASS theo dõi mọi tệp trong thư mục sass, tự biên dịch lại bất kỳ tệp nào thay đổi và lưu tệp CSS mới vào thư mục css. Nhờ đó bạn không phải chạy lệnh thủ công mỗi lần sửa tệp SASS.
Lưu ý: Hai cú pháp của SASS:
.scssdùng dấu ngoặc nhọn và dấu chấm phẩy giống CSS (đang được dùng phổ biến, và là cú pháp của giáo trình), còn.sassbỏ ngoặc nhọn, chỉ dựa vào thụt dòng. Hãy bắt đầu với.scssvì mọi CSS hợp lệ đều là SCSS hợp lệ.
Để bạn tự "nhìn thấy" trình biên dịch làm gì, ví dụ dưới đây là một bộ biên dịch SCSS thu nhỏ (khoảng 35 dòng) hỗ trợ biến, lồng quy tắc, ký hiệu & và mixin có tham số. Nó cũng chỉ để học, không thay thế sass thật. Các mục sau sẽ dùng lại bộ biên dịch này với đúng các ví dụ của giáo trình.
// Bộ biên dịch SCSS thu nhỏ: biến, lồng quy tắc, &, mixin có tham số
function compile(src) {
src = src.replace(/\/\/.*$/gm, "");
const vars = {}, mixins = {};
src = src.replace(/@mixin\s+([\w-]+)\s*\(([^)]*)\)\s*\{([^}]*)\}/g, (m, name, params, body) => {
mixins[name] = { params: params.split(",").map((p) => p.trim().slice(1)), body };
return "";
});
src = src.replace(/\$([\w-]+)\s*:\s*([^;]+);/g, (m, k, v) => { vars[k] = v.trim(); return ""; });
src = src.replace(/@include\s+([\w-]+)\(([^)]*)\)\s*;/g, (m, name, args) => {
const mx = mixins[name], vals = args.split(",").map((a) => a.trim());
let body = mx.body;
mx.params.forEach((p, i) => { body = body.replace(new RegExp("\\$" + p + "\\b", "g"), vals[i]); });
return body;
});
src = src.replace(/\$([\w-]+)/g, (m, k) => (k in vars ? vars[k] : m));
const css = [];
(function walk(text, sel) {
const decls = [], kids = [];
let buf = "", depth = 0, head = "", start = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === "{") { if (depth === 0) { head = buf.trim(); buf = ""; start = i + 1; } depth++; }
else if (c === "}") { depth--; if (depth === 0) kids.push([head, text.slice(start, i)]); }
else if (depth === 0) { if (c === ";") { if (buf.trim()) decls.push(buf.trim()); buf = ""; } else buf += c; }
}
if (sel && decls.length) css.push(sel + " {\n" + decls.map((d) => " " + d + ";").join("\n") + "\n}");
for (const [h, body] of kids) walk(body, h.startsWith("&") ? sel + h.slice(1) : sel ? sel + " " + h : h);
})(src, "");
return css.join("\n");
}
// Ví dụ đầu tiên của giáo trình: hai biến màu và lớp .button
console.log(compile(`
$primary-color: #3498db;
$secondary-color: #2ecc71;
.button {
background-color: $primary-color;
color: $secondary-color;
}`));Kết quả là CSS thuần với các tên biến đã được thay bằng giá trị. Đây chính là điều "biên dịch" nghĩa là gì ở đây: thay thế và làm phẳng, rồi xuất tệp CSS.
Sử dụng biến
Ví dụ trong giáo trình khai báo ba biến $font, $color-1, $color-2 rồi dùng chúng cho body và h1:
$font: Arial, Helvetica, sans-serif;
$color-1: white;
$color-2: navy;
body {
font-family: $font;
background-color: $color-1;
}
h1 {
color: $color-2;
}CSS được biên dịch từ SASS:
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
}
h1 {
color: navy;
}Khi biên dịch, giá trị của biến thay thế tên biến. Điều này đảm bảo cùng một giá trị được dùng thống nhất trên toàn Website, và muốn đổi giá trị trong cả Website bạn chỉ cần đổi một chỗ trong biến rồi biên dịch lại.
Mẹo: CSS hiện đại cũng có biến (
--ten: giá trịvàvar(--ten), bạn đã học ở Buổi 7). Khác biệt: biến SASS biến mất sau khi biên dịch (đã thay bằng giá trị), còn biến CSS tồn tại trong trình duyệt và có thể đổi lúc chạy bằng JavaScript.
Chạy thử với đúng ví dụ trên. Hãy đoán trước: kết quả có còn chữ $font nào không?
// Bộ biên dịch SCSS thu nhỏ: biến, lồng quy tắc, &, mixin có tham số
function compile(src) {
src = src.replace(/\/\/.*$/gm, "");
const vars = {}, mixins = {};
src = src.replace(/@mixin\s+([\w-]+)\s*\(([^)]*)\)\s*\{([^}]*)\}/g, (m, name, params, body) => {
mixins[name] = { params: params.split(",").map((p) => p.trim().slice(1)), body };
return "";
});
src = src.replace(/\$([\w-]+)\s*:\s*([^;]+);/g, (m, k, v) => { vars[k] = v.trim(); return ""; });
src = src.replace(/@include\s+([\w-]+)\(([^)]*)\)\s*;/g, (m, name, args) => {
const mx = mixins[name], vals = args.split(",").map((a) => a.trim());
let body = mx.body;
mx.params.forEach((p, i) => { body = body.replace(new RegExp("\\$" + p + "\\b", "g"), vals[i]); });
return body;
});
src = src.replace(/\$([\w-]+)/g, (m, k) => (k in vars ? vars[k] : m));
const css = [];
(function walk(text, sel) {
const decls = [], kids = [];
let buf = "", depth = 0, head = "", start = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === "{") { if (depth === 0) { head = buf.trim(); buf = ""; start = i + 1; } depth++; }
else if (c === "}") { depth--; if (depth === 0) kids.push([head, text.slice(start, i)]); }
else if (depth === 0) { if (c === ";") { if (buf.trim()) decls.push(buf.trim()); buf = ""; } else buf += c; }
}
if (sel && decls.length) css.push(sel + " {\n" + decls.map((d) => " " + d + ";").join("\n") + "\n}");
for (const [h, body] of kids) walk(body, h.startsWith("&") ? sel + h.slice(1) : sel ? sel + " " + h : h);
})(src, "");
return css.join("\n");
}
console.log(compile(`
$font: Arial, Helvetica, sans-serif;
$color-1: white;
$color-2: navy;
body { font-family: $font; background-color: $color-1; }
h1 { color: $color-2; }`));Cách sử dụng SASS để lồng các quy tắc kiểu
Ví dụ trong giáo trình lồng các quy tắc cho ul, li và a của một menu dọc bên trong phần tử nav. Khi biên dịch, nó được chuyển thành CSS tiêu chuẩn với bộ chọn con cháu nav ul, nav li, nav a. Có thể thấy mã SASS dễ đọc hơn CSS vì phản ánh đúng cấu trúc HTML.
SASS cho quy tắc lồng:
nav {
ul {
list-style-type: none;
margin-left: 1cm;
margin-bottom: 1.5cm;
}
li {
width: 150px;
margin-bottom: 0.5cm;
border: 2px solid red;
}
a {
display: block;
padding: 0.5cm 1cm;
text-decoration: none;
color: red;
}
}CSS được dịch ra từ SASS:
nav ul {
list-style-type: none;
margin-left: 1cm;
margin-bottom: 1.5cm;
}
nav li {
width: 150px;
margin-bottom: 0.5cm;
border: 2px solid red;
}
nav a {
display: block;
padding: 0.5cm 1cm;
text-decoration: none;
color: red;
}Để viết bộ chọn gắn với chính phần tử cha (ví dụ lớp giả :hover), SASS dùng ký hiệu &: a { &:hover { ... } } biên dịch thành a:hover. Bài 7.2 ở cuối bài dùng ký hiệu này. Ví dụ sau chạy bộ biên dịch thu nhỏ cho cả hai ý.
// Bộ biên dịch SCSS thu nhỏ: biến, lồng quy tắc, &, mixin có tham số
function compile(src) {
src = src.replace(/\/\/.*$/gm, "");
const vars = {}, mixins = {};
src = src.replace(/@mixin\s+([\w-]+)\s*\(([^)]*)\)\s*\{([^}]*)\}/g, (m, name, params, body) => {
mixins[name] = { params: params.split(",").map((p) => p.trim().slice(1)), body };
return "";
});
src = src.replace(/\$([\w-]+)\s*:\s*([^;]+);/g, (m, k, v) => { vars[k] = v.trim(); return ""; });
src = src.replace(/@include\s+([\w-]+)\(([^)]*)\)\s*;/g, (m, name, args) => {
const mx = mixins[name], vals = args.split(",").map((a) => a.trim());
let body = mx.body;
mx.params.forEach((p, i) => { body = body.replace(new RegExp("\\$" + p + "\\b", "g"), vals[i]); });
return body;
});
src = src.replace(/\$([\w-]+)/g, (m, k) => (k in vars ? vars[k] : m));
const css = [];
(function walk(text, sel) {
const decls = [], kids = [];
let buf = "", depth = 0, head = "", start = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === "{") { if (depth === 0) { head = buf.trim(); buf = ""; start = i + 1; } depth++; }
else if (c === "}") { depth--; if (depth === 0) kids.push([head, text.slice(start, i)]); }
else if (depth === 0) { if (c === ";") { if (buf.trim()) decls.push(buf.trim()); buf = ""; } else buf += c; }
}
if (sel && decls.length) css.push(sel + " {\n" + decls.map((d) => " " + d + ";").join("\n") + "\n}");
for (const [h, body] of kids) walk(body, h.startsWith("&") ? sel + h.slice(1) : sel ? sel + " " + h : h);
})(src, "");
return css.join("\n");
}
console.log(compile(`
nav {
ul { list-style-type: none; margin-left: 1cm; }
li { width: 150px; border: 2px solid red; }
a {
display: block;
color: red;
&:hover { color: navy; }
}
}`));Dòng cuối của kết quả (nav a:hover) cho thấy & ghép bộ chọn cha và :hover không có khoảng trắng. Nếu bạn quên & và viết :hover { ... } lồng trong a, SASS sẽ sinh a :hover (nghĩa là mọi phần tử con ở trạng thái hover), một lỗi khá phổ biến.
Cảnh báo: Đừng lồng quá sâu (quá 3 – 4 cấp). Lồng sâu sinh ra bộ chọn rất dài, độ đặc hiệu cao (đã học ở Buổi 6) và khó ghi đè về sau.
Sử dụng mixins của SASS
Mixin là cách tái sử dụng các đoạn mã CSS. Bạn định nghĩa một tập quy tắc CSS rồi gọi lại ở bất cứ nơi nào cần, thay vì viết lại cùng đoạn mã ở nhiều chỗ. Ví dụ, bạn có thể viết mixin tạo hộp chứa có màu nền rồi gọi mỗi khi cần kiểu đó. Ví dụ đơn giản của giáo trình:
@mixin box($bgcolor) {
width: 80px;
height: 90px;
background-color: $bgcolor;
}
.box1 {
@include box(blue);
}
.box2 {
@include box(yellow);
}CSS được biên dịch từ SASS:
.box1 {
width: 80px;
height: 90px;
background-color: blue;
}
.box2 {
width: 80px;
height: 90px;
background-color: yellow;
}Ở đây mixin tên box có một tham số $bgcolor. Mixin đặt chiều rộng, chiều cao và đặt màu nền bằng giá trị của $bgcolor truyền vào. Hai lớp box1, box2 dùng @include để chèn mixin: lần đầu truyền blue, lần sau truyền yellow.
Xem hai hộp ấy hiện ra như thế nào khi dùng CSS đã biên dịch (ví dụ HTML dưới đây dán sẵn kết quả biên dịch):
<style>
/* CSS biên dịch từ SCSS có @mixin box($bgcolor) */
.box1 { width: 80px; height: 90px; background-color: blue; }
.box2 { width: 80px; height: 90px; background-color: yellow; }
.hang { display: flex; gap: 12px; }
</style>
<div class="hang">
<div class="box1"></div>
<div class="box2"></div>
</div>
<script>
const b1 = getComputedStyle(document.querySelector(".box1"));
console.log("box1:", b1.width, b1.height, b1.backgroundColor);
</script>Và chạy bộ biên dịch thu nhỏ để thấy mixin được "bung" ra:
// Bộ biên dịch SCSS thu nhỏ: biến, lồng quy tắc, &, mixin có tham số
function compile(src) {
src = src.replace(/\/\/.*$/gm, "");
const vars = {}, mixins = {};
src = src.replace(/@mixin\s+([\w-]+)\s*\(([^)]*)\)\s*\{([^}]*)\}/g, (m, name, params, body) => {
mixins[name] = { params: params.split(",").map((p) => p.trim().slice(1)), body };
return "";
});
src = src.replace(/\$([\w-]+)\s*:\s*([^;]+);/g, (m, k, v) => { vars[k] = v.trim(); return ""; });
src = src.replace(/@include\s+([\w-]+)\(([^)]*)\)\s*;/g, (m, name, args) => {
const mx = mixins[name], vals = args.split(",").map((a) => a.trim());
let body = mx.body;
mx.params.forEach((p, i) => { body = body.replace(new RegExp("\\$" + p + "\\b", "g"), vals[i]); });
return body;
});
src = src.replace(/\$([\w-]+)/g, (m, k) => (k in vars ? vars[k] : m));
const css = [];
(function walk(text, sel) {
const decls = [], kids = [];
let buf = "", depth = 0, head = "", start = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === "{") { if (depth === 0) { head = buf.trim(); buf = ""; start = i + 1; } depth++; }
else if (c === "}") { depth--; if (depth === 0) kids.push([head, text.slice(start, i)]); }
else if (depth === 0) { if (c === ";") { if (buf.trim()) decls.push(buf.trim()); buf = ""; } else buf += c; }
}
if (sel && decls.length) css.push(sel + " {\n" + decls.map((d) => " " + d + ";").join("\n") + "\n}");
for (const [h, body] of kids) walk(body, h.startsWith("&") ? sel + h.slice(1) : sel ? sel + " " + h : h);
})(src, "");
return css.join("\n");
}
console.log(compile(`
@mixin box($bgcolor) {
width: 80px;
height: 90px;
background-color: $bgcolor;
}
.box1 { @include box(blue); }
.box2 { @include box(yellow); }`));Lợi ích của biến và mixin là bạn đổi CSS của cả biểu định kiểu chỉ bằng cách đổi giá trị biến hoặc mixin rồi biên dịch lại. Với mixin, chỉ cần sửa một vài dòng trong tệp SCSS, sau biên dịch thay đổi hiện ra trên nhiều dòng của tệp CSS. SASS còn có toán tử số học, chia tệp thành mô-đun và kế thừa; đó là lý do SASS phổ biến như một ngôn ngữ mở rộng của CSS.
Lưu ý: Quy tắc
@importcủa SASS đã ngừng phát triển; hiện nay nên dùng@usevà@forwardđể nhập mô-đun.
Vừa học xong biến, lồng quy tắc và mixin, hãy làm nhóm bài sau để luyện đọc SCSS và dự đoán CSS biên dịch ra.
4. Bootstrap
Giáo trình: Chương 7 — mục "Bootstrap", gồm "Giới thiệu về Bootstrap", "Cách sử dụng hệ thống lưới Bootstrap", "Cách sử dụng các thành phần Bootstrap" và "Biểu tượng (Icon)".
Giới thiệu về Bootstrap
Bootstrap là một framework phát triển Web đầy đủ tính năng, giúp dựng trang Web dễ dàng hơn. Bootstrap gồm bốn mô-đun chính:
| Mô-đun | Mô tả |
|---|---|
| Layout | Các thành phần và lớp CSS để tạo bố cục cho trang Web |
| Content | Các thành phần và lớp CSS để hiển thị nội dung trên trang một cách hấp dẫn |
| Components | Các thành phần giao diện phổ biến: menu điều hướng, nút, biểu mẫu, thanh tiêu đề... |
| Utilities | Các lớp tiện ích để điều chỉnh nhỏ trên các thành phần của Bootstrap |
Về bản chất, Bootstrap là một bộ lớp CSS (class) được viết sẵn cùng một ít JavaScript. Bạn không viết CSS cho từng thứ; bạn gắn các tên lớp có sẵn vào phần tử HTML, chẳng hạn class="btn btn-primary".
Mã cơ bản cần có để dùng Bootstrap trong một trang Web (nạp qua CDN, cần kết nối mạng) như sau:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
/>
<style>
body { margin: 50px; }
</style>
</head>
<body>
<!-- Thêm jQuery (tuỳ chọn: từ phiên bản 5, Bootstrap không còn phụ thuộc jQuery) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Không cần nhúng Popper riêng -->
<!-- vì tệp bootstrap.bundle.min.js bên dưới đã kèm sẵn Popper -->
<!-- Thêm Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>Hãy chú ý ba điểm: (1) thẻ <meta name="viewport" ...> là bắt buộc để thiết kế đáp ứng hoạt động trên điện thoại (Buổi 9); (2) tệp CSS đặt trong head, tệp JS đặt cuối body; (3) tệp bootstrap.bundle.min.js đã kèm Popper (thư viện định vị menu xổ xuống, chú giải) nên không cần nhúng riêng. Từ phiên bản 5, Bootstrap không còn phụ thuộc jQuery.
Mở rộng: CDN (Content Delivery Network) là mạng máy chủ phân phối tệp thư viện. Dùng CDN thì không cần tải thư viện về, nhưng trang sẽ không hiển thị đúng khi mất mạng. Với dự án thật, nhiều nhóm tải tệp về và đặt trong thư mục
assetscủa mình, hoặc cài quanpm.
Cách sử dụng hệ thống lưới Bootstrap
Bootstrap dùng hệ thống lưới (grid) dựa trên vùng chứa (container), hàng (row) và cột (column). Vùng chứa chứa một hoặc nhiều hàng; mỗi hàng chia thành tối đa 12 cột. Bootstrap cung cấp sẵn các lớp CSS để làm việc với lưới này.
<div class="col-md-4 col-sm-6">Cột 1</div><div class="col-md-4 col-sm-6">Cột 2</div><div class="col-md-4 col-sm-12">Cột 3</div>
Mỗi hàng (row) có 12 cột; lớp col-* cho biết một phần tử chiếm bao nhiêu cột, tiền tố sm/md/lg/xl/xxl đổi số cột theo độ rộng màn hình.
Hình trên minh họa lưới 12 cột: các ô col-4, col-6, col-2 ghép lại đủ 12 phần. Hãy đếm số phần mỗi ô chiếm và cộng lại: tổng là 12 thì vừa đủ một hàng.
Các lớp quan trọng nhất (Bảng 7-3 của giáo trình):
| Tên lớp | Công dụng |
|---|---|
container |
Chứa hàng và nội dung khác; các phần tử được căn giữa, chiều rộng dựa trên kích thước màn hình |
container-fluid |
Chứa hàng hoặc nội dung khác; dùng 100% chiều rộng màn hình |
row |
Tạo một hàng để chứa các cột |
col-count |
Số cột mà một phần tử sử dụng |
col-size-count |
Số cột mà một phần tử dùng ở một kích thước màn hình nhất định; các kích thước: sm (từ 576px), md (từ 768px), lg (từ 992px), xl (từ 1200px), xxl (từ 1400px) |
Giáo trình dùng ba ví dụ để bố trí một vùng chứa có một hàng và ba phần tử div.
Ví dụ 1. Vùng chứa container có chiều rộng cố định; mỗi div được gán một lớp cho biết số cột:
<div class="container">
<div class="row">
<div class="col-4">Cột 1</div>
<div class="col-6">Cột 2</div>
<div class="col-2">Cột 3</div>
</div>
</div>Ví dụ 2. Tương tự, nhưng dùng container-fluid nên vùng chứa luôn bằng toàn bộ chiều rộng của khung nhìn (viewport):
<div class="container-fluid">
<div class="row">
<div class="col-4">Cột 1</div>
<div class="col-6">Cột 2</div>
<div class="col-2">Cột 3</div>
</div>
</div>Vì khung chạy thử không nạp được CDN, ví dụ dưới đây tự viết lại khoảng 12 dòng CSS thực hiện đúng nguyên lý của lưới Bootstrap (flex với width theo phần trăm) rồi dựng đúng hai ví dụ trên cạnh nhau. Hãy so sánh: ví dụ 1 bị giới hạn bề rộng và căn giữa, ví dụ 2 trải hết khung.
<style>
* { box-sizing: border-box; }
body { font: 14px sans-serif; margin: 0; padding: 6px; }
/* Lưới thu nhỏ mô phỏng Bootstrap */
.container, .container-fluid { width: 100%; margin: 0 auto; padding: 0 12px; }
.container { max-width: 420px; }
.row { display: flex; flex-wrap: wrap; margin: 0 -12px; }
.row > * { padding: 0 12px; max-width: 100%; width: 100%; flex-shrink: 0; }
.col-4 { flex: 0 0 auto; width: 33.33333333%; }
.col-6 { flex: 0 0 auto; width: 50%; }
.col-2 { flex: 0 0 auto; width: 16.66666667%; }
.row > * > span { display: block; padding: 8px 2px; text-align: center; color: #fff; }
.col-4 > span { background: #3b82f6; } .col-6 > span { background: #10b981; } .col-2 > span { background: #f59e0b; }
h4 { margin: 8px 0 4px; }
</style>
<h4>container (độ rộng cố định, căn giữa)</h4>
<div class="container"><div class="row">
<div class="col-4"><span>Cột 1</span></div><div class="col-6"><span>Cột 2</span></div><div class="col-2"><span>Cột 3</span></div>
</div></div>
<h4>container-fluid (100% khung nhìn)</h4>
<div class="container-fluid"><div class="row">
<div class="col-4"><span>Cột 1</span></div><div class="col-6"><span>Cột 2</span></div><div class="col-2"><span>Cột 3</span></div>
</div></div>
<script>
console.log("Có 2 vùng chứa:", document.querySelectorAll(".container, .container-fluid").length);
console.log("Tổng số cột trong hàng:", 4 + 6 + 2);
</script>Ví dụ 3: layout đáp ứng. Vùng chứa container-fluid như ví dụ 2, nhưng mỗi div mang hai lớp cột. Lớp thứ nhất cho biết số cột trên khung nhìn trung bình (từ 768px), lớp thứ hai cho biết số cột trên khung nhìn nhỏ (từ 576px):
<div class="container-fluid">
<div class="row">
<div class="col-md-4 col-sm-6">Cột 1</div>
<div class="col-md-4 col-sm-6">Cột 2</div>
<div class="col-md-4 col-sm-12">Cột 3</div>
</div>
</div>Lưu ý quan trọng: nếu vùng chứa được hiển thị trên khung nhìn lớn (từ 992px) hoặc cực lớn (từ 1200px), phần tử vẫn trải trên cùng số cột như ở khung nhìn trung bình. Lý do: nếu một lớp không được chỉ định cho một kích thước màn hình, Bootstrap dùng số cột của kích thước nhỏ hơn gần nhất đã được chỉ định (nguyên tắc mobile-first). Vì vậy từ khung nhìn trung bình trở lên, cả ba div đều chiếm 4 cột. Trên khung nhìn nhỏ, hai div đầu chiếm 6 cột, div thứ ba chiếm 12 cột. Vì một hàng không chứa quá 12 cột, div thứ ba xuống dòng bên dưới hai div đầu.
Quy tắc "lấy lớp của breakpoint nhỏ hơn gần nhất" có thể lập trình được. Ví dụ JavaScript sau tính số cột thực tế của từng div ở nhiều chiều rộng khung nhìn:
const BREAKPOINTS = [["xxl", 1400], ["xl", 1200], ["lg", 992], ["md", 768], ["sm", 576], ["xs", 0]];
const tenBreakpoint = (w) => BREAKPOINTS.find(([, min]) => w >= min)[0];
// Số cột hiệu lực: tìm lớp của breakpoint lớn nhất không vượt quá chiều rộng hiện tại
function soCot(lop, w) {
const hieuLuc = {};
for (const l of lop) {
const m = l.match(/^col-(?:(sm|md|lg|xl|xxl)-)?(\d+)$/);
hieuLuc[m[1] || "xs"] = +m[2];
}
for (const [ten, min] of BREAKPOINTS) if (w >= min && ten in hieuLuc) return hieuLuc[ten];
return 12; // không có lớp nào áp dụng: phần tử chiếm cả hàng
}
const cot = [["col-md-4", "col-sm-6"], ["col-md-4", "col-sm-6"], ["col-md-4", "col-sm-12"]];
for (const w of [500, 600, 800, 1000, 1300]) {
console.log(`${w}px (${tenBreakpoint(w)}):`, cot.map((c) => soCot(c, w)).join(" + "));
}Bảng kết quả khớp mô tả của giáo trình: dưới 576px mọi cột chiếm trọn hàng (12), từ 576px là 6 + 6 + 12 (cột ba xuống dòng), từ 768px là 4 + 4 + 4, và các kích thước lớn hơn giữ nguyên 4 + 4 + 4.
Ví dụ HTML sau dựng lại đúng ví dụ 3 bằng lưới thu nhỏ có hai điểm ngắt (breakpoint) 576px và 768px. Hãy thay đổi chiều rộng cửa sổ trình duyệt (hoặc mở trang ở khổ khác nhau) để thấy bố cục đổi từ 1 cột sang 2 cột rồi 3 cột; bảng console bên dưới chỉ xác nhận cấu trúc hàng và lớp của cột thứ ba.
<style>
* { box-sizing: border-box; }
body { font: 14px sans-serif; margin: 0; padding: 6px; }
.container-fluid { width: 100%; padding: 0 12px; }
.row { display: flex; flex-wrap: wrap; margin: 0 -12px; }
.row > * { padding: 0 12px; max-width: 100%; width: 100%; flex-shrink: 0; }
.row > * > span { display: block; margin-bottom: 8px; padding: 14px 2px; text-align: center; color: #fff; background: #6366f1; }
@media (min-width: 576px) {
.col-sm-6 { flex: 0 0 auto; width: 50%; }
.col-sm-12 { flex: 0 0 auto; width: 100%; }
}
@media (min-width: 768px) {
.col-md-4 { flex: 0 0 auto; width: 33.33333333%; }
}
</style>
<div class="container-fluid"><div class="row">
<div class="col-md-4 col-sm-6"><span>Cột 1</span></div>
<div class="col-md-4 col-sm-6"><span>Cột 2</span></div>
<div class="col-md-4 col-sm-12"><span>Cột 3</span></div>
</div></div>
<script>
const cot = [...document.querySelectorAll(".row > div")];
console.log("Số cột trong hàng:", cot.length);
console.log("Lớp của cột 3:", cot[2].className);
</script>Mẹo: Hãy thiết kế mobile-first: viết các lớp cho khổ nhỏ trước (
col-12), rồi thêm lớp cho khổ lớn hơn (col-md-6,col-lg-4). Lớp nào bạn không viết thì tự thừa hưởng từ khổ nhỏ hơn gần nhất.
Mở rộng: Nếu chỉ dùng lớp
col(không kèm số), Bootstrap chia đều phần không gian còn lại giữa các cột cùng hàng. Để chừa khoảng trống bên trái, thêmoffset-N(ví dụcol-6 offset-3đặt cột 6 giữa hàng). Ngoài ra, lưới có thể lồng nhau: trong một cột đặt thêm mộtrowmới với 12 cột con.
Bạn có thể tự do thử mô phỏng sau: chọn số cột cho từng breakpoint và xem hàng dịch chuyển.
Lưới 12 cột của Bootstrap
kéo độ rộng khung nhìn, sửa lớp col-* của từng cột- Cột 1col-sm-6→ 6/12 (50.0%)
- Cột 2col-sm-6→ 6/12 (50.0%)
- Cột 3col-sm-12→ 12/12 (100.0%)
<div class="container-fluid"> <div class="row"> <div class="col-sm-6 col-md-4">Cột 1</div> <div class="col-sm-6 col-md-4">Cột 2</div> <div class="col-sm-12 col-md-4">Cột 3</div> </div> </div>
Quy tắc: nếu một lớp không khai báo cho kích thước màn hình, Bootstrap dùng số cột của kích thước nhỏ hơn gần nhất đã khai báo (mobile-first); cột không có lớp nào chiếm 100% hàng.
Bên trong mô phỏng, hãy thử ba thao tác: đặt col-4 cho ba ô, rồi thêm ô thứ tư (nó sẽ xuống dòng vì vượt 12 cột), và thử đổi khổ màn hình.
Cách sử dụng các thành phần Bootstrap
Giống như hệ thống lưới, các thành phần (components) của Bootstrap phụ thuộc vào các lớp CSS định nghĩa sẵn. Các lớp này dùng để định nghĩa thành phần và điều khiển giao diện, hành vi của chúng. Bootstrap cung cấp hàng chục thành phần dựng sẵn, tái sử dụng được: biểu tượng, danh sách thả xuống, nhóm ô nhập liệu, thanh điều hướng, cảnh báo,... (xem getbootstrap.com).
Ví dụ trong giáo trình dùng thành phần Dropdown để tạo một menu xổ xuống (Hình 7-3). Từ phiên bản 5, menu chỉ cần thuộc tính data-bs-toggle="dropdown", hoặc có thể khởi tạo bằng JavaScript thuần như sau:
<script>
document.querySelectorAll(".dropdown-toggle").forEach(function (el) {
new bootstrap.Dropdown(el);
});
</script>Một menu xổ xuống Bootstrap thật gồm một nút .dropdown-toggle và một danh sách .dropdown-menu; Bootstrap bật/tắt lớp show khi người dùng bấm nút. Ví dụ sau dựng lại hành vi này bằng một đoạn JavaScript thuần rất ngắn và CSS tối giản: bấm nút để mở/đóng menu.
<style>
body { font: 14px sans-serif; padding: 8px; }
.dropdown { position: relative; display: inline-block; }
.btn { padding: 6px 14px; border: 1px solid #0d6efd; background: #0d6efd; color: #fff; border-radius: 6px; cursor: pointer; }
.dropdown-menu { display: none; position: absolute; top: 100%; left: 0; min-width: 160px; margin-top: 4px; padding: 4px 0; background: #fff; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,.15); }
.dropdown-menu.show { display: block; }
.dropdown-item { display: block; padding: 6px 14px; color: #222; text-decoration: none; }
.dropdown-item:hover { background: #e9ecef; }
</style>
<div class="dropdown">
<button class="btn dropdown-toggle" data-bs-toggle="dropdown">Chọn môn học</button>
<ul class="dropdown-menu" style="list-style:none">
<li><a class="dropdown-item" href="#">Thiết kế Web</a></li>
<li><a class="dropdown-item" href="#">Cơ sở dữ liệu</a></li>
<li><a class="dropdown-item" href="#">Lập trình Python</a></li>
</ul>
</div>
<script>
// Mô phỏng hành vi data-bs-toggle="dropdown"
document.querySelectorAll("[data-bs-toggle='dropdown']").forEach((nut) => {
const menu = nut.nextElementSibling;
nut.addEventListener("click", () => {
menu.classList.toggle("show");
console.log("Menu đang", menu.classList.contains("show") ? "mở" : "đóng");
});
});
console.log("Đã gắn", document.querySelectorAll("[data-bs-toggle]").length, "menu xổ xuống");
</script>Điều đáng nhớ: bạn không viết CSS cho nút, menu hay mục menu. Trong Bootstrap thật, ba lớp btn btn-primary, dropdown-menu, dropdown-item đã có sẵn kiểu. Một số lớp thường dùng khác: bảng <table class="table"> (câu hỏi 7.6), nút btn btn-primary, cảnh báo alert alert-warning, thanh điều hướng navbar.
Ví dụ thực tế: Trang quản trị nội bộ của một công ty cần bảng, nút, form và thanh điều hướng thật nhanh, không cần thiết kế riêng: Bootstrap cho kết quả "đủ đẹp" chỉ sau vài chục dòng HTML. Ngược lại, trang giới thiệu thương hiệu cần diện mạo độc đáo thường tự viết CSS (hoặc SASS) riêng.
Biểu tượng (Icon)
Cách đơn giản nhất để thêm biểu tượng vào trang HTML là dùng một thư viện biểu tượng, chẳng hạn Font Awesome. Bạn chỉ cần thêm tên lớp của biểu tượng vào một phần tử nội tuyến bất kỳ như <i> hoặc <span>. Các biểu tượng trong những thư viện này đều là ảnh vectơ, nên phóng to thu nhỏ tùy ý và tùy chỉnh bằng CSS (kích thước, màu sắc, bóng đổ,...).
Để dùng glyphicons của Bootstrap, giáo trình hướng dẫn thêm liên kết sau vào phần <head> (bản Bootstrap 3.3.7):
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">Ví dụ:
<i class="glyphicon glyphicon-cloud"></i>
<i class="glyphicon glyphicon-remove"></i>
<i class="glyphicon glyphicon-user"></i>
<i class="glyphicon glyphicon-envelope"></i>Lưu ý: Glyphicons chỉ có trong Bootstrap 3; từ Bootstrap 4 nó đã bị bỏ. Với Bootstrap 5, hãy dùng thư viện biểu tượng riêng như Bootstrap Icons hoặc Font Awesome. Đừng nạp lẫn Bootstrap 3 và Bootstrap 5 trong cùng một trang vì các lớp CSS sẽ xung đột.
Tính chất "vectơ, đổi kích thước và màu bằng CSS" có thể kiểm chứng bằng một biểu tượng SVG tự vẽ (không cần thư viện). Ví dụ sau vẽ biểu tượng phong bì và đổi cỡ, màu chỉ bằng CSS:
<style>
.icon { width: 1em; height: 1em; vertical-align: -0.125em; fill: none; stroke: currentColor; stroke-width: 2; }
.nho { font-size: 16px; color: #555; }
.vua { font-size: 32px; color: #0d6efd; }
.lon { font-size: 64px; color: #dc3545; }
</style>
<svg style="display:none">
<symbol id="thu" viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="2"></rect><polyline points="2,7 12,14 22,7"></polyline></symbol>
</svg>
<p>
<span class="nho"><svg class="icon"><use href="#thu"></use></svg></span>
<span class="vua"><svg class="icon"><use href="#thu"></use></svg></span>
<span class="lon"><svg class="icon"><use href="#thu"></use></svg></span>
</p>
<script>
console.log("Số biểu tượng:", document.querySelectorAll("svg.icon").length, "- cùng một nguồn vectơ, ba kích thước/màu khác nhau");
</script>Ba biểu tượng cùng một mã SVG nhưng khác font-size và color, vẫn sắc nét ở mọi cỡ: đó là lợi thế của ảnh vectơ so với ảnh bitmap (đã học ở Buổi 5).
Vừa học xong lưới và thành phần Bootstrap, hãy làm nhóm bài sau để luyện tính số cột, phần trăm bề rộng và cách các cột xuống dòng.
5. jQuery
Giáo trình: Chương 7 — mục "jQuery" (Hình 7-4, 7-5), gồm các mục con từ "Chạy mã khi sẵn sàng với $(document).ready()" đến "jQuery trong bối cảnh hiện nay".
Giới thiệu jQuery
jQuery là một thư viện JavaScript. Khẩu hiệu của jQuery là "write less, do more" (viết ít, làm nhiều): thư viện giúp viết ít mã JavaScript hơn mà vẫn làm được nhiều việc hơn (Hình 7-4 là biểu tượng và khẩu hiệu của jQuery). Để dùng jQuery bạn cần có kiến thức JavaScript, vì jQuery chỉ là cách viết gọn cho các thao tác DOM, sự kiện và AJAX bạn đã học ở Buổi 12 và 13.
Để tích hợp jQuery vào dự án, thêm thẻ script sau vào trang. Lưu ý không dùng bản slim vì bản này đã lược bỏ $.ajax:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>Giáo trình tạo một trang HTML đơn giản gồm danh sách mua sắm và hai nút:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ví dụ jQuery</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body>
<h2>Danh sách mua sắm</h2>
<ul id="shopping-list">
<li>Sữa</li>
<li>Trứng</li>
<li>Bánh mì</li>
</ul>
<button id="add-item">Thêm mục</button>
<button id="remove-item">Xóa mục</button>
<script src="script.js"></script>
</body>
</html>Nội dung tệp script.js:
$(document).ready(function(){
//Thêm một mục mới
$("#add-item").click(function(){
var newItem = prompt("Nhập một mục mới:");
if (newItem) {
$("#shopping-list").append("<li>" + newItem + "</li>");
}
});
//Xóa mục cuối cùng
$("#remove-item").click(function(){
$("#shopping-list li:last").remove();
});
});Giải thích mã trên:
$(document).ready(function(){...}): đảm bảo mọi mã jQuery chỉ chạy sau khi tài liệu HTML đã tải xong.$("#add-item").click(function(){...}): gán sự kiện click cho nút có idadd-item; khi bấm, hộp thoạiprompthiện ra để nhập mục mới cho danh sách.$("#remove-item").click(function(){...}): gán sự kiện click cho nútremove-item; khi bấm, phần tử<li>cuối cùng trong danh sách bị xóa nhờ phương thức.remove().
Thư viện thật cần CDN, nên ví dụ chạy thử dưới đây dùng mini-jQuery (khoảng 50 dòng) do lớp tự dựng, đủ để chạy nguyên văn mã script.js ở trên. Bạn sẽ thấy $ thực ra chỉ là một hàm: nhận bộ chọn, trả về một đối tượng bọc (wrapper) chứa các phần tử cùng các phương thức có thể gọi nối tiếp (chaining). Mã sau giả lập người dùng bấm "Thêm mục" một lần (nhập "Cà chua") rồi bấm "Xóa mục" hai lần.
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
document.body.innerHTML = `<h2>Danh sách mua sắm</h2>
<ul id="shopping-list"><li>Sữa</li><li>Trứng</li><li>Bánh mì</li></ul>
<button id="add-item">Thêm mục</button><button id="remove-item">Xóa mục</button>`;
// ---- nguyên văn script.js của giáo trình ----
$(document).ready(function(){
$("#add-item").click(function(){
var newItem = prompt("Nhập một mục mới:");
if (newItem) {
$("#shopping-list").append("<li>" + newItem + "</li>");
}
});
$("#remove-item").click(function(){
$("#shopping-list li:last").remove();
});
});
// ---- hết script.js ----
const mucs = () => $("#shopping-list li").els.map((li) => li.textContent).join(", ");
console.log("Ban đầu:", mucs());
$("#add-item").click();
console.log("Sau khi thêm:", mucs());
$("#remove-item").click();
$("#remove-item").click();
console.log("Sau khi xóa 2 lần:", mucs());Hầu hết mã jQuery được viết theo kiểu $(bộ chọn).phươngThức(...), ví dụ $("#title").html("Hello world");. Ngoài ra jQuery còn có các hàm tiện ích gọi trực tiếp trên $, chẳng hạn $.trim(" lots of extra whitespace ").
Lưu ý:
$.trimđã lỗi thời từ jQuery 3.5; hãy dùng phương thứctrim()sẵn có của chuỗi JavaScript. Giáo trình giữ lại ví dụ này để bạn nhận ra khi gặp trong mã cũ.
Như vậy trong jQuery có hai dạng gọi là $() (chọn phần tử, trả về đối tượng jQuery) và $ (các hàm tiện ích). Khi lập trình ta kết hợp cả hai:
<h1 id="title"></h1>
<script>
$("#title").append($.trim( " My Title " ))
</script>Hoặc dùng $.each để duyệt mảng và thêm từng mục vào danh sách:
<ul id="shopping-list"></ul>
<script>
$.each(["Sữa", "Trứng", "Bánh mì"], function (idx, val) {
$("#shopping-list").append("<li>" + val + "</li>");
});
</script>Mini-jQuery chạy được đúng đoạn mã thứ hai. Kết quả trên trang là danh sách ba mục như Hình 7-5 của giáo trình.
<ul id="shopping-list"></ul>
<script>
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
$.each(["Sữa", "Trứng", "Bánh mì"], function (idx, val) {
$("#shopping-list").append("<li>" + val + "</li>");
});
console.log("Số mục:", $("#shopping-list li").length);
</script>$("#id")document.querySelector("#id")$(".lop")document.querySelectorAll(".lop")$(el).addClass("x")el.classList.add("x")$(el).on("click", f)el.addEventListener("click", f)$.get(url)fetch(url).then(res => res.json())$(document).ready(f)document.addEventListener("DOMContentLoaded", f)$("#title").html("Hello world")$.trim(" lots of extra whitespace ")Cùng một việc, jQuery viết ngắn hơn (write less, do more); nay các API sẵn có của trình duyệt đã đủ tiện dụng nên dự án mới ít dùng jQuery.
Sơ đồ trên đặt hai cách viết cạnh nhau: bên trái là jQuery gọn, bên phải là JavaScript thuần tương đương. Bạn sẽ gặp lại bảng đối chiếu chi tiết ở cuối phần jQuery.
Chạy mã khi sẵn sàng với $(document).ready()
Bạn không thể thao tác với các phần tử của trang khi trang chưa ở trạng thái sẵn sàng (ready). jQuery phát hiện trạng thái này và thực thi các câu lệnh khi trang đã sẵn sàng, thông qua hàm $(document).ready():
$(document).ready(function() {
console.log( "Sẵn sàng!" );
});Từ jQuery 3.0, cách viết gọn $(function () { ... }) được khuyến nghị dùng thay cho $(document).ready().
Vì sao cần chờ? Ví dụ dưới đây (JavaScript thuần) cho thấy script đặt trước phần tử không tìm thấy phần tử đó, còn sau sự kiện DOMContentLoaded thì tìm thấy. Đó chính là cơ chế bên dưới ready().
<script>
console.log("Trước khi trang dựng xong:", document.getElementById("tieude"));
document.addEventListener("DOMContentLoaded", function () {
console.log("Sẵn sàng! #tieude là", document.getElementById("tieude").tagName);
});
</script>
<h1 id="tieude">Xin chào</h1>Dòng đầu in null vì thẻ <h1> ở bên dưới chưa được trình duyệt đọc tới. Dòng sau in H1.
Mẹo: Nếu bạn đặt thẻ
<script>ngay trước</body>(như tệp mẫu của giáo trình) thì phần tử đã có sẵn khi mã chạy; dù vậy vẫn nên dùngreadyđể mã chạy đúng ở mọi vị trí.
Các bộ chọn
jQuery hỗ trợ hầu hết các bộ chọn (selector) của CSS3 (bạn đã học ở Buổi 6):
$("#myId"); // chọn theo ID
$(".myClass"); // chọn theo tên lớp
$("input[name='first_name']"); // chọn theo tên và giá trị của thuộc tính
$("#contents ul.people li"); // dùng CSS kết hợpNgoài ra jQuery có thêm một số bộ chọn phần tử giả (pseudo) riêng của nó:
$("a.external:first");
$("tr:odd");
$("#myForm :input");
$("div:visible");
$("div:gt(2)");
$("div:animated");Lưu ý: Các bộ chọn như
:first,:odd,:gt(),:visible,:inputlà phần mở rộng của jQuery, không có trong CSS chuẩn nêndocument.querySelectorAll()không hiểu chúng. Bản jQuery gần đây khuyến nghị thay:first,:eq()bằng phương thức.first(),.eq()để thư viện tận dụng được bộ chọn gốc của trình duyệt.
Sau khi chọn, ta có thể lọc tiếp để lấy đúng phần tử mong muốn:
$("div.foo").has("p"); // các div.foo có chứa thẻ <p>
$("h1").not(".bar"); // các h1 không có lớp bar
$("ul li").filter(".current"); // các mục danh sách có lớp current
$("ul li").first(); // chỉ mục đầu tiên của danh sách
$("ul li").eq(5); // mục thứ sáu (đếm từ 0)Kết quả chọn có thể lưu vào biến để dùng lại: var divs = $("div");. Ví dụ dưới đây dùng mini-jQuery chọn và lọc trên một danh sách thật. Hãy đoán trước: eq(2) là mục thứ mấy?
<ul id="kho">
<li>Bút</li><li class="current">Vở</li><li>Thước</li><li class="current">Tẩy</li>
</ul>
<script>
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
console.log("Tất cả li:", $("#kho li").length);
console.log("filter('.current'):", $("#kho li").filter(".current").els.map((e) => e.textContent));
console.log("not('.current'):", $("#kho li").not(".current").els.map((e) => e.textContent));
console.log("first():", $("#kho li").first().text());
console.log("eq(2):", $("#kho li").eq(2).text());
console.log("li:odd:", $("#kho li:odd").els.map((e) => e.textContent));
</script>Chỉ số của eq() và :odd đều tính từ 0: eq(2) là mục thứ ba ("Thước"), còn :odd lấy các mục có chỉ số 1 và 3 ("Vở", "Tẩy").
Lấy và thiết lập giá trị
Các phần tử đã chọn dùng được với nhiều phương thức. Một số phương thức thông dụng để đọc hoặc thiết lập giá trị:
| Phương thức | Tác dụng |
|---|---|
.html() |
Lấy ra toàn bộ mã HTML bên trong phần tử |
.text() |
Lấy ra văn bản bên trong phần tử |
.attr() |
Lấy ra giá trị thuộc tính |
.width() |
Lấy hoặc thiết lập độ rộng |
.height() |
Lấy hoặc thiết lập độ cao |
.val() |
Lấy ra giá trị của phần tử biểu mẫu (form) |
Quy luật chung: không có đối số thì là đọc, có đối số thì là ghi:
$("h1").html(); // Đọc giá trị
$("h1").html("Xin chào"); // Thiết lập giá trịCảnh báo:
.html(chuỗi)hiểu chuỗi là HTML (giốnginnerHTML), còn.text(chuỗi)coi là văn bản thuần (giốngtextContent). Đưa dữ liệu người dùng nhập vào.html()hoặc nối vào.append("<li>" + ... + "</li>")mà không kiểm tra có thể mở đường cho tấn công XSS; với văn bản người dùng, hãy dùng.text().
Các phương thức sau dùng để di chuyển phần tử: .insertAfter() (chèn vào sau), .insertBefore() (chèn vào trước), .appendTo() (ghép vào cuối), .prependTo() (ghép vào đầu). Với HTML ban đầu:
<h1 id="title">Title</h1>
Content
<ul id="list">
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>Giáo trình lần lượt cho kết quả của $("#list").insertAfter("#title") (danh sách nằm ngay sau h1, trước chữ "Content"), $("#list").insertBefore("#title") (danh sách lên trước h1), $("#list").appendTo("#title") (danh sách nằm cuối bên trong h1) và $("#list").prependTo("#title") (danh sách nằm đầu bên trong h1, trước chữ "Title"). Ví dụ dưới đây dựng lại từng trường hợp bằng JavaScript thuần (after, before, append, prepend) và in mã HTML kết quả, kèm cả clone(), remove() và empty():
const GOC = '<h1 id="title">Title</h1>Content<ul id="list"><li>Apple</li><li>Banana</li><li>Orange</li></ul>';
function thu(ten, thaoTac) {
document.body.innerHTML = GOC;
thaoTac(document.getElementById("title"), document.getElementById("list"));
console.log(ten.padEnd(34), document.body.innerHTML.replace(/<li>.*?<\/li>/g, "").replace(/<ul id="list"><\/ul>/g, "<ul#list>"));
}
thu("insertAfter → title.after(list)", (t, l) => t.after(l));
thu("insertBefore → title.before(list)", (t, l) => t.before(l));
thu("appendTo → title.append(list)", (t, l) => t.append(l));
thu("prependTo → title.prepend(list)", (t, l) => t.prepend(l));
thu("clone().insertAfter", (t, l) => t.after(l.cloneNode(true)));
thu("remove()", (t, l) => l.remove());
thu("empty()", (t, l) => (l.innerHTML = ""));
document.body.innerHTML = GOC;
const ban = document.getElementById("list").cloneNode(true);
document.getElementById("title").after(ban);
console.log("Số phần tử có id=list sau khi clone:", document.querySelectorAll("#list").length);Mỗi dòng in ra hiển thị vị trí <ul#list> so với h1 và chữ "Content" (các <li> được bỏ bớt cho gọn). Dòng cuối nhắc một lưu ý của giáo trình: bản sao từ clone() giữ nguyên thuộc tính id, nên trang có hai phần tử trùng id và cần đổi id của bản sao.
Để sao chép phần tử, dùng clone(); để xóa phần tử, dùng remove() (xóa cả phần tử); empty() chỉ xóa nội dung bên trong, phần tử vẫn còn (ở ví dụ trên là thẻ <ul id="list"></ul> rỗng). Để lấy hoặc đổi thuộc tính, dùng .attr():
var hrefValue = $("#myDiv a:first").attr("href");
$("#myDiv a:first").attr("href", "newDestination.html");Tìm kiếm phần tử xung quanh
Từ một phần tử, jQuery cho phép "đi quanh" cây DOM (Buổi 12). Với HTML ban đầu:
<div class="body">
<h1 id="title">Title</h1>
Content
<ul id="list">
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>
</div>| Mục đích | jQuery | Ghi chú |
|---|---|---|
| Phần tử cha | parent() |
$("#title").parent().addClass("bg-dark") thêm lớp cho div cha |
| Mọi tổ tiên | parents() |
Tất cả cha, ông,... của phần tử |
| Tổ tiên gần nhất | closest("div") |
Cha là thẻ div gần nhất |
| Các con trực tiếp | children() |
children("h1") chỉ lấy con là h1 |
| Hậu duệ ở mọi cấp | find("li") |
Tìm sâu hơn mọi cấp |
| Phần tử cùng cấp | next(), prev(), nextAll(), prevAll() |
$("#title").next() là ul#list |
Ví dụ dưới đây dùng mini-jQuery thực hiện đúng các thao tác của giáo trình rồi in các lớp của từng phần tử:
<div class="body">
<h1 id="title">Title</h1>
Content
<ul id="list"><li>Apple</li><li>Banana</li><li>Orange</li></ul>
</div>
<script>
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
$("#title").parent().addClass("bg-dark");
console.log("div.body:", $(".body").attr("class"));
$(".body").removeClass("bg-dark").children().addClass("bg-dark");
console.log("children():", $(".body").children().els.map((e) => e.tagName + (e.className ? "." + e.className : "")));
console.log("children('h1'):", $(".body").children().filter("h1").length, "| find('li'):", $(".body").find("li").length);
$(".body").children().removeClass("bg-dark");
$("#title").next().addClass("bg-dark");
console.log("sau next().addClass:", document.getElementById("list").className);
console.log("siblings của #title:", $("#title").siblings().length);
</script>Hãy đối chiếu: children() trả về hai phần tử (H1 và UL; chữ "Content" là nút văn bản nên không tính), find("li") tìm ra cả ba li ở cấp sâu hơn, và next() của #title chính là danh sách ul. Trong JavaScript thuần bạn dùng parentElement, children, querySelectorAll, nextElementSibling và closest().
Vừa học xong cách chọn, đọc/ghi giá trị và duyệt cây DOM, hãy làm nhóm bài sau để luyện đếm và chọn phần tử đúng như jQuery.
Định dạng kiểu
Hàm css() dùng để lấy giá trị một thuộc tính CSS của phần tử, ví dụ thuộc tính fontSize:
console.log($("h1").css("fontSize"))css() cũng dùng để thiết lập thuộc tính, một thuộc tính hoặc nhiều thuộc tính cùng lúc bằng một đối tượng:
$("h1").css("fontSize", "100px")
$("h1").css({
fontSize: "100px",
color: "red"
});Các hàm addClass(), removeClass(), toggleClass() thêm, xóa hoặc bật/tắt lớp (class), ví dụ $("h1").addClass("big");. Các phần tử còn có thể chứa dữ liệu: dùng hàm data() để lưu và lấy, ví dụ $("#title").data("x", 12);.
Mẹo: Nên đổi kiểu bằng
addClass/removeClass(kiểu viết sẵn trong CSS) hơn làcss()từng thuộc tính. Như vậy phần trình bày nằm trong CSS, phần hành vi nằm trong JavaScript, đúng nguyên tắc tách biệt đã học ở Buổi 2.
Ví dụ sau chạy các thao tác trên bằng mini-jQuery (với data() của jQuery, JavaScript thuần dùng dataset hoặc data-*):
<style>
.big { text-decoration: underline; }
</style>
<h1 id="title" style="font-size: 20px">Tiêu đề</h1>
<script>
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
console.log("Đọc css:", $("h1").css("fontSize"));
$("h1").css({ fontSize: "40px", color: "red" });
console.log("Sau khi đặt:", $("h1").css("fontSize"), $("h1").css("color"));
$("h1").addClass("big");
$("h1").toggleClass("big");
$("h1").toggleClass("big");
console.log("class sau 1 add + 2 toggle:", $("h1").attr("class"));
// data() của jQuery giữ nguyên kiểu số; dataset luôn trả về chuỗi
document.getElementById("title").dataset.x = 12;
console.log("dataset.x:", typeof document.getElementById("title").dataset.x, document.getElementById("title").dataset.x);
</script>Sự kiện
jQuery cho phép gán sự kiện cho các phần tử HTML rất dễ dàng:
$("#title").click(function (){
console.log( "You clicked the title!" );
})Sự kiện click cũng gán được bằng hàm on():
$("#title").on("click", function (){
console.log( "You clicked the title!" );
})Các sự kiện khác cũng gán được; để lấy giá trị liên quan đến sự kiện, dùng đối tượng event:
$("input").on("change", function( event ) {
console.log(event.target.value);
});Các thao tác còn lại thường dùng:
| Mục đích | Mã |
|---|---|
| Gán sự kiện nhưng chỉ cho dùng một lần | $("#title").one("click", function () { console.log("Bạn vừa bấm vào tiêu đề!"); }) |
| Gỡ bỏ sự kiện | $("#title").off("click") |
| Kích hoạt sự kiện bằng mã (không cần người dùng) | $("#title").trigger("click"); |
Ví dụ sau thử bốn hàm với mini-jQuery. Hãy đoán số lần dòng "bấm" được in trước khi chạy: on chạy mỗi lần bấm, one chỉ lần đầu, và sau off thì on cũng không còn chạy.
<h2 id="title">Bấm vào tôi</h2>
<script>
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
let dem = 0;
$("#title").on("click", function () { dem++; console.log("on: bấm lần", dem); });
$("#title").one("click", function () { console.log("one: chỉ in một lần"); });
$("#title").trigger("click"); // bấm bằng mã
$("#title").trigger("click");
$("#title").off("click");
$("#title").trigger("click"); // đã gỡ: không in gì
console.log("Tổng số lần on chạy:", dem);
</script>Lưu ý: Với JavaScript thuần, tương đương của
onlàaddEventListener, củaofflàremoveEventListener(cần giữ tham chiếu tới hàm), củaonelà tùy chọn{ once: true }.
Hiệu ứng
jQuery có thể ẩn hiện thông tin bằng show() và hide(), kèm hiệu ứng nếu thêm tốc độ "slow", "fast" hoặc một số mili giây:
$("p").hide();
$("div.hidden").show();
$("p").hide("slow");
$("div.hidden").show("fast");
$("p").hide(500);
$("div.hidden").show(1250);Các hàm khác cho hiệu ứng ẩn/hiện:
$("p").slideUp(800); // cuộn lên để ẩn
$("div.hidden").slideDown(600); // cuộn xuống để hiện
$("p").fadeOut(1500); // mờ dần rồi ẩn
$("div.hidden").fadeIn(750); // hiện dần
$("p").toggle(); // chuyển trạng thái ẩn/hiện
$("img").toggle("slow");
$("div").toggle(1800);Có thể nối thêm lời gọi khác ngay sau hiệu ứng, nhưng phương thức nối thêm chạy ngay lập tức, không chờ hiệu ứng kết thúc:
$("p.hidden").fadeIn(750).addClass("lookAtMe");Muốn một việc chỉ chạy sau khi hiệu ứng kết thúc, truyền vào một hàm callback:
$("p.hidden").fadeIn(750, function() {
$(this).addClass("lookAtMe");
});Sự khác biệt này khá tinh tế nên hãy kiểm chứng. Ví dụ sau tự dựng một hàm fadeIn giả lập (đợi hết thời gian rồi mới gọi callback) để thấy thứ tự thực hiện của hai cách viết:
function fadeIn(ms, callback) {
console.log(" [hiệu ứng] bắt đầu,", ms, "ms");
setTimeout(function () {
console.log(" [hiệu ứng] kết thúc");
if (callback) callback();
}, ms);
}
console.log("Cách 1: nối lời gọi");
fadeIn(300); // giống .fadeIn(750).addClass(...)
console.log(" addClass chạy NGAY (hiệu ứng chưa xong)");
setTimeout(function () {
console.log("Cách 2: dùng callback");
fadeIn(300, function () {
console.log(" addClass chạy SAU khi hiệu ứng kết thúc");
});
}, 500);Trong trình duyệt, hiệu ứng ẩn/hiện bằng CSS hiện nay thường êm hơn và nhẹ hơn: bạn bật/tắt một lớp và để thuộc tính transition lo phần chuyển động. Ví dụ sau làm điều tương tự fadeOut/fadeIn/toggle mà không cần thư viện (bấm nút để thử):
<style>
#hop { width: 160px; padding: 14px; background: #fde68a; border-radius: 8px; transition: opacity .6s; }
#hop.mo { opacity: 0; }
</style>
<button id="nut">Bật/tắt</button>
<div id="hop">Hộp thông báo</div>
<script>
const nut = document.getElementById("nut"), hop = document.getElementById("hop");
nut.addEventListener("click", () => {
hop.classList.toggle("mo");
console.log("Hộp đang", hop.classList.contains("mo") ? "mờ (ẩn)" : "hiện");
});
// Giả lập một lần bấm để thấy kết quả trong console
nut.click();
</script>Sử dụng AJAX
jQuery cũng hỗ trợ AJAX, giúp gửi yêu cầu tới máy chủ và xử lý bất đồng bộ (bạn đã học XMLHttpRequest và fetch() ở Buổi 13). Ví dụ gửi một yêu cầu GET khi bấm vào tiêu đề:
$("#title").click(function () {
$.get("https://jsonplaceholder.typicode.com/posts/1", function (data) {
$("#title").html(data.title);
});
});Ngoài get() còn có post(). Với các phương thức HTTP khác dùng hàm ajax(), trả về một đối tượng cho phép nối .done(), .fail() và .always():
$.ajax({
url: "https://jsonplaceholder.typicode.com/posts/1",
type: "GET"
}).done(function (data) {
$("#title").html(data.title);
}).fail(function (xhr, status, errorThrown) {
$("#title").html("Rất tiếc, đã có lỗi xảy ra!");
}).always(function (xhr, status) {
console.log("Yêu cầu đã hoàn tất!");
});Dữ liệu có thể được gửi kèm theo một yêu cầu POST (ở đây gửi JSON, nhớ khai báo Content-Type):
$.ajax({
"url": "http://localhost:8080/api/movie",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"data": JSON.stringify({"title":"Superman","year":2012}),
}).done(function (response) {
console.log(response);
});Để thao tác nhanh, bạn có thể dùng Postman: gửi thử yêu cầu để kiểm tra dữ liệu từ máy chủ (Hình 7-6), rồi dùng chức năng xuất mã để lấy sẵn đoạn mã jQuery Ajax (Hình 7-7), như Bài 6.3 ở Buổi 13.
Vì khung chạy thử không có mạng, ví dụ sau dùng một "máy chủ giả" (hàm trả về Promise) để chứng minh sự tương ứng giữa $.ajax().done().fail().always() và fetch().then().catch().finally():
// Máy chủ giả: URL có "loi" thì thất bại, còn lại trả JSON
function mayChuGia(url) {
return new Promise((ok, loi) => setTimeout(() => (url.includes("loi") ? loi(new Error("404")) : ok({ id: 1, title: "Bài viết mẫu" })), 50));
}
async function thu(url) {
// Tương đương $.ajax({url}).done(...).fail(...).always(...)
await mayChuGia(url)
.then((data) => console.log("done:", data.title)) // .done
.catch((err) => console.log("fail:", err.message)) // .fail
.finally(() => console.log("always: hoàn tất", url)); // .always
}
(async () => {
await thu("/api/posts/1");
await thu("/api/loi");
})();Khi gửi biểu mẫu (submit), nên kiểm tra dữ liệu nhập, rồi gửi yêu cầu bằng AJAX và gọi event.preventDefault() để trình duyệt không gửi biểu mẫu theo cách thông thường (gây tải lại trang). Mã của giáo trình:
<form action="http://localhost:8080/api/movie" method="post" id="form">
<input type="text" name="title" id="title" class="required">
<input type="text" name="year" id="year" class="required">
<input type="submit">
</form>
<script>
$(document).ready(function () {
$("#form").submit(function (event) {
//Nếu có ô bắt buộc nào còn để trống
if ($(".required").toArray().some(function (el) { return $(el).val().length === 0; })) {
//Hiển thị thông báo lỗi ở đây
} else {
var settings = {
"url": "http://localhost:8080/api/movie",
"method": "POST",
"timeout": 0,
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"data": {
"title": $("#title").val(),
"year": $("#year").val()
}
};
$.ajax(settings).done(function (response) {
$("#form").append("<br>" + response)
});
}
event.preventDefault();
});
})
</script>Ví dụ JavaScript thuần tương đương dưới đây giữ nguyên logic (kiểm tra ô bắt buộc, gửi, chèn phản hồi, preventDefault), nhưng gửi tới máy chủ giả. Trang tự điền dữ liệu mẫu và gửi một lần để bạn thấy kết quả; bạn có thể xóa trống một ô rồi bấm "Gửi" để thấy nhánh lỗi.
<form id="form">
<input type="text" name="title" id="title" class="required" placeholder="Tên phim">
<input type="text" name="year" id="year" class="required" placeholder="Năm" size="6">
<input type="submit" value="Gửi">
</form>
<p id="thongbao" style="color:#b91c1c"></p>
<script>
const guiGia = (du_lieu) => Promise.resolve("Đã lưu phim: " + du_lieu.title + " (" + du_lieu.year + ")");
document.getElementById("form").addEventListener("submit", function (event) {
event.preventDefault(); // không để trình duyệt tải lại trang
const trong = [...document.querySelectorAll(".required")].some((el) => el.value.length === 0);
const tb = document.getElementById("thongbao");
if (trong) { tb.textContent = "Vui lòng điền đủ các ô bắt buộc."; console.log("Thiếu dữ liệu"); return; }
tb.textContent = "";
guiGia({ title: title.value, year: year.value }).then((res) => {
this.insertAdjacentHTML("beforeend", "<br>" + res);
console.log("Phản hồi:", res);
});
});
// Tự điền dữ liệu mẫu và gửi một lần
title.value = "Superman"; year.value = "2012";
document.getElementById("form").dispatchEvent(new Event("submit", { cancelable: true }));
</script>Cảnh báo: Kiểm tra dữ liệu ở trình duyệt chỉ để cải thiện trải nghiệm. Máy chủ vẫn phải kiểm tra lại mọi dữ liệu nhận được (Buổi 13). Ngoài ra, địa chỉ
localhost:8080trong ví dụ chỉ chạy trên máy bạn; khi triển khai thật hãy thay bằng địa chỉ máy chủ thực và dùng HTTPS.
Các plugin của jQuery
jQuery có rất nhiều plugin (phần mở rộng) được tập hợp tại trang plugins.jquery.com. Ngoài ra còn nhiều thư viện hoạt động dựa trên jQuery như DataTables (datatables.net, bảng có tìm kiếm và sắp xếp, bạn sẽ thực hành ở Bài 7.1), Muuri (muuri.dev, bố cục lưới kéo thả) và Face Detection (facedetection.jaysalvat.com, nhận diện khuôn mặt trên ảnh).
jQuery trong bối cảnh hiện nay
jQuery ra đời năm 2006, thời điểm mỗi trình duyệt hiện thực DOM một kiểu khác nhau. Thư viện này che đi những khác biệt đó và cung cấp cú pháp ngắn gọn, nhờ vậy từng có mặt trên phần lớn Website. Ngày nay các API sẵn có của trình duyệt đã thống nhất và đủ tiện dụng, nên các dự án mới thường không dùng jQuery nữa; Bootstrap từ phiên bản 5 cũng bỏ hẳn phụ thuộc vào thư viện này.
Tuy vậy jQuery vẫn cần được giới thiệu, vì một khối lượng lớn mã nguồn đang vận hành, cùng nhiều mã nguồn mở và giao diện quản trị, vẫn được viết bằng jQuery. Bảng đối chiếu vài thao tác thường gặp:
| jQuery | JavaScript thuần |
|---|---|
$("#id") |
document.querySelector("#id") |
$(".lop") |
document.querySelectorAll(".lop") |
$(el).addClass("x") |
el.classList.add("x") |
$(el).on("click", f) |
el.addEventListener("click", f) |
$.get(url) |
fetch(url).then(res => res.json()) |
Ví dụ sau xác nhận từng cặp tương đương bằng mini-jQuery (cho ba cặp đầu) và fetch giả (cặp cuối):
<p id="a" class="lop">A</p><p class="lop">B</p>
<script>
// mini-jQuery: chỉ để học, KHÔNG thay thế thư viện thật
function $(s) {
if (typeof s === "function") return document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s);
let els;
if (typeof s !== "string") els = [s];
else {
const m = s.match(/^(.*?):(first|last|odd|even|eq\((\d+)\))$/);
els = [...document.querySelectorAll(m ? m[1] : s)];
if (m) els = m[2] === "first" ? els.slice(0, 1) : m[2] === "last" ? els.slice(-1) : m[2] === "odd" ? els.filter((_, i) => i % 2) : m[2] === "even" ? els.filter((_, i) => !(i % 2)) : els.slice(+m[3], +m[3] + 1);
}
return wrap(els);
}
function wrap(els) {
const q = { els, length: els.length };
const each = (f) => { els.forEach(f); return q; };
const getSet = (get, set) => (v) => (v === undefined ? (els[0] ? get(els[0]) : undefined) : each((e) => set(e, v)));
Object.assign(q, {
each: (f) => { els.forEach((e, i) => f.call(e, i, e)); return q; },
html: getSet((e) => e.innerHTML, (e, v) => (e.innerHTML = v)),
text: getSet((e) => e.textContent, (e, v) => (e.textContent = v)),
val: getSet((e) => e.value, (e, v) => (e.value = v)),
attr: (n, v) => (v === undefined ? els[0] && els[0].getAttribute(n) : each((e) => e.setAttribute(n, v))),
css: (p, v) => typeof p === "object" ? each((e) => Object.assign(e.style, p)) : v === undefined ? els[0].style[p] : each((e) => (e.style[p] = v)),
addClass: (c) => each((e) => e.classList.add(c)),
removeClass: (c) => each((e) => e.classList.remove(c)),
toggleClass: (c) => each((e) => e.classList.toggle(c)),
on: (ev, f) => each((e) => { e.addEventListener(ev, f); (e.h = e.h || []).push([ev, f]); }),
one: (ev, f) => each((e) => { const g = (x) => { e.removeEventListener(ev, g); f.call(e, x); }; e.addEventListener(ev, g); }),
off: (ev) => each((e) => (e.h || []).filter((x) => x[0] === ev).forEach((x) => e.removeEventListener(ev, x[1]))),
trigger: (ev) => each((e) => e.dispatchEvent(new Event(ev, { bubbles: true }))),
click: (f) => (f ? q.on("click", f) : q.trigger("click")),
ready: (f) => f(),
append: (h) => each((e) => e.insertAdjacentHTML("beforeend", h)),
remove: () => each((e) => e.remove()),
empty: () => each((e) => (e.innerHTML = "")),
parent: () => wrap(els.map((e) => e.parentElement)),
children: () => wrap(els.flatMap((e) => [...e.children])),
find: (sel) => wrap(els.flatMap((e) => [...e.querySelectorAll(sel)])),
next: () => wrap(els.map((e) => e.nextElementSibling).filter(Boolean)),
siblings: () => wrap(els.flatMap((e) => [...e.parentElement.children].filter((x) => x !== e))),
eq: (i) => wrap(els.slice(i, i + 1)),
first: () => q.eq(0),
last: () => wrap(els.slice(-1)),
not: (sel) => wrap(els.filter((e) => !e.matches(sel))),
filter: (sel) => wrap(els.filter((e) => e.matches(sel))),
has: (sel) => wrap(els.filter((e) => e.querySelector(sel))),
hide: () => each((e) => (e.style.display = "none")),
show: () => each((e) => (e.style.display = "")),
});
return q;
}
$.trim = (s) => s.trim();
$.each = (a, f) => a.forEach((v, i) => f(i, v));
console.log("$('#id') ≡ querySelector:", $("#a").els[0] === document.querySelector("#a"));
console.log("$('.lop') ≡ querySelectorAll:", $(".lop").length === document.querySelectorAll(".lop").length);
const el = document.getElementById("a");
$(el).addClass("x");
console.log("addClass ≡ classList:", el.classList.contains("x"));
$(el).on("click", () => console.log("click qua $.on"));
el.addEventListener("click", () => console.log("click qua addEventListener"));
el.click();
// $.get(url) ≡ fetch(url).then(res => res.json())
const fetchGia = () => Promise.resolve({ json: () => Promise.resolve({ ok: true }) });
fetchGia("/api").then((res) => res.json()).then((d) => console.log("fetch + json:", d));
</script>Mở rộng: Chọn công cụ theo hoàn cảnh: nếu bạn bảo trì một hệ thống cũ dùng jQuery hoặc một plugin như DataTables thì dùng jQuery; nếu bạn bắt đầu dự án mới, hãy ưu tiên JavaScript thuần (hoặc các framework hiện đại), vì nó nhẹ hơn (không phải tải thêm thư viện) và là nền tảng chung cho mọi công nghệ về sau.
Đến đây bạn đã gặp cả năm công cụ. Nhóm bài cuối cùng tổng hợp: nhóm bài này đòi hỏi kết hợp hiểu biết về Emmet, bộ chọn và lớp của jQuery để mô phỏng một chuỗi lệnh hoàn chỉnh.
Thực hành theo giáo trình
Giáo trình: Chương 7 — mục "Thực hành": Bài 7.1 (Thực hành với jQuery DataTables) và Bài 7.2 (Thực hành với SASS).
Bài 7.1. Thực hành với jQuery DataTables
Mục tiêu: biến một bảng HTML thông thường thành bảng có tìm kiếm, sắp xếp và phân trang bằng plugin DataTables của jQuery. Các bước theo giáo trình:
- Truy cập
datatables.net. - Nhúng CSS và JS của DataTables, lưu ý phải nhúng jQuery trước:
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css">
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>- Tạo trang HTML có bảng
id="example"vớithead,tbody,tfoot:
<table id="example" class="display" style="width:100%">
<thead>
<tr><th>Name</th><th>Position</th><th>Office</th><th>Age</th><th>Start date</th><th>Salary</th></tr>
</thead>
<tbody>
<tr>
<td>Tiger Nixon</td><td>System Architect</td><td>Edinburgh</td>
<td>61</td><td>2011/04/25</td><td>$320,800</td>
</tr>
</tbody>
<tfoot>
<tr><th>Name</th><th>Position</th><th>Office</th><th>Age</th><th>Start date</th><th>Salary</th></tr>
</tfoot>
</table>- Tạo mã JS (một dòng là đủ):
$(document).ready(function() {
$('#example').DataTable();
});- Để tải dữ liệu từ nguồn trực tuyến, dùng AJAX tích hợp sẵn của DataTables:
$(document).ready(function() {
$('#example').DataTable( {
ajax: {
url: 'https://api.themoviedb.org/3/discover/movie?api_key=YOUR_API_KEY',
dataSrc: 'results'
},
"columns": [
{ "data": "adult" },
{ "data": "backdrop_path" },
{ "data": "genre_ids" },
{ "data": "id" },
{ "data": "original_title" }, { "data": "release_date" }
]
} );
});Cảnh báo:
YOUR_API_KEYlà chỗ bạn điền khóa API của dịch vụ. Khóa viết trong mã JavaScript phía trình duyệt ai cũng xem được; đừng dùng khóa trả phí hoặc khóa có quyền ghi, và đừng đưa khóa thật lên GitHub (xem Bài 7.3).
Vì DataTables cần jQuery và CDN, ví dụ dưới đây là một DataTables thu nhỏ bằng JavaScript thuần, thể hiện ba chức năng cốt lõi: nhấp tiêu đề cột để sắp xếp, ô tìm kiếm để lọc, và dòng thông tin đếm số dòng. Hãy thử nhấp vào "Age" và gõ chữ vào ô tìm kiếm.
<style>
body { font: 13px sans-serif; margin: 6px; }
table { border-collapse: collapse; width: 100%; }
th, td { border-bottom: 1px solid #ddd; padding: 5px 8px; text-align: left; }
th { cursor: pointer; background: #f3f4f6; }
#thongtin { margin-top: 6px; color: #555; }
</style>
Search: <input id="tim" type="search">
<table id="example">
<thead><tr><th data-c="0">Name</th><th data-c="1">Position</th><th data-c="2">Office</th><th data-c="3" data-so="1">Age</th></tr></thead>
<tbody></tbody>
</table>
<div id="thongtin"></div>
<script>
const DATA = [
["Tiger Nixon", "System Architect", "Edinburgh", 61],
["Lê Hà", "Web Developer", "Hà Nội", 29],
["Phạm Minh", "Designer", "Đà Nẵng", 34],
["Nguyễn An", "Web Developer", "Hà Nội", 24],
["Trần Bình", "Tester", "Hồ Chí Minh", 31],
["Vũ Hạnh", "Project Manager", "Hà Nội", 45],
];
let cot = 0, tang = true;
function ve() {
const q = document.getElementById("tim").value.toLowerCase();
const loc = DATA.filter((r) => r.join(" ").toLowerCase().includes(q));
loc.sort((a, b) => (a[cot] > b[cot] ? 1 : a[cot] < b[cot] ? -1 : 0) * (tang ? 1 : -1));
document.querySelector("tbody").innerHTML = loc.map((r) => "<tr>" + r.map((x) => "<td>" + x + "</td>").join("") + "</tr>").join("");
document.getElementById("thongtin").textContent = "Showing " + loc.length + " of " + DATA.length + " entries";
return loc;
}
document.querySelectorAll("th").forEach((th) => th.addEventListener("click", () => {
const c = +th.dataset.c;
tang = c === cot ? !tang : true; cot = c; ve();
}));
document.getElementById("tim").addEventListener("input", ve);
console.log("Ban đầu:", ve().length, "dòng");
document.querySelector("th[data-c='3']").click();
console.log("Sắp xếp theo Age tăng dần:", [...document.querySelectorAll("tbody tr td:nth-child(4)")].map((td) => td.textContent).join(", "));
document.getElementById("tim").value = "hà nội";
console.log("Tìm 'hà nội':", ve().length, "dòng");
</script>Bài 7.2. Thực hành với SASS
Mục tiêu: viết một tệp SCSS dùng biến, mixin, lồng quy tắc và hàm màu, rồi biên dịch ra CSS. Các bước:
Bước 1. Cài đặt SASS qua npm (Node Package Manager); nếu chưa có npm thì cài Node.js trước:
npm install -g sassBước 2. Tạo tệp SASS styles.scss. (Các tiền tố -webkit-, -moz-, -ms- dưới đây chỉ minh họa cho mixin; với border-radius, mọi trình duyệt hiện nay đều hỗ trợ cú pháp chuẩn nên không cần tiền tố.)
@use "sass:color"; // Nạp mô-đun màu của SASS
$primary-color: #3498db;
$secondary-color: #2ecc71;
// Định nghĩa mixin
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
border-radius: $radius;
}
// Định nghĩa selector và sử dụng biến, mixin
.card {
background-color: $primary-color;
padding: 20px;
margin: 10px;
@include border-radius(5px);
h2 {
color: $secondary-color;
margin-bottom: 10px;
}
p {
font-size: 14px;
line-height: 1.5;
}
a {
text-decoration: none;
color: color.adjust($primary-color, $lightness: -10%);
&:hover {
color: color.adjust($primary-color, $lightness: 10%);
}
}
}Bước 3. Biên dịch SASS thành CSS:
sass styles.scss styles.cssBước 4. Kết quả. Tệp styles.css được sinh ra có nội dung tương tự:
.card {
background-color: #3498db;
padding: 20px;
margin: 10px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
border-radius: 5px;
}
.card h2 {
color: #2ecc71;
margin-bottom: 10px;
}
.card p {
font-size: 14px;
line-height: 1.5;
}
.card a {
text-decoration: none;
color: #217dbb;
}
.card a:hover {
color: #5faee3;
}Điều bất ngờ nhất là hai màu #217dbb và #5faee3: bạn chưa hề viết chúng. Hàm color.adjust($primary-color, $lightness: -10%) lấy màu #3498db, giảm độ sáng (lightness) trong hệ HSL đi 10 điểm phần trăm rồi đổi lại thành mã hex; với +10% là sáng hơn. Ví dụ JavaScript sau tự tính lại để kiểm chứng:
function hexToHsl(hex) {
const n = parseInt(hex.slice(1), 16);
const r = ((n >> 16) & 255) / 255, g = ((n >> 8) & 255) / 255, b = (n & 255) / 255;
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
let h = 0, s = 0;
const l = (mx + mn) / 2;
if (d) {
s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
h *= 60;
}
return [h, s * 100, l * 100];
}
function hslToHex(h, s, l) {
s /= 100; l /= 100;
const k = (n) => (n + h / 30) % 12, a = s * Math.min(l, 1 - l);
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return "#" + [f(0), f(8), f(4)].map((x) => Math.round(x * 255).toString(16).padStart(2, "0")).join("");
}
const adjust = (hex, dl) => { const [h, s, l] = hexToHsl(hex); return hslToHex(h, s, l + dl); };
const [h, s, l] = hexToHsl("#3498db");
console.log("#3498db trong HSL:", Math.round(h), "độ,", Math.round(s) + "%,", l.toFixed(1) + "%");
console.log("adjust lightness -10% →", adjust("#3498db", -10));
console.log("adjust lightness +10% →", adjust("#3498db", 10));Cuối cùng, xem thẻ .card dùng CSS đã biên dịch (di chuột lên liên kết để thấy màu a:hover):
<style>
.card { background-color: #3498db; padding: 20px; margin: 10px; border-radius: 5px; }
.card h2 { color: #2ecc71; margin-bottom: 10px; }
.card p { font-size: 14px; line-height: 1.5; }
.card a { text-decoration: none; color: #217dbb; }
.card a:hover { color: #5faee3; }
</style>
<div class="card">
<h2>Thẻ SASS</h2>
<p>CSS này được sinh ra từ <a href="#">styles.scss</a></p>
</div>
<script>
const a = getComputedStyle(document.querySelector(".card a"));
console.log("Màu liên kết (đã biên dịch):", a.color);
</script>Mẹo: Khi đã quen, hãy chạy
sass --watch styles.scss:styles.cssđể tự biên dịch lại mỗi lần lưu tệp.
Câu hỏi ôn tập
Giáo trình: Chương 7 — mục "Câu hỏi ôn tập lý thuyết" (câu 7.1 – 7.13; đáp án đối chiếu Phụ lục).
7.1. Emmet có sẵn cho các trình soạn thảo mã nguồn nào? A. Sublime Text, Photoshop và Sketch · B. Visual Studio, Illustrator và Figma · C. Sublime Text, Visual Studio Code và WebStorm · D. Tất cả các phương án đều đúng.
Đáp án: C. Photoshop, Sketch, Illustrator, Figma là phần mềm thiết kế đồ họa, không phải trình soạn thảo mã tích hợp Emmet.
7.2. Lệnh Git nào dùng để khởi tạo một kho lưu trữ mới?
A. git clone · B. git init · C. git add · D. git commit.
Đáp án: B. git clone chỉ tải về một kho đã có.
7.3. Lệnh Git nào dùng để kiểm tra trạng thái?
A. git add · B. git status · C. git commit · D. git push.
Đáp án: B (git status).
7.4. Lệnh Git nào dùng để đưa tệp hoặc thư mục mới vào vùng chờ commit?
A. git add · B. git clone · C. git push · D. git pull.
Đáp án: A (git add đưa vào vùng chờ / staging area).
7.5. Lệnh Git nào dùng để gộp hai nhánh lại với nhau?
A. git branch · B. git checkout · C. git merge · D. git pull.
Đáp án: C (git merge). git branch tạo nhánh, git checkout chuyển nhánh.
7.6. Đoạn mã nào được dùng để tạo một bảng với Bootstrap?
A. <table class="table"> · B. <div class="container"></div> · C. <img src="image.jpg" alt="Image"> · D. <button class="btn btn-primary">Button</button>.
Đáp án: A. Lớp table của Bootstrap áp kiểu cho thẻ <table>.
7.7. Lệnh jQuery nào dùng để chờ tài liệu HTML tải xong trước khi thực hiện các tác vụ?
A. $(document).ready() · B. $(window).load() · C. $(document).load() · D. $(window).ready().
Đáp án: A.
7.8. Lệnh jQuery nào dùng để chọn tất cả các phần tử <div>?
A. $("div") · B. $("#div") · C. $(".div") · D. "$div".
Đáp án: A. #div chọn theo id, .div chọn theo lớp.
7.9. Phương thức jQuery nào dùng để lấy giá trị của một thuộc tính?
A. .attr() · B. .val() · C. .text() · D. .html().
Đáp án: A (.attr()).
7.10. Phương thức jQuery nào dùng để thay thế toàn bộ mã HTML bên trong một phần tử?
A. .text() · B. .html() · C. .val() · D. .append().
Đáp án: B (.html(chuỗi)). .append() chỉ thêm vào cuối, không thay thế.
7.11. SASS là viết tắt của từ gì? A. Scalable Application Style Sheets · B. Syntactically Awesome Style Sheets · C. Simple Accessible Style Sheets · D. Structured Application Style Sheets.
Đáp án: B.
7.12. Để khai báo một mixin trong SASS, ta dùng từ khóa nào?
A. @import · B. @include · C. @extend · D. @mixin.
Đáp án: D (@mixin). @include là để gọi mixin đã khai báo.
7.13. Phương thức jQuery nào dùng để nhân bản một phần tử?
A. .clone() · B. .copy() · C. .duplicate() · D. .repeat().
Đáp án: A (.clone()).
Bài tập tự thực hành theo giáo trình
Giáo trình: Chương 7 — mục "Bài tập tự thực hành" (Bài 7.3 và 7.4).
Bài 7.3. Thực hành với GitHub. Truy cập github.com và thực hiện: (1) tạo tài khoản để quản lý và lưu trữ mã nguồn trên các kho; (2) đưa mã nguồn đã làm ở các phần trước lên GitHub (dùng chuỗi lệnh git init đến git push ở Phần 2); (3) thực hành các câu lệnh Git cơ bản (status, log --oneline, branch, merge, pull).
Bài 7.4. Thực hành với Bootstrap và jQuery. Triển khai thêm Bootstrap và jQuery vào các bài tập tự thực hành ở chương trước (như Website giới thiệu sản phẩm ở Chương 6): dùng lưới col-* cho danh sách sản phẩm, một thành phần Bootstrap (navbar hoặc dropdown), và một hiệu ứng jQuery (fadeToggle, slideToggle...) cho phần mô tả.
Lưu ý: Hai bài này cần tài khoản, dòng lệnh và mạng thật nên được thực hiện trên máy cá nhân, không có ô chạy trong trang này.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
| Gõ Emmet nhưng không nhấn Tab/Enter, hoặc tệp chưa đặt đúng loại HTML/CSS | Cụm viết tắt không được khai triển | Kiểm tra ngôn ngữ của tệp rồi nhấn Tab hoặc Enter |
Chạy git commit mà quên git add |
Thay đổi chưa vào vùng chờ nên không có trong commit | git status để kiểm tra, rồi git add trước khi commit |
Đưa .env, mật khẩu, khóa API lên GitHub |
Lộ bí mật, kể cả khi sau này xóa tệp | Dùng .gitignore; nếu lỡ lộ thì đổi khóa ngay |
Nhúng tệp .scss vào HTML (<link href="style.scss">) |
Trình duyệt không hiểu, mất kiểu | Biên dịch ra style.css rồi nhúng tệp CSS |
| Lồng quá sâu trong SASS | CSS sinh ra dài, khó ghi đè | Lồng tối đa 3 – 4 cấp, dùng lớp rõ nghĩa |
Viết .col-md-4 cho khổ lớn nhưng không có lớp cho khổ nhỏ |
Trên điện thoại cột chiếm trọn hàng, bố cục khác ý định | Thiết kế mobile-first: viết col-12 hoặc col-sm-* trước |
| Cột trong một hàng cộng quá 12 | Cột thừa tự xuống dòng | Tính tổng cột mỗi hàng, hoặc cố ý để xuống dòng |
Nhúng jQuery sau mã dùng $, hoặc dùng bản slim rồi gọi $.ajax |
Lỗi $ is not defined hoặc $.ajax is not a function |
Nhúng jQuery trước mã của bạn; dùng bản đầy đủ |
| Chạy mã jQuery trước khi trang sẵn sàng | Không chọn được phần tử, mã không có tác dụng | Bọc trong $(function () { ... }) |
.append("<li>" + dữ liệu người dùng + "</li>") |
Nguy cơ XSS | Dùng .text() cho văn bản; kiểm tra dữ liệu |
| Dùng jQuery chỉ để làm một việc nhỏ trong dự án mới | Tải thêm thư viện không cần thiết | Dùng JavaScript thuần (querySelector, classList, fetch) |
Tóm tắt
- Emmet biến cụm viết tắt thành HTML/CSS:
!sinh khung trang;tag*n,>,#id,.class,[attr],{text},$là các toán tử chính. Có sẵn trong VS Code, Sublime Text, WebStorm. - Git là hệ thống kiểm soát mã nguồn phân tán; GitHub lưu kho trên mạng; GitHub Desktop là giao diện đồ họa. Các lệnh:
init,clone,status,add,commit,log,push,pull,branch,checkout/switch,merge. - SASS (Syntactically Awesome Style Sheets) mở rộng CSS với biến (
$), lồng quy tắc, mixin (@mixin,@include); biên dịch bằngsassra CSS.@importđã ngừng phát triển, dùng@use,@forward. - Bootstrap gồm Layout, Content, Components, Utilities; lưới 12 cột với
container,row,col-*,col-sm|md|lg|xl|xxl-*theo nguyên tắc mobile-first; từ phiên bản 5 không cần jQuery. - jQuery ("write less, do more"):
$(document).ready(), bộ chọn kiểu CSS,.html() .text() .attr() .val(), duyệt DOM (parent,children,find,next),css/addClass, sự kiện (on,one,off,trigger), hiệu ứng (fadeIn,slideUp, callback), AJAX ($.get,$.ajax); ngày nay dự án mới thường dùng JavaScript thuần.