+6

Series Web app không dùng framework: Bài 1 Pure JS client-side rendering

Nội dung

  1. Tại sao không dùng framework
  2. Rendering
  3. Rendering with component

Tại sao không dùng framework?

TLDR:

  • Không dùng framework để hiểu công dụng của framework
  • Hiểu rõ trade-offs trong quyết định sử dụng framework
  • Tối ưu hoá nợ kỹ thuật (technical debt

Frameworks là những công cụ cực kỳ cần thiết trong bất kỳ lĩnh vực nào không riêng frontend nói riêng và phát triển phần mềm nói chung.

Tuy nhiên, thông thường khi sử dụng một framework thì trước tiên ta cần phải hiểu rõ framework được tạo ra để giải quyết vấn đề gì thì những nguyên lý được sử dụng trong framework mới thật sự có ý nghĩa.

Nếu hiểu rõ vấn đề chính mà mọi framework muốn giải quyết, bạn sẽ có nền tảng để đánh giá những lợi ích và hạn chế mà từng framework mang lại.

Trong series này, mình sẽ đi qua một trường hợp điển hình trong phát triển ứng dụng web frontend để có thể làm rõ những vấn đề chính mà các framework như React, Angular, Svelte giải quyết.

Rendering

Trước tiên, ta sẽ bắt đầu với một trường hợp ứng dụng cụ thể và phát triển những chức năng thường thấy trong framework.

Trong bài viết này mình sẽ sử dụng trường hợp một ứng dụng ghi chú todo. Cấu trúc thư mục và html như sau:

Screenshot 2024-05-24 at 13.43.45.png

Chức năng đầu tiên mà mọi ứng dụng GUI (graphic user interface) làm là rendering. Rendering là một function nhận state và trả về view. State là dữ liêu của các component trong ứng dụng (vd: todos, filter)

View = Render(state)

Mọi ứng dụng web được render bởi trình duyệt web. Dữ liệu trình duyệt dùng để thể hiện nội dung là HTML, được represent bằng DOM. Để thay đổi nội dung của front end, ta thay đổi nội dung của HTML và giao tiếp với rendering interface của browser.

Screenshot 2024-05-22 at 20.43.34.png

Tiếp đến là ta sẽ xây dựng chức năng render trong file render.js

Component đơn giản nhất trong ứng dụng của chúng ta là todo, cấu trúc của todo có thể như sau:

Screenshot 2024-05-22 at 18.52.53.png

Component todo sẽ tiêu thụ state và trả về một html để gán vào index.html

Component tiếp theo là count của todo:

Screenshot 2024-05-22 at 18.56.50.png

Cuối cùng, function render sẽ copy DOM hiện tại từ root node của ứng dụng (sau đây gọi là DOM ảo), update những giá trị mới trong state vào DOM ảo và trả về DOM ảo.

Screenshot 2024-05-24 at 13.53.27.png

Sau đó render của ta sẽ có nhiệm vụ giao tiếp thay đổi trong state với API render của browser.

Controller sẽ thay nhận state mới, pass state cho render, và cuối cùng yêu cầu trình duyệt update DOM cũ theo những thay đổi trong DOM ảo. Đây là nội dung của file index.js:

Screenshot 2024-05-24 at 13.54.49.png

requestAnimationFrame gọi callback render và dùng replaceWith để thay đổi nội dung html, tất cả quá trình này sẽ xảy ra trước bước repaint tiếp theo trong các bước render của browser.

Cuối cùng đây là todo app của chúng ta với render đã được implement:

Render với components

Hiện tại, chức năng render của ứng dụng của chúng ta đã hoàn thành. Tuy nhiên, chức năng render còn nhiều hạn chế:

  • Tight coupling giữa logic sử lý dữ liệu trong mỗi thành phần của giao diện và logic của component
  • Khó quản lý các thành phần giao diện vì mọi thứ được xử lý chung trong cùng một function

Screenshot 2024-05-23 at 15.55.34.png

Để có trải nghiệm tốt hơn, ta cần phải tìm giải pháp giúp sử dụng các component một cách declarative.

Screenshot 2024-05-23 at 15.43.54.png

Ta cần tách biệt logic của render với quá trình import và sử dụng các component.

Để làm được điều đó ta có thể abstract chức năng của render đang bị coupled với component.

Các chức năng hiện tại của render gồm:

  • Xác định DOM của các component khác nhau trong giao diện
  • Pass state mới của giao diện vào function tạo DOM ảo của từng component

Để tách bạch chức năng trên, ta có thể sử dụng một class component để các component sau inherit chức đăng xác định DOM.

Tuy nhiên, ta có thể đi theo cách sử dụng pure functions của React.

Vì mô hình hiện tại của chúng ta thì view = render(state), ta có thể tiếp tục sử dụng function này cho các component vì các component cũng chỉ là một "immediate step" trong quá trình traversal DOM tree của render.

Để giữ các component là pure functions, ta mở rộng chức năng mỗi component với một higher order function khác là componentWrapper.

Function componentWrapper sẽ có các chức năng ta cần decouple khỏi component:

Component binding

Ta sẽ abstract chức năng đầu tiên. Ta tạo function bind các component khác nhau với các DOM của nó:

Screenshot 2024-05-23 at 11.09.21.png

Để componentWrapper có thể nhận diện được component nào bind với DOM node nào thì ta cần key value như dictionary để có thể liên kết chúng với nhau.

Ta có thể dùng class hoặc id của DOM node làm key nhưng điều này có thể gây ra nhập nhằng khi ta bắt đầu phải làm việc với các bước sau như layout.

Với HTML ta có thể dùng data attributes để định dạng attribute riêng cho components.

screenshotHTML.png

Giờ ta có thể sử dụng .dataset.component để xác định tên component trong html.

Tiếp đến ta sẽ tách các component function thành những file riêng:

todos.js

Screenshot 2024-05-24 at 14.11.23.png

count.js

Screenshot 2024-05-24 at 14.12.18.png

filter.js

Screenshot 2024-05-24 at 14.12.37.png

*Lưu ý là mỗi component function đều trả về lại một DOM node ảo đã được clone với state mới

Sau đó ta sẽ liên kết từng component với data attribute trong html trong file index.js

Screenshot 2024-05-23 at 15.10.55.png

Trong file render.js ta tạo một renderer object và function add để gọi wrap với các component function

Screenshot 2024-05-23 at 15.12.07.png

Cuối cùng file render.js sẽ như sau

Screenshot 2024-05-23 at 15.15.00.png

Hiện tại ta đã bind logic của từng component. Ta có thể declare component mà không cần phải tìm DOM node.

Passing state

Tiếp theo ta phải tạo virtual DOM từ root và tạo logic phân tán state từ root cho các component con.

Ta thêm function renderRoot

Screenshot 2024-05-23 at 15.20.40.png

rootComponent chỉ có nhiệm vụ clone root và pass state cho các component con. Lưu ý rằng từ lúc này bất kỳ operation nào sau đó được thực hiện ở root node đã được clone (virtual DOM)

Sau đó, ta chỉ cần sửa lại componentWrapper để nó gọi lại componentWrapper với các childComponents trong component đã được wrap.

Screenshot 2024-05-24 at 14.17.41.png

boundedComponent sẽ tìm các DOM node có data-component, gọi component function cùng tên, và thay thế child node trong virtual DOM.

Có những thuật toán khác để viết function này hiệu năng hơn (ví dụ như của React), nhưng nội dung đó nằm ngoài phạm vi của bài viết hôm nay. Mình sẽ bàn về vấn đề này trong những bài sau.

Cuối cùng file render.js đã hoàn thành

Screenshot 2024-05-23 at 15.33.09.png

Cuối cùng ta sẽ thay thế virtual DOM với DOM cũ trong index.js và fake dữ liệu dynamic

Screenshot 2024-05-24 at 14.21.01.png

Giờ ta có thể tạo một ứng dụng front-end cơ bản một cách declaratively, và đây là ứng dụng hoàn chỉnh

Tổng kết

Nếu bạn đã từng sử dụng React hoặc các framework declarative, các bạn có thể thấy sự tương đồng trong cách viết và sử dụng các component, virtual DOM, và cách state được pass.

Trong phần tiếp theo mình sẽ tiếp tục bàn về diffing engine, event handing, routing và implement các chức năng này dùng Javascript để ứng dụng front-end của chúng ta trở nên reactive.

Mình viết để bồi dưỡng kiến thức nên nếu có chỗ nào sai hoặc có thể cải thiện mong mọi người thông cảm và giúp mình sửa sai do kiến thức mình chưa tốt.

Cảm ơn các bạn đã đọc bài viết rất dài và khá phức tạp. !

Tham khảo: https://github.com/Apress/frameworkless-front-end-development


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí