Nhân dịp Sài Gòn quay trở lại sau đợt cách ly thì mình cũng ra một bài viết mới. Frontend Du Ký kì này sẽ kể về quá trình lựa chọn công nghệ để phát triển ứng dụng Cross-platform Apps.
Nếu bạn chưa đọc tập trước, bạn có thể đọc tại đây.

Giới thiệu

Cross-platform apps

Cross-platform Apps sẽ được viết tắt bằng CPA để thuận tiện cho người viết bài

Khi mình còn là một chàng dev trẻ trâu trung, năng động, thích khám phá và thử thách, mình được sếp tin tưởng giao trọng trách lựa chọn công nghệ để phát triển CPA vì công ty ai cũng bận sếp tin tưởng vào khả năng của mình. Không phụ lòng tin của sếp, mình tìm hiểu và lựa chọn những công nghệ hay nhất, mới nhất lúc bấy giờ để thực hiện.

Kế hoạch phát triển bao gồm 2 thành phần chính:

  • App: ứng dụng nhúng chạy trên website của khách
  • App Admin: admin dùng để cài đặt, thiết lập ứng dụng dành cho khách hàng
    Cùng nhau tìm hiểu mình đã chọn công nghệ cho từng phần như thế nào nhé!

Công nghệ App

Ngôn ngữ

JavaScript logo

Tuy TypeScript đã khá thịnh hành tại thời điểm hiện tại nhưng mình vẫn chọn JavaScript để phát triển. Lý do khiến mình chọn JavaScript:

  • Quen thuộc. Team chưa từng làm qua hoặc chỉ biết sơ qua TypeScript nên việc áp dụng TypeScript ngay vào dự án sẽ gây khó khăn khi phát triển
  • Linh động. JavaScript nhờ không có phần type-checking nên tương đối cơ động và dễ dàng biến hóa, rất phù hợp cho dự án có tính thay đổi cao và đặc biệt phù hợp với CPA – một dự án thử nghiệm

Dĩ nhiên là JavaScript không phải là lựa chọn hoàn hảo. Do không có static type-checking nên khi làm việc với JavaScript, team phải hết sức cẩn thận với việc quản lý dữ liệu để tránh các trường hợp gọi sai tên hoặc gán sai kiểu (ví dụ: gán number vào một biến string). Ngoài ra thì hỗ trợ từ code editor cũng bị hạn chế và không mạnh bằng việc sử dụng TypeScript.

UI (User Interface)

Svelte logo

