浅谈模板引擎

模板原理

模板的诞生是为了将显示与数据分离,模板技术多种多样,但其本质是将模板文件和数据通过模板引擎生成最终的HTML代码。
浅谈模板引擎
模板技术并不是什么神秘技术,干的是拼接字符串的体力活。模板引擎就是利用正则表达式识别模板标识,并利用数据替换其中的标识符。比如:

Hello, <%= name%>

数据是{name: '木的树'},那么通过模板引擎解析后,我们希望得到Hello, 木的树。模板的前半部分是普通字符串,后半部分是模板标识,我们需要将其中的标识符替换为表达式。模板的渲染过程如下:
浅谈模板引擎

//字符串替换的思想
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

模板编译

上述代码中有如下部分:

        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 + "+ '";//加入模板逻辑时要注意,保证拼接成正确的函数字符串
            })

仍然要注意其中的字符串闭合问题

模板引擎是一个系统的问题,复杂模板还支持模板嵌套,这里就不介绍了,希望此文能够抛砖引玉,让大火带来更好的干货!

上一篇:开源数据同步神器——canal


下一篇:node如何让一个端口同时支持https与http