一步一步实现一个前端模板引擎

不要重复发明轮子,这是我听到最多的一句话,而且现在有很多优秀的模板引擎:handlebar、ejs、artTemplate...那么为什么还要自己实现一个呢?原因不外乎有两个,
一来是手痒,二来是满足一点小小的虚荣心:看,模板引擎我也会,简单!感觉非常优(zhuang)秀(bi)。

既然是自己动手,那么网上的教程肯定先放一边,突然有点耗子啃南瓜——无从下口的感觉...

一切从需求出发

从后台拿到数据,拼接成字符串放在页面中,这是我们初入前端时常要做的工作,特别是遇到结构稍微复杂的页面,光拼接字符串都能搞得你一脸懵逼、二脸懵逼,终于
有一天遇到模板引擎,一边惊为天人,一边暗自骂自己傻逼。那么,今天我们动手实现的模板引擎,就从那最初的那一天开始吧!

字符串模板

话说有天接到需求,需要将一组JSON数据,渲染到页面中。如下所示:

var data = [
    { text: 'text1' ,status:'done' },
    { text: 'text2' ,status:'pending' }
];

var tpl = '<ul>'+
    '<%for(var i = 0, len = data.length; i < len; i++) {%>'+
    '<li class="<%= data[i].status%>"><span><%= data[i].text%></span></li> '+
    '<%}%>'+
    '</ul>';

最初的渲染函数

机智如我自然想到用函数来循环。。。

var render = function(data) {
    var tmp = '';
    tmp += '<ul>';
    for(var i = 0, len = data.length; i < len; i++) {
        tmp += '<li class="'+ data[i].status +'">'+data[i].text+'</li>';
    }
    tmp += '</ul>';
    return tmp;
};

目前来讲,我们返回了渲染好的字符串,而且看来工作的很顺利。但如果将字符串增加点内容,这个函数就GG思密达了。由此看来,我们需要把字符串模板单独提取出来,然后再
进行数据渲染。

牛B的Function

我们用的最多的就是 function 关键字了,但对于 function 的爸爸 Function 却有点陌生,那么 Function 究竟哪里流弊呢?红宝石书不是建议我们不要用 Function吗?
其实,在JS中,但我们使用 function 声明函数的时候,JS会自动调用 Function 来生成实例。并且,Function 为我们提供了更强大的武器——动态函数。

语法
var function_name = new Function(arg1, arg2, ..., argN, function_body)

等同于

var function_name = function(arg1,..., argN) {function_body}

于是,我们就有了一把强力的武器,将动态的字符串,放在动态的函数中执行了。

提取字符串构造函数体

有了前面的知识基础,这一步,我们就要把 tpl 中的字符串,变成 render 的函数体。这就需要另一把武器——正则表达式。利用它,来找到需要渲染数据的位置。

var reg = /<%([\s\S]+?)%>/g

然后,通过 replace 方法替换 reg 找到的位置,构造成函数体!

var template = function(tpl) {
    var reg = /<%([\s\S]+?)%>/g;
    // index 用来记录替换的位置
    var index = 0;
    // 需要构造的函数体(一步一步和上面的render函数对比)
    var func_body = "var tmp = '';";
    func_body += "tmp += '";

    tpl.replace(reg, function(match, val, offset, str){
        // 每一次匹配到后,截取当前匹配位置和上一次匹配完成后位置之间的字符串
        func_body += tpl.substring(index, offset);

        // 根据 %= 判断如何进行拼接函数体
        if(match.indexOf('%=') < 0) {
            func_body +="';" + val + ";tmp += '";
        } else {
            func_body += "' + " + val.replace('=', '').trim() + "+'";
        }

        // 完成一次match,改变index 的值
        index = offset + match.length;
        return index;
    });

    // 完成所有匹配后,将剩下的字符串加入
    func_body += tpl.substring(index);
    // 返回 tmp
    func_body += "';return tmp;";
    return func_body;
};

现在,只要我们调用 template 函数,就会返回如 render 的函数体类似的字符串。要使template 函数返回的字符串运行起来,就要用到 Function 了。

var tmpEngine = function (tpl, data) {
    // 返回字符串函数体
    var func_body = template(tpl);
    // 通过 Function 运行
    return new Function('data', func_body).call(null, data);
};

于是,我们调用 tmpEngine, 就可以得到经过数据渲染后的字符串了。

var m = render(tpl2, data2);

console.log('m:' +m);

// m: <ul><li class="done"><span>text1</span></li> <li class="pending"><span>text2</span></li> </ul>

至此,我们的模板引擎的功能层面已经完成,可以愉快的玩耍了。但是!还有很多优化工作等待着推进,这里罗列几条,周末再战:

  • 特殊字符转义,业务可能需要输出html代码,减少XSS攻击
  • 数据为空时的处理
  • 性能

......

posted @ 2017-05-03 20:07  Liaofy  阅读(2410)  评论(0编辑  收藏  举报