淺談模板引擎

模板原理

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

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 = "return '" + tm; //"'Hello' + obj.name"
        var compile = new Function('obj', tm);
        return compile(obj);
    }
}

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

模板編譯

上述代碼中有以下部分:xss

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

爲了可以與數據一塊兒執行生成字符串,咱們須要將原始的模板字符串轉換成一個函數對象。這個過程稱爲模板編譯。模板編譯使用了new Function(), 這裏經過它建立了一個函數對象,語法以下:函數

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

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

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

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

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。爲此咱們能夠將函數緩存起來,代碼以下:htm

//模板預編譯
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 + "+ '";//加入模板邏輯時要注意,保證拼接成正確的函數字符串
            })

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

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

相關文章
相關標籤/搜索