浅谈模板引擎

模板原理

模板的诞生是为了将显示与数据分离,模板技术多种多样,但其本质是将模板文件和数据经过模板引擎生成最终的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 + "+ '";//加入模板逻辑时要注意,保证拼接成正确的函数字符串
            })

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

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

相关文章
相关标签/搜索