Để đáp ứng được các nhu cầu của bộ bundler (đề cập ở bài viết trước) thì mình quyết định chọn Svelte. Lý do:

  • Hiệu năng tốt. So với nhu cầu của team thì hiệu năng cao chưa phải là một vấn đề quá lớn để team phải cân nhắc. Tuy nhiên, sau khi xem qua các benchmark của Svelte thì mình cũng khá ấn tượng với hiệu năng của nó so với các thư viện hoặc framework khác
  • Dung lượng bundle nhỏ. Đây là điểm sáng của Svelte. File build cuối cùng của Svelte khá nhỏ so với React hoặc Vue, đồng thời hỗ trợ sẵn tính năng đính kèm CSS vào file build nên khá thuận tiện với nhu cầu của mình
  • Dễ tiếp cận. Mình chỉ mất khoảng 1 ngày để làm quen và làm việc với Svelte. Tương đối dễ học, tài liệu cũng khá trực quan nên việc tiếp cận nó đối với một người chưa biết gì khá là dễ. Hơn nữa thì syntax (cú pháp) của Svelte giống hoàn toàn với HTML, CSS và JavaScript thông thường nên ít gặp những trường hợp như nhầm classclassName hoặc forhtmlFor ở React
  • Syntax tiện dụng. Mình khá thích {#await expression}...{:then name}...{:catch name}...{/await}{#each expression as name}...{:else}...{/each}. So với với các thư viện khác thì khá tiện dụng, không cần phải tạo biến isLoading hoặc kiểm tra array.length === 0 bằng tay nữa

Thoạt nhìn thì Svelte cũng có vẻ hoàn hảo nhưng thực sự thì vẫn có một số điểm cần lưu ý khi lựa chọn Svelte:

  • Mới. Đồ mới luôn tốt hơn đồ cũ là một nhận định đúng nhưng chưa chắc đã chính xác trong ngành công nghệ thông tin. Bản thân Svelte là một thư viện còn khá mới, hơn nữa không có tổ chức nào lớn "chống lưng" nên cũng rất dễ bị bỏ ngang. Việc có một ông lớn "chống lưng" không phải lúc nào cũng là một điều tốt nhưng ít nhiều cũng sẽ khiến chúng ta an tâm hơn khi lựa chọn công nghệ đó. Ngoài ra thì do Svelte khá mới mẻ, syntax cũng lạ nên team vẫn sẽ mất thời gian tiếp cận dù ít hay nhiều
  • Global CSS khá bất tiện. Tuy Svelte hỗ trợ viết CSS trực tiếp trong code nhưng mặc định CSS đó sẽ được "scoped" (tạm dịch là gán) cho component đó. Điều này khá bất tiện nếu bạn muốn style một class chung cho tất cả component thì bạn sẽ phải dùng cú pháp đặc biệt để khai báo. Dù không ảnh hưởng gì nhiều nhưng mình thấy khá bất tiện trong quá trình code

Thư viện hỗ trợ

Lodash logo DayJS logo Embla Carousel logo

Phần này thì tùy thuộc đặc thù app mà sẽ thay đổi tương ứng, tuy nhiên mình cũng đề cập thêm để các bạn có thể tham khảo

  • Embla Carousel. Do hạn chế không import trực tiếp được file CSS của Svelte nên việc lựa chọn thư viện carousel cũng tương đối khó. Rất may là Embla đáp ứng được nhu cầu này, đồng thời hiệu năng cũng rất tốt nên mình lựa chọn nó để phát triển phần carousel
  • DayJS. Thư viện xử lý về thời gian (ngày, tháng, năm, giờ, …) thì có rất nhiều và hầu hết thư viện đều đáp ứng được nhu cầu chung của app. Mình lựa chọn dayjs vì kích thước thư viện vừa phải, tính năng cũng vừa đủ những thứ mình cần
  • Lodash. Về các hàm utility (get, set, debounce, clone, …) thì mình chọn lodash. Underscore và ramda cũng là những lựa chọn các bạn nên cân nhắc. Team mình sử dụng lodash quen rồi nên tiếp tục sử dụng thôi. Các bạn chỉ cần lưu ý lúc import hàm để tránh "bưng" cả thư viện thay vì những hàm mình dùng là ổn

Vậy là xong phần app. Vì nhu cầu chưa có gì nhiều và phức tạp nên mình chỉ lựa chọn trước các thư viện như trên. Tiếp theo mình sẽ nói tới phần app admin, nơi mà tinh hoa công nghệ bắt đầu xuất hiện.

Công nghệ App Admin

Ngôn ngữ

JavaScript logo

Cũng giống như các lý do đã đề cập phía trên. Mình vẫn ưu tiên sử dụng JavaScript ở thời điểm hiện tại vì sự linh động của nó.

UI (User Interface)

NextJS logo

Team mình tiếp tục sử dụng NextJS để làm UI, cụ thể là NextJS 9. Phiên bản này có các thay đổi khá đáng giá như:

  • Dynamic routing theo đường dẫn file. Việc hỗ trợ trực tiếp dynamic routing thông qua cách đặt tên file thực sự rất cần thiết. Nếu như ở các phiên bản trước, bạn sẽ phải tạo custom server để xử lý việc routing, phải học qua chút chút về express để hiểu được cú pháp thì tới phiên bản 9, bạn chỉ cần đặt tên theo dạng [id].jsx là đã có thể có được dynamic route rồi. Đây là thay đổi đáng chú ý nhất ở phiên bản lần này
  • Tự động tối ưu hóa trang. Phiên bản 9 hỗ trợ tự động export trang tĩnh nếu trong trang không xuất hiện việc truy xuất dữ liệu ở phía server. Điều này làm tăng đáng kể tốc độ tải trang và qua đó tăng trải nghiệm của người dùng lên nhiều lần
  • Hỗ trợ TypeScript. Nếu như ở các phiên bản khác bạn phải thiết lập Babel, Webpack và nhiều thứ khác nữa để sử dụng TypeScript thì bây giờ nó đã được hỗ trợ trực tiếp ngay trong NextJS. Đây cũng là một điểm đáng chú ý ở phiên bản 9 dù team mình chưa có ý định sử dụng TypeScript

Khi vừa ra mắt thì NextJS 9 xuất hiện lỗi hot-reload code. Mình nhớ lúc đó code xong lưu lại có khi phải chờ 1 đến 2 giây mới có phản hồi trên giao diện. Lỗi này đã được khắc phục ở các bản nâng cấp sau đó nhưng trải nghiệm ban đầu khi sử dụng khá là khó chịu.

Deployment

Vercel logo

Nếu các bạn có nhớ thì team mình trước giờ vẫn deploy bằng tay, tức là sẽ có người đảm nhận công việc merge code, build và push lên server. Quá trình này tiêu tốn khá nhiều nguồn lực của team nên khi phát triển CPA, team quyết định sử dụng luôn hệ thống Vercel (trước đây là Zeit Now) để tự động deploy mỗi khi code được push lên GitHub.

NextJS là một trong những sản phẩm của Vercel nên nó được hỗ trợ khá tốt, không cần tinh chỉnh gì nhiều ngoài việc thiết lập biến môi trường (Environment Variable). Nhờ việc dự án được tự động deploy nên team cũng có nhiều thời gian hơn để tập trung vào việc phát triển tính năng và cũng không còn lo sợ khi người phụ trách deploy vắng mặt ở công ty nữa.

Component kit

Shopify Polaris logo

Rút kinh nghiệm của EGANY Apps, lần này team quyết định sẽ sử dụng một bộ component có sẵn để tiết kiệm thời gian phát triển. Được sếp gợi ý Shopify Polaris nên mình cũng dùng thử. Sau vài ngày trải nghiệm thì mình quyết định chọn Shopify Polaris luôn, lý do:

  • Design Guidelines tốt. Shopify là một trong những công ty e-commerce hàng đầu hiện nay. Vì vậy, Design Guidelines của Shopify rất rõ ràng, chi tiết và vô cùng phù hợp với các hệ thống e-commerce
  • Gần gũi với người dùng. Nhiều nền tảng e-commerce đi theo xu hướng thiết kế của Shopify nên khi sử dụng Polaris, team sẽ tạo được cảm giác thân thuộc cho khách hàng. Hơn nữa, vì các component của Polaris được thiết kế rất tốt nên xét về UI/UX thì team cũng được "hưởng soái"
  • Tài liệu chi tiết, rõ ràng. Tuy vẫn còn một số chỗ chưa chính xác tuyệt đối nhưng tài liệu component của Polaris khá tốt, có ví dụ rõ ràng và có hẳn hướng dẫn sử dụng bao gồm khi nào nên dùng, trình bày như thế nào cho đúng, …

Điểm duy nhất mình không thích ở Polaris là không custom được nhiều. Cũng dễ hiểu vì Polaris phát triển tập trung cho hệ thống e-commerce của Shopify. Mọi thứ đều được "đóng gói" và "chốt đơn". Dĩ nhiên là bạn vẫn có thể custom về màu sắc (theme) hoặc ngôn ngữ (locale) nhưng nếu bạn muốn thay đổi layout thì quả thật là hơi khó khăn.

Rich-text editor

CKEditor logo

Đa số các component kit như Ant Design, Material Design hay cụ thể là Shopify Polaris đều không hỗ trợ rich-text editor nên mình phải tìm thư viện thay thế.

Để tiết kiệm thời gian thì mình sử dụng lại CKEditor có sẵn ở EGANY Apps. Vì đa số các thư viện rich-text editor đều không hỗ trợ chế độ SSR (Server Side Rendering) nên việc tích hợp cũng mất chút thời gian.

Đánh giá sơ bộ về CKEditor:

  • Nhiều tính năng. CKEditor có khá nhiều tính năng, đây là một điểm cộng nhưng rất tiếc là team chỉ dùng một số ít trong đó nên việc custom để ẩn/tắt bớt tính năng rất mất thời gian
  • Hiệu năng không cao. Vì không hỗ trợ SSR nên mọi thứ phải được load ở phía client. Do có nhiều tính năng nên việc khởi tạo editor mất nhiều thời gian, gây cảm giác bị "khựng" ở giao diện. Tuy đã giải quyết bằng việc load async nhưng trải nghiệm người dùng ít nhiều vẫn bị ảnh hưởng
  • Tài liệu cho React không hiệu quả. Mình đánh giá tài liệu lúc bấy giờ của CKEditor cho việc tích hợp React khá mờ nhạt, không nhiều ví dụ có ích. Đa phần mình phải tự đi tìm giải pháp trong Issues ở GitHub hoặc các website hỏi đáp khác như StackOverflow

State management

React logo SWR logo

Có lẽ từ khóa state management không còn xa lạ với các bạn nếu các bạn từng tiếp xúc với React, Vue, Svelte hoặc các thư viện/framework hỗ trợ làm Single Page Application.

State management thì mình sẽ chia làm 2 dạng:

  • Server State Management. Quản lý state đến từ server, tức là có tương tác với API để kiểm soát state
  • Client State Management. Quản lý state ở client, tức là không có hoặc ít tương tác với API. Nếu có thường chỉ là trạng thái ban đầu của state (initial state)

Ở client thì mình có thử qua Recoil, một thư viện mới ra mắt của Facebook. So với Redux và MobX thì Recoil dễ sử dụng hơn, ít rườm rà hơn. Tuy nhiên, do Recoil mới chỉ ở trạng thái beta và tài liệu lúc này cũng chưa đầy đủ nên mình không dùng.

Sau khi cân đo đong đếm nhu cầu của dự án thì mình quyết định sử dụng React Context (sử dụng useContext hook). Thời gian đầu thì team không gặp vấn đề gì khi làm việc với React Context. Tuy nhiên, sau khi dự án phát triển lớn hơn thì một số vấn đề liên quan tới hiệu năng phát sinh, cụ thể là vấn đề về việc render của component. Chi tiết như thế nào hẹn các bạn ở bài sau nhé!

Client state management trước giờ làm nhiều và cũng sử dụng nhiều thư viện rồi nên mình có kinh nghiệm. Server State Management thì thật sự mà nói thì ngay tại thời điểm tìm hiểu công nghệ thì mình mới biết tới từ khóa này. Lúc bấy giờ có React Query và SWR. Mình có dùng thử cả hai, về công dụng thì cả hai đều đáp ứng tốt, React Query có phần nhỉnh hơn về tính năng có vẻ vì vậy mà nó hơi phức tạp. Cuối cùng thì mình chọn SWR, lý do:

  • Được phát triển bởi team Vercel. Nếu đã dùng NextJS và Vercel rồi thì mình nghĩ dùng luôn SWR sẽ có được sự "hòa hợp" cao nhất giữa các thư viện và framework. Nếu có vấn đề phát sinh thì phía Vercel cũng sẽ hỗ trợ cho cả NextJS và SWR
  • Tiết kiệm lượt request về backend. Vì request sẽ được cache ở phía client nên khi gọi một request nhiều lần (thông qua SWR) thì tiết kiệm được kha khá lượt request. Hơn nữa, nhờ kết quả được cache trước nên tốc độ tải trang tăng đáng kể. Team rất bất ngờ với hiệu năng mà SWR mang lại cho website
  • Trải nghiệm real-time. SWR hỗ trợ gọi lại API (re-fetch) khi người dùng chuyển tab và quay lại ứng dụng. Điều này tạo ra cảm giác "real-time" về mặt dữ liệu của người dùng. Ví dụ:
    • Người dùng nhấn nút thanh toán
    • Ứng dụng mở tab mới cho người dùng thanh toán
    • Sau khi thanh toán xong, người dùng quay lại tab đang mở ứng dụng
    • Ứng dụng fetch lại thông tin thanh toán và cập nhật lên giao diện

Mình thật sự hài lòng với trải nghiệm mà SWR mang lại. Ngay chính sếp mình khi trực tiếp sử dụng trong quá trình điều chỉnh nội dung trang (lấy từ API ở dạng JSON) cũng khá bất ngờ với tính năng "real-time" này. Mình nhớ sếp còn hỏi: "Ủa em làm API nội dung real-time luôn à?". Mình chỉ cười và gật đầu. #cú-lừa

Thư viện hỗ trợ

Lodash logo DayJS logo

Axios logo

ExcelJS logo

Ngoài lodash và dayjs thì mình còn sử dụng thêm các thư viện sau:

  • ExcelJS. Hỗ trợ xử lý file excel, chủ yếu phục vụ cho việc import/export dữ liệu trong ứng dụng
  • Axios. Thư viện hỗ trợ gọi API. Cá nhân mình thấy axios xử lý khá tốt trong việc gọi API, bổ sung nhiều tính năng hữu ích như timeout hoặc progress khi upload file. Điều mình không thích là object error trả về của axios khá rườm rà. Nếu xử lý chung với các lỗi thông dụng thì sẽ phải qua nhiều bước kiểm tra để xác định là lỗi của axios hay lỗi runtime bình thường

Câu chuyện license

Nguồn ảnh: https://dribbble.com/shots/10565800-Black-Caution-Sign

Một ngày đẹp trời sau khi hoàn thành mọi tính năng, trong khi chờ phản hồi từ tester thì mình có đọc qua một bài viết nói về việc lựa chọn license phù hợp cho dự án của mình. Đọc xong thấy khá thú vị, tò mò vào đọc license của Shopify Polars thì mới nhận ra vấn đề:

  • Polaris cho bạn toàn quyền khi phát triển các ứng dụng nằm trong hệ sinh thái của Shopify
  • Các ứng dụng nằm ngoài hệ sinh thái của Shopify nếu có sử dụng Polaris thì phải khác biệt (cả về tính năng và giao diện) so với các sản phẩm hoặc dịch vụ của Shopify cung cấp – điều này thuộc quyền quyết định của Shopify

CPA chắc chắn sẽ khác về tính năng nhưng rất tiếc là phần admin của CPA khá giống so với phần admin của Shopify. Ngoài ra thì team cũng có điều chỉnh component để phù hợp hơn với design của team. Điều này đồng nghĩa với việc team đang có nguy cơ vi phạm license của phía Shopify.

Nhận được tin thì team cũng có họp lại với nhau và đưa ra quyết định là sẽ tiếp tục sử dụng Polaris cho tới khi sản phẩm hoàn thiện và đến được tay của một số người dùng beta. Sau khi nhận được phản hồi của người dùng thì team sẽ tiến hành chuyển đổi qua một component kit khác, hay nói cách khác là đập bỏ toàn bộ giao diện hiện tại và thay thế bằng một giao diện khác.

Tạm kết

Như các bạn đã thấy, công sức vài tháng trời xem như công cốc chỉ vì vài dòng chữ license. Đúng như lời ông bà ta nói: "Sai một ly, đi một dặm". Đây quả thật là một bài học đắt giá cho cá nhân mình cũng như các thành viên trong team trong việc lựa chọn công nghệ để sử dụng trong dự án, đặc biệt là những dự án thương mại.

Quá trình chuyển đổi diễn ra như thế nào? Công nghệ nào sẽ được sử dụng cho phiên bản tiếp theo? Hẹn gặp các bạn ở tập tiếp theo nhé.

Bài thuộc chuyên mục Engineering và cả chuyên mục Culture vì công ty mình rất chú trọng văn hóa chia sẻ và học hỏi. Các bạn có thể đọc "sương sương" về EGANY để biết vì sao.
Còn các cơ hội để làm "Bug Contributor" cùng mình thì ở đây 😆.

Cám ơn các bạn đã đọc bài. Happy hacking! 👌