前言
underscore 提供了模板引擎的功能,舉個例子:
var tpl = "hello: <%= name %>";
var compiled = _.template(tpl);
compiled({name: 'Kevin'}); // "hello: Kevin"
感覺好像沒有什麼強大的地方,再來舉個例子:
在 HTML 文件中:
<ul id="name_list"></ul>
<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>
JavaScript 文件中:
var container = document.getElementById("user_tmpl");
var data = {
users: [
{ "name": "Kevin", "url": "http://localhost" },
{ "name": "Daisy", "url": "http://localhost" },
{ "name": "Kelly", "url": "http://localhost" }
]
}
var precompile = _.template(document.getElementById("user_tmpl").innerHTML);
var html = precompile(data);
container.innerHTML = html;
效果為:
那麼該如何實現這樣一個 _.template 函數呢?
實現思路
underscore 的 template 函數參考了 jQuery 的作者 John Resig 在 2008 年發表的一篇文章 JavaScript Micro-Templating,我們先從這篇文章的思路出發,思考一下如何寫一個簡單的模板引擎。
依然是以這段模板字符串為例:
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
John Resig 的思路是將這段代碼轉換為這樣一段程序:
// 模擬數據
var users = [{"name": "Kevin", "url": "http://localhost"}];
var p = [];
for (var i = 0; i < users.length; i++) {
p.push('<li><a href="');
p.push(users[i].url);
p.push('">');
p.push(users[i].name);
p.push('</a></li>');
}
// 最後 join 一下就可以得到最終拼接好的模板字符串
console.log(p.join('')) // <li><a href="http://localhost">Kevin</a></li>
我們注意,模板其實是一段字符串,我們怎麼根據一段字符串生成一段代碼呢?很容易就想到用 eval,那我們就先用 eval 吧。
然後我們會發現,為了轉換成這樣一段代碼,我們需要將<%xxx%>轉換為 xxx,其實就是去掉包裹的符號,還要將 <%=xxx%>轉化成 p.push(xxx),這些都可以用正則實現,但是我們還需要寫 p.push('<li><a href="'); 、p.push('">');吶,這些該如何實現呢?
那我們換個思路,依然是用正則,但是我們
- 將
%>替換成p.push(' - 將
<%替換成'); - 將
<%=xxx%>替換成');p.push(xxx);p.push('
我們來舉個例子:
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
按照這個替換規則會被替換為:
');for ( var i = 0; i < users.length; i++ ) { p.push('
<li>
<a href="');p.push(users[i].url);p.push('">
');p.push(users[i].name);p.push('
</a>
</li>
'); } p.push('
這樣肯定會報錯,畢竟代碼都沒有寫全,我們在首和尾加上部分代碼,變成:
// 添加的首部代碼
var p = []; p.push('
');for ( var i = 0; i < users.length; i++ ) { p.push('
<li>
<a href="');p.push(users[i].url);p.push('">
');p.push(users[i].name);p.push('
</a>
</li>
'); } p.push('
// 添加的尾部代碼
');
我們整理下這段代碼:
var p = []; p.push('');
for ( var i = 0; i < users.length; i++ ) {
p.push('<li><a href="');
p.push(users[i].url);
p.push('">');
p.push(users[i].name);
p.push('</a></li>');
}
p.push('');
恰好可以實現這個功能,不過還要注意一點,要將換行符替換成空格,防止解析成代碼的時候報錯,不過在這裏為了方便理解原理,就只在代碼裏實現。
第一版
我們來嘗試實現第一版:
// 第一版
function tmpl(str, data) {
var str = document.getElementById(str).innerHTML;
var string = "var p = []; p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g,"p.push('")
+ "');"
eval(string)
return p.join('');
};
為了驗證是否有用:
HTML 文件:
<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>
JavaScript 文件:
var users = [
{ "name": "Byron", "url": "http://localhost" },
{ "name": "Casper", "url": "http://localhost" },
{ "name": "Frank", "url": "http://localhost" }
]
tmpl("user_tmpl", users)
完整的 Demo 可以查看 template 示例一
Function
在這裏我們使用了 eval ,實際上 John Resig 在文章中使用的是 Function 構造函數。
Function 構造函數創建一個新的 Function 對象。 在 JavaScript 中, 每個函數實際上都是一個 Function 對象。
使用方法為:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
arg1, arg2, ... argN 表示函數用到的參數,functionBody 表示一個含有包括函數定義的 JavaScript 語句的字符串。
舉個例子:
var adder = new Function("a", "b", "return a + b");
adder(2, 6); // 8
那麼 John Resig 到底是如何實現的呢?
第二版
使用 Function 構造函數:
// 第二版
function tmpl(str, data) {
var str = document.getElementById(str).innerHTML;
var fn = new Function("obj",
"var p = []; p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g,"p.push('")
+ "');return p.join('');");
return fn(data);
};
使用方法依然跟第一版相同,具體 Demo 可以查看 template 示例二
不過值得注意的是:其實 tmpl 函數沒有必要傳入 data 參數,也沒有必要在最後 return 的時候,傳入 data 參數,即使你把這兩個參數都去掉,代碼還是可以正常執行的。
這是因為:
使用Function構造器生成的函數,並不會在創建它們的上下文中創建閉包;它們一般在全局作用域中被創建。當運行這些函數的時候,它們只能訪問自己的本地變量和全局變量,不能訪問Function構造器被調用生成的上下文的作用域。這和使用帶有函數表達式代碼的 eval 不同。
這裏之所以依然傳入了 data 參數,是為了下一版做準備。
with
現在有一個小問題,就是實際上我們傳入的數據結構可能比較複雜,比如:
var data = {
status: 200,
name: 'kevin',
friends: [...]
}
如果我們將這個數據結構傳入 tmpl 函數中,在模板字符串中,如果要用到某個數據,總是需要使用 data.name、data.friends 的形式來獲取,麻煩就麻煩在我想直接使用 name、friends 等變量,而不是繁瑣的使用 data. 來獲取。
這又該如何實現的呢?答案是 with。
with 語句可以擴展一個語句的作用域鏈(scope chain)。當需要多次訪問一個對象的時候,可以使用 with 做簡化。比如:
var hostName = location.hostname;
var url = location.href;
// 使用 with
with(location){
var hostname = hostname;
var url = href;
}
function Person(){
this.name = 'Kevin';
this.age = '18';
}
var person = new Person();
with(person) {
console.log('my name is ' + name + ', age is ' + age + '.')
}
// my name is Kevin, age is 18.
最後:不建議使用 with 語句,因為它可能是混淆錯誤和兼容性問題的根源,除此之外,也會造成性能低下
第三版
使用 with ,我們再寫一版代碼:
// 第三版
function tmpl(str, data) {
var str = document.getElementById(str).innerHTML;
var fn = new Function("obj",
// 其實就是這裏多添加了一句 with(obj){...}
"var p = []; with(obj){p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g,"p.push('")
+ "');}return p.join('');");
return fn(data);
};
具體 Demo 可以查看 template 示例三
第四版
如果我們的模板不變,數據卻發生了變化,如果使用我們的之前寫的 tmpl 函數,每次都會 new Function,這其實是沒有必要的,如果我們能在使用 tmpl 的時候,返回一個函數,然後使用該函數,傳入不同的數據,只根據數據不同渲染不同的 html 字符串,就可以避免這種無謂的損失。
// 第四版
function tmpl(str, data) {
var str = document.getElementById(str).innerHTML;
var fn = new Function("obj",
"var p = []; with(obj){p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g,"p.push('")
+ "');}return p.join('');");
var template = function(data) {
return fn.call(this, data)
}
return template;
};
// 使用時
var compiled = tmpl("user_tmpl");
results.innerHTML = compiled(data);
具體 Demo 可以查看 template 示例四
下期預告
至此,我們已經跟着 jQuery 的作者 John Resig 實現了一個簡單的模板引擎,雖然 underscore 基於這個思路實現,但是功能強大,相對的,代碼也更加複雜一下,下一篇,我們一起去分析 underscore 的 template 函數實現。
underscore 系列
underscore 系列目錄地址:https://github.com/mqyqingfeng/Blog。
underscore 系列預計寫八篇左右,重點介紹 underscore 中的代碼架構、鏈式調用、內部函數、模板引擎等內容,旨在幫助大家閲讀源碼,以及寫出自己的 undercore。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啓發,歡迎 star,對作者也是一種鼓勵。