javascript模板引擎道理

本文同步自我的博客园:http://www.cnblogs.com/hustskyking/

一、媒介

什么是模板引擎,说的简朴点,就是一个字符串中有几个变量待定。比方:

var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';

经由历程模板引擎函数把数据塞进去,

var data = {
    "name": "Barret Lee",
    "age": "20"
};

var result = tplEngine(tpl, data);
//Hei, my name is Barret Lee, and I'm 20 years old.

那这玩艺儿有什么作用呢?实在他就是一个预处置惩罚器(preprocessor),搞php开辟的童鞋对Smarty必定是异常熟习,Smarty是一个php模板引擎,tpl中待处置惩罚的字符经由历程数据婚配然后输出响应的html代码,加上比较给力的缓存手艺,其速率和易用性是异常给力的!JS Template也是一样的,我们的数据库里保留着数以万万计的数据,而每一条数据都是经由历程同一种体式格局输入,就拿上面的例子来讲,我们不可能在数据库里存几千条”Hei, my name…”,而是只保留对应的name和age,经由历程模板输出效果。

JS模板引擎应当做哪些事变?看看下面一串代码:

var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' + 
    'var post = posts[i]; %>' +
    '<% if(!post.expert){ %>' +
        '<span>post is null</span>' +
    '<% } else { %>' +
        '<a href="#"><% post.expert %> at <% post.time %></a>' +
    '<% } %>' +
'<% } %>';

一个基础的模板引擎最少能够保证上面的代码能够平常剖析。如送入的数据是:

var data = {
    "posts": [{
        "expert": "content 1",
        "time": "yesterday"
    },{
        "expert": "content 2",
        "time": "today"
    },{
        "expert": "content 3",
        "time": "tomorrow"
    },{
        "expert": "",
        "time": "eee"
    }]
};

能够输出:

<a href="#">content 1 at yesterday</a>
<a href="#">content 2 at today</a>
<a href="#">content 3 at tomorrow</a>
<span>post is null</span>

下面就详细说说这个模板引擎的道理是啥样的。

二、JS模板引擎的完成道理

1.正则抠出要婚配的内容

针对这一串代码,经由历程正则猎取内容

var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var data = {
    "name": "Barret Lee",
    "age": "20"
};

最简朴的体式格局就是经由历程replace函数了:

var result = tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
    return data[s1];
});

经由历程正则替代,我们很轻松的拿到了result,你能够去试一试,他正式我们想要的效果。然则这里又有了一个题目,改一下data和tpl,

var tpl = 'Hei, my name is <%name%>, and I\'m <%info.age%> years old.';
var data = {
    "name": "Barret Lee",
    "info": { age": "20"}
};

再用上面的体式格局去猎取效果,呵呵,不可了吧~ 这里data[“info.age”]自身就是undefined,所以我们须要换一种体式格局来处置惩罚这个题目,那就是将它转换成真正的JS代码。如:

return 'Hei, my name is ' + data.name + ', and I\'m ' + data.info.age' + ' years old.'

然则接着又有一个题目来了,当我们的代码中涌现for轮回和if的时刻,上面的转换显著是不起作用的,如:

var tpl = 'Posts: ' + 
          '<% for(var i = 0; i < post.length; i++) {'+
            '<a href="#"><% post[i].expert %></a>' + 
          '<% } %>'

假如继承采纳上面的体式格局,获得的效果就是:

return 'Posts: ' + 
       for(var i = 0; i < post.length; i++) { +
         '<a href="#">' + post[i].exper + '</a>' +
       }

这明显不是我们缘由看到的,轻微视察一下上面的组织,假如能够返回一个如许的效果也挺不错哦:

'Posts: ' 
for(var i = 0; i < post.length; i++) {
    '<a href="#">' + post[i].exper + '</a>'
}

然则我们须要获得的是一个字符串,而不是上面如许零星的片断,因而能够把这些东西装入数组中。

2.装入数组

var r = [];
r.push('Posts: ' );
r.push(for(var i = 0; i < post.length; i++) {);
r.push('<a href="#">');
r.push(post[i].exper);
r.push('</a>');
r.push(});

有人看到上面的代码就要笑了,第三行和末了一行代码的逻辑显著是不正确的嘛,那肿么办呢?呵呵,很简朴,不放进去就好了呗,

var r = [];
r.push('Posts: ' );
for(var i = 0; i < post.length; i++) {
    r.push('<a href="#">');
    r.push(post[i].exper);
    r.push('</a>');
}

如许的逻辑就异常完美了,不存在太多的破绽,然则这个转化的历程是怎样完成的?我们必需照样要写一个剖析的模板函数出来。

3.区分js逻辑部份

var r = [];
tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
    //完蛋了,这里貌似又要回到上面那好笑的逻辑有毛病的一步啦... 该怎样处置惩罚比较好?
});

完蛋了,这里貌似又要回到上面那好笑的逻辑有毛病的一步啦… 该怎样处置惩罚比较好?我们晓得,JS给我们供应了组织函数的“类”,

var fn = new Function("data", 
    "var r = []; for(var i in data){ r.push(data[i]); } return r.join(' ')");
fn({"name": "barretlee", "age": "20"}); // barretlee 20

晓得了这个就好办了,我们能够把逻辑部份和非逻辑部份的代码链接成一个字符串,然后运用相似fn的函数直接编译代码。而/<%([^%>]+)?%>/g,这一个正则只能把逻辑部份婚配出来,要想把一切的代码都组合到一同,必需还得婚配非逻辑部份代码。replace函数虽然很壮大,他也能够完成这个使命,然则完成的逻辑比较艰涩,所以我们换别的一种体式格局来处置惩罚。

