Juicer - 一個Javascript模板引擎的實現和優化
讓我們從一段代碼說起,假設有一段這樣的JSON數據:
var json={ name:"流火", blog:"ued.taobao.com" };
我們需要根據這段JSON生成這樣的HTML代碼:
流火 (blog: ued.taobao.com)
傳統的Javascript代碼一定是這個樣子:
var html;
html=''+json.name+' (blog: '+json.blog+')';
不言而喻,這樣的代碼混雜了html結構和代碼邏輯,而且代碼不具可讀性,不便于后期維護,于是便有了這樣一個函數:
function sub(str,data) { return str .replace(/{(.*?)}/igm,function($,$1) { return data[$1]?data[$1]:$; }); }
有了這個函數,我們拼接字符串的工作就可以簡化為:
var tpl='{name} (blog: {blog})';
var html=sub(tpl,json);
看到這里,不用我多說,我想通過這個例子直觀的展現出前端模板引擎的好處所在,這么做能夠完全剝離html和代碼邏輯,便于多人協作和后期的代碼維護。當然,當我們的業務邏輯需要對數據源進行循環遍歷,if判斷等的時候,這個簡明的函數很顯然并不能滿足我們的需求,于是便有了如今這市面上眾多的模板引擎,諸如Mustache, jQuery tmpl, Kissy template, ejs, doT, nTenjin, etc.
“如無必要,勿增實體。” 這是著名的奧卡姆剃須刀法則,簡單的說就是避免重復造輪子。那么就會有童鞋質疑,既然已然有這么多現成的東西可用,為什么還要重新打造一個呢?
我個人認為一個完善的模板引擎應該兼顧這幾點:
- 語法簡明
- 執行效率高
- 安全性
- 錯誤處理機制
- 多語言通用性 </ul>
- 循環 {@each}…{@/each}
- 判斷 {@if}…{@else if}…{@else}…{@/if}
- 變量(支持函數)${varname|function}
- 注釋 {# comment here} </ul>
而市面上現有的模板引擎沒有做到兼顧以上幾點,比如Mustache支持多種語言,通用性不錯,不過性能稍差,而且語法不支持高級特性,例如遍歷的時候無法做if判斷,也無法獲得index索引值,jQuery tmpl依賴jQuery,缺乏可移植性,Kissy template雖然依賴Kissy, 不過性能和語法都值得推薦,doT/nTenjin 性能和靈活性都很不錯,但是語法需要用原生的js來寫,寫好的模板代碼可讀性稍差。
魚和熊掌不可兼得,語法的處理,安全性的輸出過濾和錯誤處理機制的引入在一定程度上都會或多或少降低模板引擎的性能,因此就需要我們權衡。 Juicer 在實現上首先將性能看做第一個重要的指標,畢竟性能好壞直接影響用戶的感知,同時兼顧了安全性和錯誤處理機制(即便這樣會導致性能的略微下降)。
首先來看下jsperf上同幾個主流模板引擎的性能對比。
可以看到,性能上比傳統模板引擎均有提升,下邊的介紹主要從語法、安全性和錯誤處理,以及如何使用這幾個方面介紹下Juicer.
a. 語法
詳細的語法請參考 Juicer Docs.
b. 安全性
安全性,簡單地說就是對輸出數據在輸出前進行一次轉義過濾,避免XSS這樣的腳本注入攻擊,簡單掃下盲,舉個XSS的例子。
var json={ output:'alert("XSS");' };
如果JSON數據是第三方接口返回或者含有用戶輸入(像BBS、評價)的內容,我們如果赤裸裸的將output寫到頁面上就會執行惡意的js代碼,所以Juicer默認是對數據輸出做了安全轉義的,當然如果不想被轉義,可以使用$${varname}。
juicer.to_html('${output}',json); //輸出:<script>alert("XSS");</script>juicer.to_html('$${output}',json); //輸出: </pre>
c. 錯誤處理
如果沒有錯誤處理,當模板引擎編譯(Compile)或者渲染(Render)出錯時候就會引起后續js代碼停止執行,可想而知,如果因為一個逗號或者JSON數據的偶發錯誤導致整個頁面掛掉,是我們不能接受的。但是Juicer在遇到這些錯誤的時候不會影響后續代碼的執行,只會在控制臺打出一句警告(Warn)告知開發者模板解析出現錯誤。
juicer.to_html('${varname,,,,,,,}',json); alert('hello, juicer!');執行上邊的代碼就會看到控制臺打出的“Juicer Compile Exception: Unexpected token ,”,但是不會因為錯誤導致后續的alert被阻塞掉。
實現原理
Juicer對一個模板的編譯和渲染的過程主要有以下幾個步驟:
- 1、對模板代碼進行語法分析
- 2、分析后生成原生的Javascript代碼字符串
- 3、將生成的代碼轉為可重用的Function (Compiled Template) </ul>
var json={ list:[ {name:"benben"}, {name:"liuhuo"} ] }; var tpl='{@each data.list as value,key}$${value.name}{@/each}'; var compiled_tpl=juicer.compile(tpl,{errorhandling:false});
我們通過compiled_tpl.render.toString()看下編譯后的代碼:
function anonymous(data) { var data = data || {}; var out = ''; out += ''; for (var i0 = 0, l = data.list.length; i0 < l; i0++) { var value = data.list[i0]; var key = i0; out += ''; out += ((value.name)); out += ''; } out += ''; return out; }
是不是已經明白了Juicer的原理?這個編譯后的函數就會每次幫我們完成從數據到html代碼的拼裝操作。
這里有幾點優化的地方值得分享下:
- 1、using += instead of array.push
- 2、avoid using with {}
- 3、cache the compiled template (function) </ul>
這幾點優化在大數據量循環渲染時候性能提升顯著,不過正因為放棄了with{}語句,所以JSON數據外層必須指定“data.”前綴,如果你覺得這點性能的提升不重要,也可以在options中指定loose:true(松散模式),這樣就可以省去data. 前綴。
最后介紹下Options配置項,左側為參數默認值。
{ cache:true/false, loose:false/true, errorhandling:true/false }
cache默認為true,即同一個模板編譯后是否被juicer緩存,也就是說如果緩存開啟的情況下,同一個模板第一次編譯后,為了縮短耗時提升性能,后續不會再次執行編譯的操作而是直接從緩存中去取編譯好的模板函數。
Juicer的API. Juicer有兩種使用方法,一種是通過
juicer.to_html(tpl,data,options);
直接根據提供的數據將模板轉為html代碼,另一種是通過compile方法先將模板編譯好,在需要的時候再對模板進行數據的Render操作:
var compiled_tpl=juicer.compile(tpl,options); compiled_tpl.render(data);
最后附上Juicer的項目主頁,上邊有詳細的文檔和Demo代碼。
http://juicer.name