淺談模板引擎

淺談模板引擎

模板原理

模板的誕生是爲了將顯示與數據分離,模板技術多種多樣,但其本質是將模板文件和數據通過模板引擎生成最終的HTML代碼。
ZWPF0M5W_CR_NEUY6H46__W
模板技術並不是什麼神祕技術,乾的是拼接字符串的體力活。模板引擎就是利用正則表達式識別模板標識,並利用數據替換其中的標識符。比如:

Hello, <%= name%>

數據是{name: '木的樹'},那麼通過模板引擎解析後,我們希望得到Hello, 木的樹。模板的前半部分是普通字符串,後半部分是模板標識,我們需要將其中的標識符替換爲表達式。模板的渲染過程如下:
7J8ICGIRY_4PH_0N_6COAXO

//字符串替換的思想
function tmpl(str, obj) {
    if (typeof str === 'string') {
        return str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
            var key = arguments[1];
            return obj[key];
        });
    }
}

var str = “Hello, <%= name%>”;
var obj = {name: “Lzz”};

模板引擎

引擎核心

上面我們演示是簡單的字符串替換,但對於模板引擎來說,要做的事情更復雜些。通常需要以下幾個步驟:

  • 利用正則表達式分解出普通字符串和模板標識符,<%=%>的正則表達式爲/<%=\s*([^%>]+)\s*%>/g.
  • 將模板標識符轉換成普通的語言表達式
  • 生成待執行語句
  • 將數據填入執行,生成最終的字符串

Demo代碼如下:

//編譯的思想
function tmpl(str, obj) {
    if (typeof str === 'string') {
        var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
            var key = arguments[1];
            return "' + obj." + key; // 在函數字符串中利用'包裹正常字符串
        });
    tm = <span class="hljs-string">"return '"</span> + tm; <span class="hljs-comment">//"'Hello' + obj.name"</span>
    <span class="hljs-keyword">var</span> compile = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Function</span>(<span class="hljs-string">'obj'</span>, tm);
    <span class="hljs-keyword">return</span> compile(obj);
}

}

var str = “Hello, <%= name%>”;
var obj = {name: “Lzz”}; // Hello, Lzz

模板編譯

上述代碼中有如下部分:

        tm = "return '" + tm; //"'Hello' + obj.name"
        var compile = new Function('obj', tm);

爲了能夠與數據一起執行生成字符串,我們需要將原始的模板字符串轉換成一個函數對象。這個過程稱爲模板編譯。模板編譯使用了new Function(), 這裏通過它創建了一個函數對象,語法如下:

new Function(arg1, arg2,..., functionbody)

Function()構造函數接受多個參數,最後一個參數作爲函數體的內容,其之前的參數全部作爲生成的新函數的參數。需要注意的是Function的參數全部是字符串類型,函數體部分對於字符串跟函數表達式一定要區分清楚,初學者往往在對函數體字符串中的普通字符串和表達式的拼接上犯錯。一定要將函數體字符串和內部字符串正確拼接,如:

new Function('obj', "return 'Hello,' + obj.name")

或者對其中的字符換使用\"

new Function('obj', 'strip', "var tmp = \"\"; with(obj){ tmp = '';for(var i = 0; i < 3; i++){ tmp+='name is ' + strip(name) +' ';} tmp+=''; } return tmp;")

模板編譯過程中每次都要利用Function重新生成一個函數,浪費CPU。爲此我們可以將函數緩存起來,代碼如下:

//模板預編譯
var tmpl = (function(){
    var cache = {};
    return function(str, obj){
        if (!typeof str === 'string') {
            return;
        }
        var compile = cache[str];
        if (!cache[str]) {
            var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + obj." + key;
            });
            tm = "return '" + tm; //"'Hello' + obj.name"
            compile = new Function('obj', tm);
            cache[str] = compile;
        }
        return compile(obj); //預編譯情況下應該返回compile函數
    }
}());
var str = "Hello, <%= name%>";
var obj = {name: "Lzz"};
tmpl(str, obj);

利用with

利用with我們可以不用把模板標識符轉換成obj.name,只需要保持name標識符即可。

// 利用with使得變量自己尋找對象, 找不到的視爲普通字符串
// 貌似return後面不能直接跟with
//模板預編譯
var tmpl = (function(){
    var cache = {};
    return function(str, obj){
        if (!typeof str === 'string') {
            return;
        }
        var compile = cache[str];
        if (!cache[str]) {
            var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + " + key;
            });
            tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "; } return tmp;"; //"'Hello' + obj.name"
            compile = new Function('obj', tm);
            cache[str] = compile;
        }
        return compile(obj); //預編譯情況下應該返回compile函數
    }
}());
var str = "Hello, <%= name%>";
var obj = {name: "LZZ"};
tmpl(str, obj);

XSS漏洞

如果上面的obj變成var obj = {name: "<script>alert(\"XSS\")</script>"};,那麼最終生成的結果就會變成:

"Hello, <script>alert("XSS")</script>"

