10 điểm bởi GN⁺ 2025-04-23 | 3 bình luận | Chia sẻ qua WhatsApp
  • 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 templateclone() → 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 templateclone()

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

 
wfedev 2025-04-24

Cuối cùng thì vẫn là web component..

 
ahwjdekf 2025-04-23

Xin chúc mừng. Một framework js nữa đã ra đời.

 
GN⁺ 2025-04-23
Ý 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 state là một anti-pattern

    • Khi dùng web components, thay vì thêm biến state cho các kiểu biến “phẳng”, tôi dùng value/textContent/checked của phần tử DOM làm nguồn chân lý duy nhất
    • Khi cần thì thêm setter và getter
    • Dù lượng code ít hơn, vẫn có nhiều thứ tự nhiên hoạt động đúng
    • Dùng WebComponents giúp tách HTML template nằm gần object, tạo ra sự phân mảnh kiểu fusilli hay macaroni thay vì spaghetti code
  • Tà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 ý

    • Design pattern này chỉ dựa vào quy ước
    • Khi nhiều lập trình viên cùng làm trên một ứng dụng phức tạp, gần như chắc chắn sẽ có ít nhất một người lệch khỏi quy ước
    • Các framework UI hướng lớp như UIKit trên iOS buộc mọi lập trình viên dùng cùng một bộ API tiêu chuẩn, khiến code dễ đoán hơn và dễ bảo trì hơn
  • 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

    • Chưa thể kết luận về khả năng mở rộng, nhưng về hiệu năng thì có lợi thế lớn
    • Nó vui, giúp học được nhiều thứ, debug đơn giản và dễ hiểu kiến trúc
    • Điều tôi nhớ nhất là templating
  • Cách làm này gợi tôi nhớ đến thư viện backbone js cũ

    • Cũng có một kho GitHub chứa ví dụ về mẫu MVC được điều chỉnh cho nền tảng web
  • Gần đây tôi cũng nghĩ ra thứ gì đó tương tự, nhưng không dùng phần tử template

    • Tôi dùng hàm và template literal để trả về chuỗi, rồi đưa vào innerHTML của phần tử có sẵn hoặc tạo một phần tử div mới để chèn vào
    • Các hàm bị lồng nhau nên khó tổ chức theo cách hợp lý
  • Đ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 mạnh hơn ở backend nên cho rằng các tương tác liên quan đến bảo mật nên đi qua server
    • Tôi xem JS là thứ bổ sung chức năng phía client trên nền HTML và CSS vững chắc
  • Tôi dùng một helper tương tự React.createElement

    • Có ví dụ hoạt động của một dashboard mock server
  • Tô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

    • Tôi vẫn chưa giải quyết được reactive/data binding hai chiều cho đúng nghĩa, nhưng grab/patch khá ổn
    • Cách dùng template khiến việc di chuyển các phần của template trở nên rất dễ dàng
  • Ở 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

    • Khi đó nhóm đã viết lại frontend lần thứ ba và phải đổi framework
    • Tôi từng lập luận rằng nên tự viết framework riêng, nhưng cả nhóm không thích đề xuất của tôi
    • Sau đó tôi nhận được đề nghị tốt hơn từ công ty khác nên rời đi
    • Về sau tôi thử thêm một framework khác tên là tiny.js và hiện vẫn dùng nó cho các dự án cá nhân