underscore 系列之實現一個模板引擎(上)

冴羽發表於2019-03-03

前言

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(`">`);吶,這些該如何實現呢?

那我們換個思路,依然是用正則,但是我們

  1. %> 替換成 p.push(`
  2. <% 替換成 `);
  3. <%=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(/[
	
]/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(/[
	
]/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.namedata.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(/[
	
]/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(/[
	
]/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 系列目錄地址:github.com/mqyqingfeng…

underscore 系列預計寫八篇左右,重點介紹 underscore 中的程式碼架構、鏈式呼叫、內部函式、模板引擎等內容,旨在幫助大家閱讀原始碼,以及寫出自己的 undercore。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章