Node.js模板引擎的深刻探討

每次當我想用 node.js 來寫一個 web 相關項目的時候。我老是會陷入無比的糾結。緣由是 JavaScript 生態圈裏的模板引擎實在太多了,但那麼多卻實在找不出一個接近完美的,所謂完美的概念就是功能豐富,書寫簡單,先後端可共用等一些屬性。儘管可以在 Template Chooser 按功能進行挑選。但挑選的結果再用來對照仍是各有各的問題。javascript

因此乾脆就一些模板引擎進行略微深刻的分析,但願經過對照總結出哪一種更值得去使用。php

第一輪排除

在上次node模板引擎簡單比較的文章裏。事實上已經有個簡單的篩選了。總結成規則應該是這種:css

  1. 首先避免使用需要對 HTML 進行轉換的 Jade 之類。html

    對於這類需要翻譯才幹使用的語言工具我是堅定的抵制,比方噁心的CoffeeScript。緣由是這根本不是一個必要的過程。而且創造一種瀏覽器默認不支持的語言來表達首先提升了學習成本。特別是假設在團隊中合做那就有必要讓每個人都學會並配置上適合的開發工具。其次還要再翻譯回來,說的難聽點實在是脫褲子放屁,即便有本身主動化工具。但帶來便捷上的收益抵只是徒增的成本。前端

  2. 第二類是原生或者完整語法的 EJS 之類java

    這類引擎自己沒有什麼太大的問題,對於學習成原本說 EJS 是很低的,會 JS 就會寫,但是相比較於後來的 mustache 輕邏輯系列,那麼一定認爲在模板中寫完整的 JavaScript 語法實在太麻煩。而且在模板中。核心是模板,而不是編程語言。因此表達方式應該更偏重於模板。node

  3. 其次針對模板的功能進行考慮git

    事實上這應該是做爲首要考慮的緣由,因爲功能纔是模板引擎的核心,但是實現一個模板引擎在現在來看並不是什麼太困難的事情。因此大部分功能都差點兒相同,那麼主要考慮的就是差別部分。github

    比方模板繼承和引用是我看中的一個必要功能。可以很是大程度上提升複用性。另外可能考慮的是輸出變量表達式或者管道過濾器之類輔助功能。有這些會方便很是多。web

  4. 最後纔是性能的考慮

    因爲上面說到模板並不是太複雜的東西,性能上面通常不用特別關注。因爲大多數引擎都會帶有預編譯的功能,當一個模板預編譯成簡單的拼接函數,通常是不會有太大的性能壓力。那麼這一條考察的基本就是可否預編譯。

最後在 Template Chooser 上依據條件選擇下來,就剩這些了:

基本上就剩下 mustache 繫了

儘管以前也接觸了一些的模板引擎,傳統的比方 PHP 的 Smarty。Java 的 Velocity,甚至曾經公司裏跨平臺的火麒麟等,但我仍是認可看過一次 mustache 就對這個系列產生了偏好,那麼接下來就具體分析一下他的一些特性。

mustache類引擎特性分析

使用 # 做爲統一斷定符號

長處是簡潔, if / for 都可以通吃。但缺點是依賴於必選變量,很是難將推斷條件其擴展成表達式。另外 for 的索引變量名也沒法設置。

當 #xxx 是一個 Object 類型值時。是應該依照 if 存在推斷仍是依照 for-in 遍歷?眼下是推斷存在並建立下級做用域,這就致使沒法使用 Object 類型做爲 Map 進行 for-in 遍歷。

另外一點,假設是在進行一個 list 的循環時,沒法定義循環項和索引的變量名。很是難利用到索引這個特殊變量。只是在 GRMustache 的實現中有加強,可以使用 @index特殊變量,但這不是一個比較好的解決方式。

# 做用域效果

# 模塊生成了子級做用域,使模塊內的子級變量獲得簡寫。缺點是設計渲染引擎的時候可能對於scope的狀況考慮起來比較複雜,比方不一樣層次的同名變量,有可能會致使性能問題。

模板符號

除了變量輸出,控制語句的雙括號有點多餘了,可以考慮下降一層,因爲內部仍是一個符號,形成代碼衝突的概率事實上很是低。另外模板語句的結束符名稱事實上也可以省略掉,正如語法分析過程當中的關閉括號。演示樣例:

