
先推薦一個我自己寫的模板引擎catpl,項目地址:https://github.com/liyu365/catpl。
項目中有詳細的注釋,代碼易讀,本項目借鑒自artTemplate、juicer、laytpl等諸多項目,在學習這些項目的過程中編寫而成,并遵守以下幾個步驟,可以對照此項目代碼進行了解。
下面就是總結的編寫javascript模板引擎的幾個步驟
例如一個模板為:
<h2><%= title %></h2>
<ul>
<% for(var i = 0 ; i < sports.length ; i++ ) { %>
<li><% i + 1 %> - <% sports[i].name %></li>
<% } %>
</ul>
最終會編譯成為一個函數:
function anonymous($data, $methods) {
var title = $data.title, sports = $data.sports, $escape = $methods.$escape;
var $out = '';
$out += '<h2>';
$out += $escape(title);
$out += '</h2><ul>';
for(var i = 0 ; i < sports.length ; i++ ) {
$out += '<li>';
$out += $escape(i + 1);
$out += ' - ';
$out += $escape(sports[i].name);
$out += '</li>';
}
$out += '</ul>';
return $out;
}
可以觀察到模板中的所有的變量名都被指定成了參數$data對象的屬性,并且該函數自始至終只做了一件事,就是構建$out字符串,并將其返回。下面來梳理一下將模板編譯為函數的過程:
步驟1. 確立模板中邏輯表達式的openTag和closeTag
在上面的例子中<%
和%>
就是openTag和closeTag。
任何的HTML模板都包括兩個部分:
- HTML語句
- 邏輯表達式語句
openTag和closeTag的作用就是劃分出這兩個部分區別對待。<%
和%>
包裹著的為邏輯表達式語句,在其之外的是普通的HTML語句。
步驟2. 拆分"HTML語句"和"邏輯表達式語句"
methods.$foreach(source.split(options.openTag), function (code) {
var arr = code.split(options.closeTag);
if (arr.length === 1) {
code_body += handle_html(arr[0]);
} else {
code_body += handle_logic(arr[0]);
if (arr[1]) {
code_body += handle_html(arr[1]);
}
}
});
可以看到這一步利用openTag和closeTag把模板源碼中的HTML語句傳給了handle_html()
函數,把邏輯表達式語句傳給了handle_logic()
函數。這兩個函數會把返回的代碼拼接給一個統一的變量。
步驟3. 處理模板中的"HTML語句"
對應上面提到的handle_html()
函數。
這一步很好理解,只要原樣輸出就好。
步驟4. 處理模板中的"邏輯表達式語句"
對應上面提到的handle_logic()
函數,這一步的核心就是分析邏輯表達式語句中的變量名,函數名,識別的方式很簡單,只要不是js中的關鍵字、保留字都視為是一個變量名。
邏輯表達式語句又分為兩種:
1)賦值語句,形如<%= title %>
。
直接把title視為是一個變量名,在函數的開始處聲明title為$data.title的引用。然后對title這個字符串原樣輸出。因為$data.title對應的值可能帶有html中的特殊字符,所以在title外面包裹了$escape函數的調用。$escape函數是一個我們預先編寫好的函數,存放在methods對象中,然后傳和data一樣傳入到編譯函數中。
- 邏輯控制語句,形如for(var i = 0 ; i < sports.length ; i++ ) {
依然要分析變量名,例如這里的sports,注意分析變量名時一定要忽略.length這樣的對象屬性。
步驟5.生成編譯函數
利用Function
構造函數生成一個函數:
var fun = new Function('$data', '$method', code);
這里的code當然就是handle_html()
和handle_logic()
兩個方法翻譯后的標準js代碼拼接而成的。
fun函數還需要進一步封裝,畢竟$method還沒有值。第四部已經提到,methods其實是我們模板引擎內部的對象,存放一些模板引擎的工具方法,我們應該自己把它傳給編譯好的函數。其實給模板引擎注冊helper方法也是一樣的,需要我們自己把helper對象傳進編譯函數。包裝代碼如下:
return function (data) {
return fun(data, methods, helpers);
};
此時返回的函數就是用戶實際拿到的函數。
步驟6.進一步優化"邏輯表達式語句"的語法
其實做到第五步已經是一個具備基本功能的模板引擎了,但是模板中邏輯表達式不夠簡潔,例如:
<% for(var i = 0 ; i < sports.length ; i++ ) { %>
這種表達式其實還是原生的js語法,我們可以自己定義模板引擎的語法,例如:
<% foreach sports %>
這其實不難做到,只是在第四部開始之前利用正則把foreach sports
再變為for(var i = 0 ; i < sports.length ; i++ ) {
,之后還是一樣的流程。
catpl項目中有一個options.syntax_hook
方法就是用來定義自己的模板語法的,可以將options.syntax_hook的值設為null來在自定義語法和原生js語法之間切換。