先看一个简朴的例子:

var reg = /<%([^%>]+)?%>/g;
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var match = reg.exec(tpl);
console.log(match);

看到的是:

[
    0: "<%name%>",
    1: name,
    index: 16,
    input: "Hei, my name is <%name%>, and I'm <%age%> years old."
    length: 2
]

这。。。我们但是想获得一切的婚配啊,他居然只猎取了name而疏忽了背面的age,好吧,对正则轻微熟习点的童鞋一定会晓得应当如许处置惩罚:

var reg = /<%([^%>]+)?%>/g;
while(match = reg.exec(tpl)) {
    console.log(match);
}

关于正则表达式的内容就不在这里细说了,有兴致的同砚能够多去了解下match,exec,search等正则的相干函数。这里主如果靠match的index属性来定位遍历位置,然后运用while轮回猎取一切的内容。

4.引擎函数

所以我们的引擎函数雏形差不多就出来了:

var tplEngine = function(tpl, data){
    var reg = /<%([^%>]+)?%>/g,
            code = 'var r=[];\n',
            cursor = 0;  //重要的作用是定位代码末了一截
    var add = function(line) {
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
    };

    while(match = reg.exec(tpl)) {
        add(tpl.slice(cursor, match.index)); //增加非逻辑部份
        add(match[1]);  //增加逻辑部份 match[0] = "<%" + match[1] + "%>";
        cursor = match.index + match[0].length;
    }

    add(tpl.substr(cursor, tpl.length - cursor)); //代码的末了一截 如:" years old."

    code += 'return r.join("");'; // 返回效果,在这里我们就拿到了装入数组后的代码
    console.log(code);

    return tpl;
};

如许一来,测试一个小demo:

 var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' + 
        'var post = posts[i]; %>' +
        '<% if(!post.expert){ %>' +
            '<span>post is null</span>' +
        '<% } else { %>' +
            '<a href="#"><% post.expert %> at <% post.time %></a>' +
        '<% } %>' +
    '<% } %>';
tplEngine(tpl, data);

返回的效果让人很惬意:

var r=[];
r.push("");
r.push(" for(var i = 0; i < this.posts.length; i++) {var post = posts[i]; ");
r.push("");
r.push(" if(!post.expert){ ");
r.push("<span>post is null</span>");
r.push(" } else { ");
r.push("<a href=\"#\">");
r.push(" post.expert ");
r.push(" at ");
r.push(" post.time ");
r.push("</a>");
r.push(" } ");
r.push("");
r.push(" } ");
r.push("");
return r.join(""); 

不过我们并须要for,if,switch等这些东西也push到r数组中去,所以呢,还得改良下上面的代码,假如在line中发现了包括js逻辑的代码,我们就不应当让他进门:

regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var add = function(line, js) {
    js? code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n' :
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};

所以我们只剩下末了一步工作了,把data扔进去!

5.把data扔进去

没有比完成这东西更简朴的事变啦,经由历程上面临Function这个函数的解说,人人应当也晓得怎样做了。

return new Function(code).apply(data);

运用apply的作用就是让code中的一些变量作用域绑定到data上,不然作用域就会跑到global上,如许获得的数据索引就会出题目啦~ 固然我们能够再优化一下:

return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);

把回车换行以及tab键都给婚配掉,让代码越发清洁一点。那末终究的代码就是:

var tplEngine = function(tpl, data) {
    var reg = /<%([^%>]+)?%>/g, 
        regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, 
        code = 'var r=[];\n', 
        cursor = 0;

    var add = function(line, js) {
        js? (code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n') :
            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
        return add;
    }
    while(match = reg.exec(tpl)) {
        add(tpl.slice(cursor, match.index))(match[1], true);
        cursor = match.index + match[0].length;
    }
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
};

三、运用场景

毕竟是前端代码,所以写出来是要为前端效劳的,日常平凡我们处置惩罚的平常是一个html的模板,一般的状况下,模板代码是放在script标签或许textarea中,所以首先是要猎取到这里头的东西,然后再来做剖析。

var barretTpl = function(str, data){

    //猎取元素
    var element = document.getElementById(str);
    if (element) {
        //textarea或input则取value,别的状况取innerHTML
        var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;
        return tplEngine(html, data);
    } else {
        //是模板字符串,则天生一个函数
        //假如直接传入字符串作为模板,则可能变化过量,因而不斟酌缓存
        return tplEngine(str, data);
    }
    var tplEngine = function(tpl, data) {
        // content above
    };
};

如许一来就越发简朴了,运用体式格局就是 barretTpl(str, data), 这里的str能够是模板代码,也能够是一个DOM元素的id~
能够看看这两段代码:https://gist.github.com/barretlee/7765698, https://gist.github.com/barretlee/7765587

也能够戳这个demo

四、优化以及功用拓展

统共就三四十行代码,完成的东西肯定是一个简约版的,不过关于一个简朴的页面而言,这几行代码已充足运用了,假如还想对他做优化,能够从这几个方面斟酌:

  • 优化猎取的模板代码,比方去掉行尾空格等
  • 标记转义,假如我们想输出<span>hehe</span>相似如许的源代码,在push之前必需举行转义
  • 代码缓存,假如一个模板会常常运用,能够将它用一个数组缓存在barretTpl闭包内
  • 用户本身设置分隔符

五、参考资料

    原文作者:小胡子哥
    原文地址: https://segmentfault.com/a/1190000000426283
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