- Writing JavaScript Views the Hard Way: Bài viết giải thích cách xây dựng view chỉ bằng JavaScript thuần, không dùng framework
- Thông qua cách tiếp cận mệnh lệnh trực tiếp, có thể đạt được hiệu năng, khả năng bảo trì và tính di động
- Tách bạch rõ ràng giữa cập nhật state và cập nhật DOM, đồng thời tuân theo quy ước đặt tên nghiêm ngặt và các mẫu cấu trúc theo từng vai trò
- Cách làm này dễ debug, đảm bảo tương thích với mọi trình duyệt, và có ưu điểm lớn là 0 dependencies
- Có thể khó với người mới bắt đầu, nhưng khi học sẽ mang lại hiểu biết sâu sắc về cách hệ thống thực sự vận hành
Viết JavaScript view theo 'Hard Way'
Đây là gì?
- Đây là mẫu xây dựng view chỉ bằng JavaScript mà không cần framework như React, Vue, lit-html
- Đây là chính mẫu lập trình, không phải một thư viện hay công cụ cụ thể, giúp tránh vấn đề mã spaghetti
- Bằng cách dùng phương pháp mệnh lệnh trực tiếp, nó giảm bớt trừu tượng và tăng tính trực quan
Ưu điểm so với framework
- Hiệu năng: Nhờ mã mệnh lệnh nên hoạt động không có tính toán dư thừa, phù hợp cho cả hot path lẫn cold path
- 0 dependencies: Không bị ràng buộc bởi nâng cấp thư viện hay vấn đề tương thích
- Tính di động: Mã đã viết có thể chuyển sang bất kỳ framework nào
- Khả năng bảo trì: Cấu trúc phân khu rõ ràng và quy ước đặt tên giúp dễ xác định vị trí mã
- Hỗ trợ trình duyệt: Tương thích với hầu hết trình duyệt từ IE9 trở lên, và thậm chí có thể hỗ trợ đến IE6 với một số chỉnh sửa
- Dễ debug: Cung cấp stack trace nông mà không có lớp trung gian
- Cấu trúc hàm: Không phải bất biến, nhưng mọi thành phần đều được tổ chức dựa trên hàm
Giải thích cấu trúc
Cấu trúc tổng thể
- Gồm
template → clone() → hàm init()
- Hàm
init() tạo ra một instance view duy nhất, bao gồm biến state, tham chiếu DOM, hàm cập nhật, event listener, v.v.
Ví dụ cấu trúc mã (Hello World)
const template = document.createElement('template');
template.innerHTML = `<div>Hello <span id="name">world</span>!</div>`;
function clone() {
return document.importNode(template.content, true);
}
function init() {
let frag = clone();
let nameNode = frag.querySelector('#name');
let name;
function setNameNode(value) {
nameNode.textContent = value;
}
function setName(value) {
if(name !== value) {
name = value;
setNameNode(value);
}
}
function update(data = {}) {
if(data.name) setName(data.name);
return frag;
}
return update;
}
Cấu thành bên trong hàm init()
1. Biến DOM
frag là mảnh template được tạo từ clone()
- Các phần tử bên trong được tham chiếu bằng
querySelector(), và tên biến dùng dạng fooNode
2. DOM view
- Phần bao gồm các view khác (sub-view có thể tái sử dụng)
- Ví dụ:
let updateChildView = childView();
- Hàm cập nhật view được đặt tên theo dạng
updateFoo
3. Biến state
- Các giá trị dữ liệu có thể thay đổi bên trong view
- Để cập nhật DOM hiệu quả, so sánh với giá trị hiện tại và chỉ thay đổi DOM khi cần
4. Hàm cập nhật DOM
- Dùng khi thay đổi trạng thái của phần tử DOM
- Ví dụ:
function setNameNode(value) {
nameNode.textContent = value;
}
- Việc thao tác DOM bắt buộc chỉ được thực hiện bên trong các hàm này
5. Hàm cập nhật state
- Bao gồm logic thay đổi state và phản ánh nó vào DOM
- Bỏ qua giá trị không thay đổi để tránh các thay đổi DOM không cần thiết
- Ví dụ:
function setName(value) {
if(name !== value) {
name = value;
setNameNode(value);
}
}
Hàm template và clone()
template
- Tạo cấu trúc HTML tĩnh bằng phần tử
<template>
- Không được chèn trực tiếp vào DOM mà tạo bản sao thông qua clone
clone()
- Sao chép bằng
document.importNode(template.content, true)
- Khi cần có thể dùng
.firstElementChild để trả về phần tử gốc
Cách tương tác
Luồng dữ liệu cha → con
- Thành phần cha gọi
init() của thành phần con để lấy hàm cập nhật, rồi gọi theo dạng update({ name: 'foo' })
Lan truyền dữ liệu dựa trên sự kiện
- Về cơ bản tuân theo mô hình props down, events up
- View cấp dưới giao tiếp bằng cách dispatch sự kiện lên cấp trên
So sánh với React
constructor() (React) → init() (Hard Way)
- Phụ trách thiết lập ban đầu của component
render() (React) → update(data) (Hard Way)
- Đảm nhiệm vai trò làm mới màn hình và cập nhật UI
this.setState() (React) → setX(value) (Hard Way)
- Được thay bằng cách thiết lập trực tiếp giá trị state
props (React) → giá trị truyền qua update(data) (Hard Way)
- Cách xử lý dữ liệu được truyền từ component cha
- JSX / Virtual DOM (React) → HTML template + DOM API (Hard Way)
- Dùng thao tác DOM thủ công và template thay cho UI khai báo
Kết luận
- Cách làm này có rào cản ban đầu cao hơn so với các framework quen thuộc, nhưng có các điểm mạnh sau:
- Tối ưu hiệu năng
- Quyền kiểm soát hoàn toàn
- Hiểu sâu hơn thông qua quá trình học
- Bằng cách tách hàm theo từng vai trò và áp dụng quy ước đặt tên, vẫn có thể xây dựng UI dễ bảo trì mà không cần framework
Tương thích
- Các ví dụ mới nhất dùng API cho trình duyệt hiện đại, nhưng vẫn có thể hỗ trợ đến IE9 hoặc thấp hơn bằng các thay thế dựa trên hàm
- Cũng có thể mở rộng đến IE6 bằng cách dùng truyền hàm qua props thay cho sự kiện
3 bình luận
Cuối cùng thì vẫn là web component..
Xin chúc mừng. Một framework js nữa đã ra đời.
Ý kiến trên Hacker News
Với nhiều nhà phát triển JS, điều này có thể bị xem là tà đạo, nhưng tôi cho rằng biến
statelà một anti-patternvalue/textContent/checkedcủa phần tử DOM làm nguồn chân lý duy nhấtTài liệu nói cách tiếp cận này rất dễ bảo trì, nhưng tôi không đồng ý
Gần đây tôi đang viết ứng dụng bằng TypeScript “vanilla” thuần với vite, và ngày càng nghi ngờ các thực hành “tốt nhất” ở frontend
Cách làm này gợi tôi nhớ đến thư viện backbone js cũ
Gần đây tôi cũng nghĩ ra thứ gì đó tương tự, nhưng không dùng phần tử template
innerHTMLcủa phần tử có sẵn hoặc tạo một phần tửdivmới để chèn vàoĐoạn code này trông giống hệt kiểu code cập nhật thủ công mà các thư viện view phản ứng muốn thay thế
Tôi đã lập trình gần 20 năm nhưng vẫn không quen được với các framework frontend
Tôi dùng một helper tương tự
React.createElementTôi đang làm tại deja-vu.junglecoder.com như một nỗ lực xây dựng bộ công cụ JS cho các công cụ dựa trên HTML
Ở công việc chính thức đầu tiên sau khi tốt nghiệp đại học, tôi từng làm bản web của phần mềm Delphi