Như vậy là chúng ta lại bắt đầu thêm một mini project
trong hành trình tự học code để có thể tự xây dựng nên những phần mềm thiết yếu và cũng là để tự tìm hiểu dạng thức tư duy của chính mình và rèn luyện khả năng sắp xếp logic.
Về việc trì hoãn đăng bài viết mới trong thời gian dài khi Sub-Series chưa kết thúc thì mình thực sự rất xin lỗi nếu bạn là người đang theo dõi các Series bài viết của mình tại mạng blog Viblo này. Lý do là vì mình muốn dành thời gian hoàn thiện cái mini project
này trước và định hình một vài giới hạn về mặt triển khai code trước khi chia sẻ tại các bài viết ở đây.
Câu chuyện là nếu như bạn đã đồng hành cùng chặng đường tự học bắt đầu từ Series Tự Học Lập Trình Web đầu tiên thì đây đã là mini project
thứ ba rồi; Và mình chắc chắn là chúng ta đều đã quen thuộc với thao tác sử dụng Google Search và Translate để tìm kiếm những câu trả lời cho các vấn đề liên quan khi đọc tham khảo code ví dụ.
Chính vì vậy nên kể từ Sub-Series này trở đi thì mình sẽ không đăng tải code chi tiết trong từng bài viết, mà thay vào đó thì toàn bộ source code
đã hoàn thiện sẽ được gắn ở bài viết mở đầu của mỗi mini project
. Sau đó thì các bài viết sẽ nhằm mục đích chia sẻ lại góc nhìn thiết kế tổng quan và một số thao tác hay dạng thức đáng lưu ý trong quá trình viết code triển khai. Như vậy bạn sẽ có thể dễ dàng đọc lướt qua và tham khảo những điểm mà bạn có thể sẽ cảm thấy cần thiết để ghi chú và không phải tốn thời gian tự Google Search và áp dụng vào kiến trúc code riêng của bạn.
Và đây là liên kết tới trang blog cá nhân tại GitHub được viết bằng Elm, một ứng dụng trang đơn SPA đơn giản sử dụng GitHub Page làm hậu phần back-end
xử lý yêu cầu truy vấn dữ liệu thuần túy.
- GitHub Page: https://thinhtranhnvn.github.io/
- Source code: Simplicity SPA Elm
Data Service
Mục tiêu xây dựng của chúng ta là một ứng dụng trang đơn SPA và như vậy hiển nhiên logic hoạt động chính của trang web sẽ được gom cả vào code JavaScript
ở mặt tiền của trang web. Code này sẽ được vận hành bởi trình duyệt web trên máy tính của người dùng để điều khiển toàn bộ logic điều hướng xử lý sau mỗi thao tác sự kiện tương tác của người dùng.
Ví dụ: khi người dùng chọn mở xem một liên kết bất kỳ trong trang web, code này cần phải tạm thời chặn cách xử lý mặc định của trình duyệt web để ngăn việc tải lại toàn bộ trang web. Ngay sau đó thì SPA cần phải thực hiện phân tích đường dẫn của liên kết mới mà người dùng vừa chọn để ra quyết định điều hướng hiển thị bố cục trang đơn phù hợp. Nếu đó là một yêu cầu xem một bài viết thì SPA sẽ cần phải xác định các yếu tố định vị dữ liệu của bài viết đó trong thư viện đang lưu tại GitHub, bao gồm:
topic-id
- bài viết thuộc chủ đề nào?series-id
- bài viết thuộcSeries
nào trong chủ đề trên?post-slug
- đoạn mô tả tên định danh củaPost
đó trong thư mục củaSeries
trên?
Và đó là cách mà mình đã định hình cơ sở dữ liệu đơn giản cho trang blog cá nhân tại GitHub. Sơi đồ cây thư mục dưới đây là cấu trúc của thư mục /data
chứa dữ liệu của blog mà mình đang sử dụng với nội dung của các bài viết được lưu ở dạng code markdown
trong các tệp .txt
, còn các dữ liệu metadata
được đặt trong các tệp .json
.
/data
├── topic-list.json
|
├── origin [topic]
│ ├── overview.txt
│ ├── series-list.json
| |
│ ├── hrdaya-sutra [series]
│ | ├── post-list.json
│ | ├── 00-the-first-preface.txt
│ | ├── 01-the-second-preface.txt
│ | ├── ...
│ | └── 12-no-five-skandhas-no-object-until-we-come-to-no-realm.txt
│ └── yoga-sutra [series]
│ ├── post-list.json
│ └── ...
|
├── linux [topic]
│ ├── overview.txt
│ ├── series-list.json
| |
│ ├── gnome [series]
│ | ├── post-list.json
│ | └── ...
│ └── libreoffice [series]
│ ├── post-list.json
│ └── ...
|
└── image
Như vậy dạng liên kết có thể được sử dụng cho logic điều hướng của SPA sẽ có dạng thức đơn giản nhất là:
https://username.github.io/ topic-id / series-id / post-slug
Và như vậy, sau khi tách lấy các yếu tố định vị tệp dữ liệu bài viết nói trên thì SPA sẽ có thể gửi yêu cầu tới GitHub để truy vấn nội dung tệp nguyên gốc raw
với liên kết có dạng như sau:
https://raw.githubusercontent.com/username/username.github.io/main/data/ topic-id / series-id / post-slug.txt
Ví dụ:
Ở đây, khi người dùng chọn dừng lại ở trang đơn giới thiệu tổng quan Topic
thì SPA sẽ hiện nội dung của một bài viết overview.txt
của Topic
đó và như vậy bố cục sẽ giống với trang bài viết. Còn nếu người dùng chọn dừng lại ở trang đơn xem tổng quan của một Series
thì SPA sẽ hiển thị bố cục hơi khác một chút với phần nội dung chính được thay bằng một khối liên kết đề mục bao gồm danh sách liên kết trỏ tới các bài viết bên trong Topic
đó.
Các tệp khởi chạy
Toàn bộ source code
được đặt trong thư mục /src
ở cùng cấp đầu tiên với thư mục /data
và khởi đầu với tệp src/App.elm
. Khi chạy lệnh biên dịch elm make src/App.elm
, chúng ta sẽ nhận được một tệp /index.html
đặt ở bên ngoài thư mục /src
và sẽ được GitHub Page
nhận diện ngay để hiển thị khi người dùng mở liên kết tới trang chủ của blog lần đầu.
Thêm vào đó thì tệp này cũng được tạo bản sao ngay trong cùng thư mục để làm tệp /404.html
và như vậy khi người dùng mở một liên kết được chia sẻ ở đâu đó và trỏ tới một trang bài viết bất kỳ thì GitHub Page
sẽ trả về tệp dự phòng có chứa code với logic hoạt động tương tự.
Ví dụ:
https://thinhtranhnvn.github.io/linux/gnome/00-gioi-thieu
Khi người dùng mở liên kết trên được chia sẻ ở nguồn nào đó thì trình duyệt sẽ nhận được tệp /404.html
thay vì tệp /index.html
, và lúc này chúng ta vẫn có code logic xử lý tương tự với tệp ban đầu để phân tích đường dẫn hiện tại và gửi truy vấn dữ liệu để điều chỉnh giao diện cho phù hợp. Trong trường hợp có tìm thấy dữ liệu phù hợp để hiển thị thì hiển nhiên nội dung sẽ không phải là một bố cục trang đơn thông báo lỗi mà vẫn sẽ là trang đơn có chứa nội dung.
Kiến trúc code
Mình đã bắt đầu kiến trúc code một cách tự nhiên xuất phát từ giai đoạn tạo ra thư mục /mockup
để soạn các trang đơn HTML
tĩnh mô phỏng thiết kế trước khi bắt đầu code logic. Ở đây, chúng ta có các thành phần là: thanh điều hướng chính Navigator
, thanh điều hướng phụ Overview
, phần hiển thị nội dung bài viết Reader
, khối liên kết đề mục Indexer
. Tất cả đều được nhóm vào một thư mục /mockup/Element
, thực ra người ta thường sử dụng từ Component
, nhưng có lẽ cũng không quá quan trọng.
Sau đó các thành phần này được sử dụng cho các bố cục trang đơn là: trang chủ HomePage.html
, trang tổng quan chủ đề TopicPage.html
, trang tổng quan loạt bài viết SeriesPage.html
, và trang bài viết PostPage.html
. Tất cả số này được nhóm vào một thư mục /mockup/Layout
. Do chỉ có duy nhất bố cục SeriesPage.html
là khác biệt so với số còn lại ở phần hiển thị nội dung chính nên mình chỉ soạn có hai tệp trong thư mục đó.
Ở đoạn này thì có một chút lưu ý đó là mình sử dụng một
convention
ngầm định nho nhỏ đó là trang chủ được xem là trang tổng quan của mộtTopic
mặc định có tên làOrigin
.
Như vậy tới giai đoạn viết triển khai code logic thì mình đã định hình là cần một module
tổng quan cho toàn bộ SPA
đó là module App
. Sau đó thì App
sẽ sử dụng các thành phần được cung cấp bởi các module Element
là : Element.Navigator
, Element.Overview
, Element.Reader
, Element.Indexer
.
Tới điểm này thì là nơi mà mình băn khoăn nhiều nhất để chọn cấp độ thiết kế đóng gói và chia sẻ trong bài viết ở đây. Cụ thể là thiết kế kỳ vọng nhất là các Element
đều được thiết kế độc lập với bối cảnh context
của thiết kế và có thể sử dụng cho các trang web khác nếu muốn.
Để triển khai thiết kế ở cấp độ này thì yêu cầu của code đó là mỗi một Element
sẽ là một chương trình biệt lập và cần định nghĩa kiểu dữ liệu nội tại riêng, chứ không biết tới các yếu tố liên quan tới logic điều hướng của ứng dụng. Sau đó code sử dụng bên ngoài của một project
bất kỳ ví dụ như chúng ta đang có module App
ở đây sẽ phải import
cả kiểu dữ liệu mà Element
đó sử dụng nội tại. Sau đó khi truy vấn được dữ liệu của một Post
thì sẽ phải chuyển đổi bản ghi Post
thành dạng bản ghi dữ liệu mà Element
kia yêu cầu để hoạt động được.
Tuy nhiên, mình đã chọn dừng lại ở cấp độ thiết kế module
đóng gói không hoàn chỉnh với các Element
có hiểu biết về logic điều hướng toàn cục của ứng dụng, và thậm chí tự phát động các yêu cầu tới GitHub Page
để truy vấn dữ liệu cần thiết để hiển thị. Như vậy module App
chỉ làm nhiệm vụ điều chỉnh bố cục tổng quan của trang đơn cần hiển thị dựa trên các yếu tố định tuyến và không cần xử lý gì nhiều tới logic hoạt động chi tiết của các Element
.
Lý do cho lựa chọn này là bởi vì thứ kiến trúc này đơn giản hơn để triển khai nếu không có nhu cầu sử dụng lại các Element
cho các project
khác. Thêm vào đó là từ góc nhìn của bất kỳ ai khác cũng mới học code và Elm
thì cũng sẽ dễ theo dõi logic hoạt động của code qua các bài viết mà mình chia sẻ tại đây. Nếu bạn vẫn chưa code xong mini project
nào bằng Elm
và rất tự tin vào khả năng của bản thân thì có thể tái cấu trúc refactor
lại source code
mà mình đã đăng tải theo kiến trúc phía trên. Đó chắc chắn là một công luyện tập rất nghiêm túc và kết quả chắc chắn cũng sẽ rất đáng giá.
Nhân tiện thì thứ kiến trúc nửa vời mà mình sử dụng có phần không tối ưu về mặt hiệu năng do các module Element
tự gửi yêu cầu truy vấn dữ liệu để hiển thị chứ không sử dụng dữ liệu chia sẻ chung. Vì vậy nên sẽ có trường hợp khi người dùng yêu cầu xem một trang đơn mà các tệp metadata.json
được gửi truy vấn nhiều lần từ các Element
khác nhau.
Với phần giới thiệu tổng quan sơ lược về mini project
này thì mình hy vọng là bạn sẽ có thể nghiệm thu quá trình học Declarative + Elm
với một bộ source code
mà cá nhân bạn cảm thấy hài lòng. Và ở thời điểm hiện tại, sau khi đã sử dụng Elm
để xây dựng một trang blog đơn giản thì mình đã thêm được dữ kiện để ra quyết định rằng sẽ không có thêm một project elm-fullstack
.
Đúng là Elm
rất tuyệt, rất đơn giản để tiếp cận khi đã có nền tảng JavaScript
căn bản và tư duy Imperative
chưa quá nặng nề. Tuy nhiên thì mình cảm thấy là rất cần thiết có một Sub-Series khác dành riêng cho câu chuyện Functional Programming
nghiêm túc với ngôn ngữ Haskell
. Đây sẽ là một dự định chắc chắn sau khi chúng ta đã ưu tiên xử lý xong Sub-Series Object-Oriented + Java
đã được định vị trước đó.
(chưa đăng tải) [Functional Programming + Haskell] Bài 1 - ...