模板解析引擎有很多,比如mustache.js等。在我們解析從服務端獲取的JSON數據的時候使用模板有很大的便利。下面我們分析一下Underscore.js的template()模板渲染函數。
官網API介紹
將 JavaScript 模板編譯爲可以用於頁面呈現的函數, 對於通過JSON數據源生成複雜的HTML並呈現出來的操作非常有用。 模板函數可以使用 <%= … %>插入變量, 也可以用<% … %>執行任意的 JavaScript 代碼。 如果您希望插入一個值, 並讓其進行HTML轉義,請使用<%- … %>。 當你要給模板函數賦值的時候,可以傳遞一個含有與模板對應屬性的data對象 。 如果您要寫一個一次性的, 您可以傳對象 data 作爲第二個參數給模板 template來直接呈現, 這樣頁面會立即呈現而不是返回一個模板函數. 參數 settings 是一個哈希表包含任何可以覆蓋的設置 _.templateSettings.
var compiled = _.template("hello: <%= name %>");
compiled({name: 'moe'});
=> "hello: moe"
var list = "<% _.each(people, function(name) { %> <li><%= name %></li> <% }); %>";
_.template(list, {people: ['moe', 'curly', 'larry']});
=> "<li>moe</li><li>curly</li><li>larry</li>"
var template = _.template("<b><%- value %></b>");
template({value: '<script>'});
=> "<b><script></b>"
您也可以在JavaScript代碼中使用 print. 有時候這會比使用 <%= ... %> 更方便.
var compiled = _.template("<% print('Hello ' + epithet); %>");
compiled({epithet: "stooge"});
=> "Hello stooge"
如果ERB式的分隔符您不喜歡, 您可以改變Underscore的模板設置, 使用別的符號來嵌入代碼. 定義一個 interpolate 正則表達式來逐字匹配 嵌入代碼的語句, 如果想插入轉義後的HTML代碼 則需要定義一個 escape 正則表達式來匹配, 還有一個 evaluate 正則表達式來匹配 您想要直接一次性執行程序而不需要任何返回值的語句. 您可以定義或省略這三個的任意一個. 例如, 要執行 Mustache.js 類型的模板:
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
var template = _.template("Hello {{ name }}!");
template({name: "Mustache"});
=> "Hello Mustache!"
默認的, template 通過 with 語句 來取得 data 所有的值. 當然, 您也可以在 variable 設置裏指定一個變量名. 這樣能顯著提升模板的渲染速度.
_.template("Using 'with': <%= data.answer %>", {answer: 'no'}, {variable: 'data'});
=> "Using 'with': no"
預編譯模板對調試不可重現的錯誤很有幫助. 這是因爲預編譯的模板可以提供錯誤的代碼行號和堆棧跟蹤, 有些模板在客戶端(瀏覽器)上是不能通過編譯的 在編譯好的模板函數上, 有 source 屬性可以提供簡單的預編譯功能.
<script>
JST.project = <%= _.template(jstText).source %>;
</script>
源碼解讀
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
// Underscore默認使用標籤格式的模板分隔符,改變下面的模板設置項可以使用你自己設置的模板分隔符。
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
// 當定製了'templateSettings'設置項後,如果你不想定義interpolation,evaluation或者escaping的正則,
// 我們需要一個保證在某個屬性項(evaluate,interpolate,escape)沒有的情況下的正則。
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a string literal.
// 對特定字符進行轉碼(前面會加上"\"),這樣放在Function的字符串字面量的函數體中才能正常運行(類似於正則中我們想要對\符號的匹配一樣)
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
// 獲取escapes屬性部分的匹配的正則
var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
// NB: `oldSettings` only exists for backwards compatibility.
// JavaScript mini模板引擎,類似於John Resig的實現。Underscore的模板可以處理任意的定界符,保留空格,並且可以在插入的代碼里正確的轉義引號。
// 注意:'oldSetting'的存在只是爲了向後兼容。
// Underscore模板解析流程:
// 1、準備要對整個字符串進行匹配的正則表達式;
// 2、組裝要執行的函數體主要部分(source變量,通過對整個模板進行正則匹配來實現);
// 3、組裝整個函數體執行部分;
// 4、使用Function實例化出一個生成最終字符串的函數(對該函數傳入要渲染的參數即可獲得最終渲染字符串);
// 5、提供預編譯的source參數,方便調試與錯誤追蹤
_.template = function(text, settings, oldSettings) {
if (!settings && oldSettings) settings = oldSettings;
// 使用defaults方法來給settings參數賦默認值(如果evaluate、interpolate、escape任一屬性有值則不做覆蓋)
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
// 將界定符組合成一個正則
// 用戶如果沒有設置界定符則以下正則是:/<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
var matcher = RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
// 記錄編譯成的函數字符串,可通過_.template(tpl).source獲取
var source = "__p+='";
/**
replace()函數的各項參數意義:
1、第一個參數爲每次匹配的全文本($&)。
2、中間參數爲子表達式匹配字符串,也就是括號中的東西,個數不限
3、倒數第二個參數爲匹配文本字符串的匹配下標位置。
4、最後一個參數表示字符串本身。
*/
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
// 將要編譯的模板中正則匹配到非分解符部分的加到source上面去,這裏做了字符串轉義處理
source += text.slice(index, offset).replace(escaper, escapeChar);
// 將index跳至當前匹配分解符的結束的地方
index = offset + match.length;
// 界定符內匹配到的內容(TODO:進一步解釋)
if (escape) {
// 需要轉碼的字符串部分的處理
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
// 對象屬性部分的處理
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
// 代碼執行部分的處理
source += "';\n" + evaluate + "\n__p+='";
}
// Adobe VMs need the match returned to produce the correct offest.
// 將匹配到的內容原樣返回(Adobe VMs需要返回match來使得offset能夠正常,一般網頁並不需要)
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
// 如果沒有在第二個參數裏指定variable變量,那麼將數據值置於局部變量中執行
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
// 將組裝好的source重新組裝,成爲真正可以執行的js代碼字符串。(print相當於等號,但是比=號要方便)
// Array.prototype.join.call(arguments,'');是將所有的參數(如果是對象則調用toString()方法轉化爲字符串)以''合併在一起
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
// 防止在沒有傳settings.variable作爲with的作用域的時候,render函數的第一個參數名字爲obj(此時render函數格式:function(obj,_) {source}),
// obj爲在沒有傳遞setting.variable的時候source代碼的作用域
try {
// underscore的根對象也作爲一個變量傳入了函數
// Function傳參:前面是執行函數時的參數,最後是執行函數體字符串字面量
var render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
// 傳進去的data相當於obj
var template = function(data) {
// this一般都是指向window
return render.call(this, data, _);
};
// Provide the compiled source as a convenience for precompilation.
// 提供編譯的source,方便預編譯(據官方文檔,這麼做可以對錯誤進行跟蹤定位)
var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
// 將函數返回(對函數傳入要渲染的數據即可獲得最終渲染字符串)
return template;
};