{#test}
 * {{variable}}
{/}

可能原始設計以檢測到雙括號做爲打開解析引擎的標誌,假設改成單括號,後面碰到非模板的字符需要回朔一位。

分支語句

由於條件推斷都合併到了 # 符號的語句中。而推斷由於要簡化多種形式。致使不能使用表達式。而 mustache 僅僅設計了 ^ 用來取反的一種表達方式,實際上和 # 都沒有不論什麼關聯,在表達能力上就比較不足,比方想用 else if 就很是麻煩。

相似 switch/case 的多條件分支就更難實現,儘管用的也很少,但是仍是會有必定機會。

模板複用

在 mustache 裏僅僅有一種,就是引入模板片斷,相似於其它引擎的 include。符號是{{>partial}} 。

而且這指定的是模板名,在後端程序中通常是直接尋找文件名稱,但還需要本身映射。

另外除了Handlebars其它也不支持繼承形式的模板複用。因此我以前寫了MustLayout這個npm包來在express中預處理這兩個缺陷。

變量

變量在 mustache 中很easy,差點兒僅僅有模板替換的功能。

而在其它引擎中,可以對暫時變量賦值,輸出可以使用表達式,或者管道過濾器等便捷的方法。

對照其它模板引擎的突出特性

暫時變量賦值

如 liquid/Smarty/Swig 等中,可以在渲染模板時建立暫時變量,在某些狀況下有必定的便利性。

比方在不一樣模板裏引用一個模板片斷,該片斷中的某個變量名是固定的。但在不一樣地方引用的時候變量名不一樣,此時可以在引用以前聲明一個統一的變量,幫助統一引用。

這個特性必定程度上也可以由函數模板來完畢。

變量過濾器

在 liquid/Smarty/Swig/Etpl 等中,可以經過相似 *nix shell 的管道模式,對要輸出的變量進行不少其它處理,比方日期格式化。編碼轉義等功能。

{{ aDate | date(‘Y-m-d') }}

Swig的塊繼承

在繼承 block 塊時可以使用父模板中已定義的部分。方便的追加不少其它內容,比方 CSS 和 JS 的引用部分:

{% block head %}
  {% parent %}
  <link rel="stylesheet" href="custom.css">
{% endblock %}

Dust.js的繼承

Dust.js 的繼承方式看起來比較詭異,是使用一個正常理解應該是 include 的方式來實現的。而且符號也是從 mustache 系繼承過來的 {>parent} ,而只在以後定義 block 區塊,對父模板進行覆蓋來實現。從實現的角度看這是一個比較取巧的方式,因爲假設不過聲明 layout 。那麼聲明語句究竟放在模板的哪裏比較合適?假設聲明兩次是否會形成問題?而經過引入的話就比較直白了,不管如何這是必須寫的且只會寫一次。我是要用父模板。先拿進來。以後的 block 部分其實是重名再次定義的賦值過程。

issue裏甚至有人提到這樣的寫法應該使用開閉標籤。讓 {>parent}…{/parent} 之間包括其block的內容,也有道理,但是寫起來是略有複雜,不夠直白。

Etpl的引用帶入

在 include 一個模板片斷時代入一個本身定義的塊。以覆蓋片斷中的部份內容。這給 block 除了向上繼承之外不少其它的一種靈活性。

<!-- import: main -->
    <!-- block: main -->
        <div class="list">list</div>
        <div class="pager">pager</div>
    <!-- /block -->
<!-- /import -->

Etpl的擴輾轉換引擎

在 Etpl 中稱爲過濾器,眼下用例是將 Markdown 格式的模板內容轉換成HTML,有必定價值。但不必定是必須功能,可以考慮做爲擴展實現。

<!-- filter: markdown(${useExtra}, true) -->
## markdown document

This is the content, also I can use `${variables}`
<!-- /filter -->

期待 mustache 加強的特性

對照了那麼多,事實上說對 mustache 最基本的偏好仍是來自於模板語言表達的間接性,而對於他最核心的輕邏輯來講。有點過輕。儘管我不需要完整的原生語言控制,但輕的難以表達了就仍是需要權衡。

終於我把我期待的模板引擎的樣子描繪出來。看看是否是有人和我同樣。

最基本的變量仍是使用雙括號,而控制語句使用但括號+特殊字符,同一時候關閉可以爲自結束。並且不需要寫相應的關閉標籤名。

變量輸出

使用雙括號在模板中輸出變量:

{{ variable }}
{{ nested.element }}
{{ array[index] }}
{{ object[key] }}

輸出可以使用帶運算符的簡單表達式:

{{ ok ? 1 : 0 }}
{{ ok || 'none' }}
{{ index * (x + 3) }}

可以使用過濾器管道:

{{ variable | escapeHTML }}
{{ today | date:'Y-m-d' }}
{{ group | max }}

默認不進行 HTML 轉義。這樣可以支持不少其它情景,而不是 HTML 專屬。相反使用三括號才進行默認轉義:

{{{ content }}}

可以使用等號 = 進行暫時變量賦值,但賦值使用專門的 $ 符號語句且需要自關閉符號:

{$ x = y * 5 /}
{$ obj = {a: 1, b: []} /}

變量做用域沒有發現太大的必要性。而且可能形成性能問題,臨時取消。

條件分支

儘管 mustache 的 # 功能很是強大。但表達能力略有欠缺且easy形成歧義,因此我仍是把條件分支單獨拿出來。

if 語法用問號開頭表達,和條件表達式同樣有疑問的意思:

{? expression }
 true
{/}

{? !condition }
 false
{/}

else 語法借用原來的 ^ 符號,且再也不可以單獨使用這個取反符號:

{? expression}
 true
{^}
 false
{/}

else if 類型的多條件繼續使用 ^ 符號進行額外推斷:

{? case1 }
 1
{^ case2 }
 2
{^}
 -1
{/}

臨時沒想到怎樣簡潔的表達對同一條件的 switch/case 表達,先用 else if 結構取代。

循環迭代

普通的 for 循環繼續使用 # ,但添加迭代條目和索引暫時變量聲明:

{# list:item@index }
 <li>{{ index }}: {{ item }}</li>
{/}

循環可以針對普通數組。也可以針對 Object 類型的對象:

{# map:value@key }
 <li>{{ key }}: {{ value }}</li>
{/}

可以聯合取反符號 ^ 使用。輸出沒有元素項時的內容:

{# []:item }
 {{ item }}
{^}
 none items :(
{/}

模板複用

內嵌模板片斷:

{> partialName /}

模板名稱可以在 API 中分狀況實現。比方在後端 node.js 環境中,模板名直接相應相對路徑進行文件讀取;而在前端假設是使用 <script type="text/template">方式加載的,可以在相應標籤屬性,經過 DOM 選擇器讀取。

可以考慮引入 etpl 的片斷替換擴展:

{> partialName }
 {+ blockName }My Title{/}
{/}

繼承上級模板可以考慮引入 Swig 的父級塊引用:

<!-- parent -->
{+ scripts}
 <script type="text/javascript" src="lib.js"></script>
{/}

<!-- target -->
{< parent /} {+ scripts }  {+/}<!-- 內嵌上級的塊 -->
 <script type="text/javascript" src="main.js"></script>
{/}

這樣的聲明式寫法比較easy理解。而假設要實現簡單,可以學習 dust.js 直接利用片斷插入擴展的方法。

引擎規則

這樣設計的符號體系,讓引擎可以從最簡單的規則出發,並減小衝突的可能性。

  1. 默認進入文本狀態,當遇到第一個開始括號+控制符號 /\{([\{\$\?\/\+#^<>}])/ 時。進入控制狀態;
  2. 依據 RegExp.$1 的符號推斷進入何種控制流程。比方是 { 則直接準備輸出;
  3. 不論什麼  變量輸出 { 控制流程必須有相應的關閉結束標籤 {/} 或者自關閉.../} ,僅僅有取反條件操做 {^} 例外;
  4. 取反操做符 {^} 必須嵌套在條件語句和循環語句中使用;
  5. 僅僅要模板聲明瞭繼承 {< parent /} ,則會忽略不論什麼 {+ block }...{/} 標籤包圍以外的內容。
  6. 繼承和嵌入片斷都可以遞歸;

先後端共用模板的一些問題

問題一:後端可以用文件名稱取代模板名,但前端沒有。

因此要在前端生成模板名。

  1. 用script標籤發送到前端,解決模板名又能合併。缺點是佔用(污染)了前端的id,可能會產生衝突。

    (事實上也可以用其它元素屬性來標識,反正都是使用querySelector類的查詢)

  2. 加入一個模板名。並交給前端解析。

    缺點是添加了一種模板結構和命名,設計很差的話可能很是可貴到承認。

假設生成後,先後端模板名不一致,也會致使沒法複用。比方需要include的時候,後端默認是文件路徑。但前端通常的模板名稱不會帶有斜槓 / 字符。

問題二:在後端 render 完某個 path 的頁面後,前端接管並使用 ajax 方式,此時使用 History API 在 route 到下一個 path 的時候,調用 view 的相應模板怎樣推斷使用那一層 layout?

問題三:後端和前端在模板中使用的變量名或者層次每每不一致,假設僅做爲片斷問題到不大。但假設是整頁渲染,可能需要額外的針對性處理。

API設計

var Mustplus = Class({ config: { ifStart: ['{?', '}'], ifEnd: '{/}', elseWord: ['{^', '}'], forExp: /\{#(\w+)(?:\:(\w+))?(?

:@(\w+))\}?/, forEnd: '{/}' // ... }, templates: {}, compiled: {}, constructor: function (options) { this.read = browser ? this.readDOM : this.readFile; }, readDOM: function (name) { return $('script[type=text/template][name=' + name + ']').html(); }, readFile: function (file) { return fs.readFileSync(path.join(this.base, file)); }, // 繼承擴展 extend: function (name) { }, include: function (template) { return template.match(includeRE) ? template.replace(includeRE, function (name) { return this.include(this.read(name)); }) : template; }, resolve: function (name) { return this.include(this.extend(name)); }, compile: function (name) { var content = this.resolve(name); var stream = this.parse(content); var fn = ''; while(token = stream.next()) { switch(token.type) { case 'text': default: fn += text; } } return new Function('data', fn); }, cache: function (name) { return this.compiled[name] || (this.compiled[name] = this.compile(name)); }, render: function (name, data) { return this.cache(name)(data); } }); var engine = new Mustplus({/*if config*/}); var template = engine.compile('xxx'); template(data); // or engine.render('xxx', data);

最後

眼下市面上最接近我想法的應該就是 dust.js 了。難怪 LinkedIn 的project團隊 經過對照最後選擇的也是 dust.js 。固然如我期待的話還有可以改進的地方,YY老是要有的。萬一哪天順手就實現了呢?

相關文章
相關標籤/搜索