爲此我們需要堵上這個漏洞,基本就是要將形成HTML標籤的字符轉換成安全的字符,這些字符通常是&, <, >, ", '。轉換函數如下:

    var strip = function(html) {
        return String(html)
        .replace(/&/g, '&amp;')//&
        .replace(/</g, '&lt;')//左尖號
        .replace(/>/g, '&gt;')//右尖號
        .replace(/"/g, '&quot;')//雙引號"
        .replace(/'/g, '&#039;');//IE下不支持&apos;'
    }

這樣下來,模板引擎應該變成這樣:

var tmpl = (function(){
    var cache = {};
    var strip = function(html) {
        return String(html)
        .replace(/&/g, '&amp;')//&
        .replace(/</g, '&lt;')//左尖號
        .replace(/>/g, '&gt;')//右尖號
        .replace(/"/g, '&quot;')//雙引號"
        .replace(/'/g, '&#039;');//IE下不支持&apos;'
    }
    return function(str, obj){
        if (!typeof str === 'string') {
            return;
        }
        var compile = cache[str];
        if (!cache[str]) {
            //var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
            //    var key = arguments[1];
            //    return "' + strip(" + key + ")";
            //});
            var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var code = arguments[1];
                return "' + strip(" + code + ")"; //利用escape包裹code
            }).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + " + key;
            });
            tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "; } return tmp;"; //"'Hello' + obj.name"
            compile = new Function('obj', 'strip', tm);
            cache[str] = compile;
        }
        return compile(obj, strip); //預編譯情況下應該返回compile函數
    }
}());

var str = “<%= name%>”;
var obj = {name: “<script>alert(“XSS”)</script>”};
tmpl(str, obj);

這時候我們得到如下結果:

"&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;"

模板邏輯

功能稍微強大的模板引擎,都允許在模板中添加一部分邏輯來控制頁面的最終渲染。如:

var str = "<%for(var i = 0; i < 3; i++){%>name is <%= name%> <%}%>";

這裏我們用<%%>代表邏輯代碼<%=%>代表模板中需要替換的標識符。我們的模板代碼變成了如下所示:

//模板邏輯
var tmpl = (function(){
    var cache = {};
    var strip = function(html) {
        return String(html)
        .replace(/&/g, '&amp;')//&
        .replace(/</g, '&lt;')//左尖號
        .replace(/>/g, '&gt;')//右尖號
        .replace(/"/g, '&quot;')//雙引號"
        .replace(/'/g, '&#039;');//IE下不支持&apos;'
    }
    return function(str, obj){debugger;
        if (!typeof str === 'string') {
            return;
        }
        var compile = cache[str];
        if (!cache[str]) {
            //var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
            //    var key = arguments[1];
            //    return "' + strip(" + key + ")";
            //});
            var tm = str.replace(/<%\s*([^=][^%>]*)\s*%>/g, function() {
                var key = arguments[1];
                return "';" + key + " tmp+='"; // 邏輯代碼需要一塊塊的拼接起來,爲的是拼接成一段合理的函數字符串傳遞給new Function
            }).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var code = arguments[1];
                return "' + strip(" + code + ") +'"; //利用escape包裹code ,加入模板邏輯時要注意,保證拼接成正確的函數字符串
            }).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + " + key + "+ '";//加入模板邏輯時要注意,保證拼接成正確的函數字符串
            });debugger;
            tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "'; } return tmp;"; //"'Hello' + obj.name"
            compile = new Function('obj', 'strip', tm);
            cache[str] = compile;
        }
        return compile(obj, strip); //預編譯情況下應該返回compile函數
    }
}());

var str = “<%for(var i = 0; i < 3; i++){%>name is <%= name%> <%}%>”;
var obj = {name: “<script>alert(“XSS”)</script>”};
tmpl(str, obj);

第一步,我們將模板中的邏輯表達式找出來,用的正則表達式是/<%\s*([^=][^%>]*)\s*%>/g

str.replace(/<%\s*([^=][^%>]*)\s*%>/g, function() {
                var key = arguments[1];
                return "';" + key + " tmp+='"; // 邏輯代碼需要一塊塊的拼接起來,爲的是拼接成一段合理的函數字符串傳遞給new Function
            })

注意在拼接時,爲了防止函數字符串中的字符串沒有閉合對表達式造成影響,我們在key前後都加了'保證其中的字符串閉合
第二步, 對可能存在的HTML標籤進行轉義

.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var code = arguments[1];
                return "' + strip(" + code + ") +'"; //利用escape包裹code ,加入模板邏輯時要注意,保證拼接成正確的函數字符串
            })

同樣需要注意前後的字符串閉合
第三步,像先前一樣處理模板標識符

.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
                var key = arguments[1];
                return "' + " + key + "+ '";//加入模板邏輯時要注意,保證拼接成正確的函數字符串
            })

仍然要注意其中的字符串閉合問題

模板引擎是一個系統的問題,複雜模板還支持模板嵌套,這裏就不介紹了,希望此文能夠拋磚引玉,讓大火帶來更好的乾貨!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章