Series Web app không dùng framework: Bài 1 Pure JS client-side rendering
Nội dung
- Tại sao không dùng framework
- Rendering
- 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:
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.
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:
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:
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.
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:
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
Để 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.
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:
- Bind component và DOM node ta đã declare
- Traverse DOM tree
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ó:
Để 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.
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
count.js
filter.js
*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
Trong file render.js ta tạo một renderer object và function add để gọi wrap với các component function
Cuối cùng file render.js sẽ như sau
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
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.
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
Cuối cùng ta sẽ thay thế virtual DOM với DOM cũ trong index.js và fake dữ liệu dynamic
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.
Tham khảo: https://github.com/Apress/frameworkless-front-end-development
All rights reserved