Sử dụng template nunjucks cho ứng dụng JavaScript, ban đầu bối rối nhưng kết quả tuyệt vời

Tôi đang thử làm một website trưng bày hàng dựa trên framework Saleor, nhằm thay thế web cũ tôi đã làm cách đây 9 năm (http://kimphat.vn). Nhân việc làm mới, tôi cũng quyết áp dụng một vài kĩ thuật mới, như là biến phần giao diện web thành một ứng dụng JavaScript hẳn hoi, bằng cách sử dụng một framework MVC (Model-View-Controler) bằng JavaScript.

Việc sử dụng framework MVC JavaScript thực ra không phải là mới lắm. Tôi có một người bạn, trẻ hơn tôi nhiều tuổi và hắn đã thành thạo AngularJS lắm rồi. Tuy nhiên, do tôi không làm web một cách liên tục (từ lúc ra trường, tôi làm về phần mềm nhúng nhiều hơn), cộng thêm tuổi tác có thể gọi là già nếu so với các lập trình viên xung quanh (không so với quản lý dự án, hehe) nên tôi cảm thấy hụt hơi với các công nghệ web mới. Cho nên bước đầu dấn thân vào kĩ thuật này cũng cam go và nhiều cảm xúc.

Lần đầu tiên tôi chạm tay vào một framework MVC JavaScript là cách đây vài tháng, với Backbone, khi đang làm website cho công ty (easyuni.com). Lúc đấy, phần giao diện chủ yếu được đảm nhiệm bởi một đồng nghiệp, mà vì lí do nào đó, anh ta chọn và đưa Backbone vào sử dụng. Sau đó, tôi phải làm một tính năng là tạo widget, tóm tắt nội dung từ forum được điều hành bởi công ty (forum.easyuni.my), nhúng vào website chính của công ty. Do cái widget này chủ yếu đụng đến code ở mặt front-end, tức là code bằng JavaScript, HTML là chính, nên tôi nghĩ đến dùng Backbone luôn, nhân tiện nó đang được dùng trên website rồi.

Bây giờ, khi làm website trưng bày hàng hóa (không hẳn là thương mại điện tử), tôi vẫn chọn Backbone. Không hẳn là vì tôi chọn theo quán tính. Tôi cũng đã đưa vào tầm ngắm 3 cái: Backbone, AngularJS, Emberjs. Emberjs được đưa vào danh sách vì tôi thấy màn thể hiện ấn tượng của nó trong Discourse, phần mềm mà cty tôi dùng để chạy forum. Nhưng sau khi thấy AngularJS, Emberjs có nhiều khái niệm mới quá, sợ tốn nhiều thời gian học, không kịp làm nên tôi quay lại Backbone. Vả lại, dung lượng của 2 framework kia cũng khá nặng. AngularJS hay tự nhận nó nhẹ nhất, nhưng đấy là do nó làm cho Backbone trông có vẻ nặng bằng cách gộp dung lượng của jQuery vào. Đúng là Backbone cần phải có jQuery mới chạy được, nên việc gộp dung lượng jQuery vào Backbone là hợp lý. Nhưng vấn đề là dù tôi có dùng AngularJS đi nữa thì website của tôi cũng phải dùng jQuery, để code JavaScript cho các tính năng khác. Cho nên nếu cộng jQuery vào cả AngularJS và Backbone thì bộ đôi Backbone + jQuery vẫn nhẹ hơn.

Sau khi chọn Backbone, đọc thêm vài tài liệu, thấy bảo Backbone chỉ là 1 framework đơn sơ, cơ bản, cần thêm 1 framework "lớp trên", mở rộng từ Backbone để làm việc cho dễ. Thế là tôi lấy thêm Marionette, cho đỡ phải viết code nhiều. Nhưng mà, hình như thêm cái Marionette này vô thì làm cho cục Backbone kia trở nên nặng hơn so với cục AngularJS rồi. Chậc, thôi kệ, lỡ lấy về rồi, giữ luôn.

Sử dụng Backbone, tôi thấy khó chịu với loại template có sẵn của nó. Đó là template của Underscore, với cú pháp khá rối mắt. Tôi thích cú pháp template của Django/Jinja2. Thứ nhất vì tôi code backend cho web bằng Python, với các framework Django, Flask nên nhìn template của chúng quen rồi, thứ nhì là vì phần mềm soạn thảo tôi ưa dùng, KomodoEdit, không hỗ trợ highlight cho template của Underscore. Mà có vẻ nhiều người cũng thích cú pháp template của Django/Jinja2 giống tôi. Bằng chứng là có rất nhiều template engine sử dụng cú pháp này, như trên PHP thì có Twig, JavaScript có Swig, Nunjucks, Ruby thì có Liquid, Lua có Lupa. Không biết ai là người sáng chế ra cái cú pháp này mà có ảnh hưởng lớn đến thế.

Trước kia, khi thử Backbone lần đầu tiên, tôi dùng Swig làm template. Khi ấy cũng có tìm được Nunjucks rồi, nhưng lại đọc thấy Nunjucks khuyên dùng precompiled template, ngại cài thêm phần mềm để compile nên gạt đi, chọn Swig. Giờ mới thấy sai lầm. Nhân đọc được comment trên Reddit, thấy bảo

nunjucks is so easy, configurable and nice... nunjucks does everything himself

đồng thời lại tìm được thư viện giúp gắn kết Nunjucks với Marionette (https://github.com/paulovieira/marionette-nunjucks) nên quyết thử Nunjucks một phen. Sau một vất vả ban đầu vì không hiểu tài liệu, đến khi làm cho nó chạy được thì thấy kết quả quá tuyệt. Sau đây cũng xin chia sẻ chút kinh nghiệm kết hợp Nunjuck với các thành phần khác.

Ban đầu, tôi ngại việc compile template, cũng chưa dùng lớp gắn kết Marionette-Nunjucks nên tôi dùng code cho View như sau:

var ProductThumbView = Mn.ItemView.extend({
  tagName: 'li',
  template: false,
  render: function() {
    this.$el.html(nunjucks.render('static/tpl/product_thumb.html', this.model.attributes));
    return this.$el
  }
});

Vì tôi đang dùng Django ở backend nên file template cho Backbone được đặt trong /static/ để khỏi phải qua xử lý của Python, mà sẽ được chuyển phát trực tiếp từ Nginx. Khi cho file template chuyển phát trực tiếp từ Nginx như một file tĩnh thông thường, tôi hi vọng nó sẽ được cache bởi trình duyệt, và được sử dụng nhiều lần mà không cần tải lại. Có một lưu ý nhỏ là mặc dù URL thực của file có "/" ở đầu (/static/tpl/product_thumb.html), nhưng khi truyền vào nunjucks.render() ta phải bỏ dấu "/" này đi.

Kết quả của việc render như hình sau, coi như là thành công:

Kết quả

Cũng khá yên tâm, cho tới khi coi các request trong Firebug thì tôi tá hỏa là file template không được cache. Trái lại, ProductThumbView được dùng để render bao nhiêu sản phẩm thì file template bị tải bấy nhiêu lần, với query string thay đổi khiến cho URL thay đổi và bản cache của file không thể tái sử dụng. Điều này khiến tôi bắt buộc phải tìm cách để dùng phương án precompile.

Vì Saleor dùng Bower để tải và nhớ danh sách các thư viện JavaScript, đồng thời dùng Grunt để compile, đưa vào đúng chỗ các file JS, CSS nên trước khi có thể dùng template compile, tôi lại phải mày mò cách dùng Grunt để compile các file template này. May sao, có công cụ grunt-nunjucks.

Sau khi chạy Grunt để compile được file template ra templates.js và đưa được vào đúng chỗ (/static/), tôi lại lúng túng không biết gọi Nunjucks ra sao, hàm nào và tham số như thế nào. Trong tài liệu chỉ có ví dụ như với template chưa compile:

nunjucks.render('foo.html', { username: 'James' });

Khi đã compile thành templates.js thì chẳng lẽ tôi tiếp tục dùng

nunjucks.render('static/tpl/product_thumb.html', this.model.attributes)

y như lúc chưa compile? Hơn nữa, lúc đấy, biến template và hàm render() của ItemView phải được đặt làm sao? Đây là điều mà cả tài liệu của Nunjucks lẫn Marionette-Nunjucks đều không nói. Cố gắng mày mò, tôi xem thử nội dung của file templates.js thì thấy đoạn như sau:

(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["product_thumb.html"]

Rồi tôi thử hú họa với code như sau:

var ProductThumbView = Mn.ItemView.extend({
  tagName: 'li',
  template: 'product_thumb.html'
});

thì ô hô, code chạy được và ra đúng như ý muốn! Như vậy, so với dùng Swig trước kia, khi mà tôi phải viết một đoạn code riêng để tải file template qua Ajax, rồi đọc nội dung, gọi swig.compile() trước khi render, thì lần này với Nunjucks, code được rút ngắn đi rất nhiều! Mừng rỡ, tôi nghĩ chắc là mình sẽ dùng Nunjucks từ giờ trở đi, cho các dự án sau. Và nếu có điều kiện, chắc tôi sẽ sửa lại front-end cho www.easyuni.com để dùng Marionette và Nunjucks luôn.

Bài này tôi viết với mục đích chia sẻ mẹo dùng Nunjucks trong bối cảnh "Django + precompile template" là chính. Nhưng nội dung của mẹo thì quá ngắn, mà chẳng biết dẫn nhập làm sao, thành thử tôi phải viết lan man thành những bước chập chững sử dụng framework JavaScript và đánh vật với vấn đề template.