简单的前端渲染模板实现

国庆节过去第一天,有点懒,但一想到今年的法定节假日都已经过完,慢慢就燃起奋斗欲望。今天碰巧看到网络上一些高手博客写着渲染模板教程,就做个随笔记录吧!

渲染模板简单的说,就是将一些数据,字符串加载到几个的变量当中。

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

js数据加载方式

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

var result = tplEngine(tpl, data);

以上使用方式,相比大家都很熟悉,目前成熟的渲染模板也有很多,包括一些mvc框架avaron ,angular,vue等都自带前端渲染模板。所以大家也知道模板具有维护方便,代码清晰,版本迭代都很方便。缺点就是seo 等,不过大家做项目时候,关于框架选择,模板选择基本上都是别人搞好,我们都是学着用就可以了。

所以我们就上面代码,做个js编译,我们要把<%name%>识别出来,然后将js数据写进去,所以我们要使用正则表达式。

<div class="name">我的名字叫做<%name%></div>



    <script>
        var name = document.querySelector('.name').innerText;
        //数据源
        var data = {
            "name": "Barret Lee",
            "age": "20"
        };
        //正则获取<%name%>中的name,并用data中的name的值替换
        var result = name.replace(/<%([^%>]+)?%>/g,function(s0,s1){
            return data[s1]
        });
        document.querySelector('.name').innerText = result;
    </script>

上面代码使用正则获取<%name%>变量,然后将data中的name值替换,精简版js模板已经完成。嗯,哪怕这只是简单的字符串替换。

由于时间关系,暂时这样,明天有空继续补上。

------------------------2017-10-10---------------------

继续昨天代码,当对象为对象,字符串替换方式就失灵了,我们换一种方式去探索

<div class="render"><%name%> <%info.age%> </div>

    <script>
    let tmpl = document.querySelector('.render').innerText;
    let data = {
        "name": "Barret Lee",
        "info": { "age": "20"}
    }
    let result = tmpl.replace(/<%([^%>]+)?%>/g,function(s0,s1){
        return 'Hei, my name is ' + data.name + ', and I\'m ' + data.info.age+ 'years old.';
    })
    document.querySelector('.render').innerText = result;
    
    </script>

上面是得到变量之后,根据<%%>符号判断,然后返回一个js对象值,是的,上面返回两次,本来就有两个变量~~

根据上面思路,我们继续,假若存在for循环,我们要怎么解析它的代码?将for循环转成字符串返回?嗯我们试试

let result = tmpl.replace(/<%([^%>]+)?%>/g,function(s0,s1){
      return 'Posts: ' + 
       'for(var i = 0; i < post.length; i++) {'
         '<a href="#">' + post[i].exper + '</a> }'
    })

这段代码明显不行,为何?他会直接输出该字符串,我们要的是得到多个a标签和内容,所以我们需要分开保存,js数组上场了。

let result = [];
            result.push('Post');
            result.push('for(var i = 0; i < post.length; i++) {');
            result.push('<a href="#">');
            result.push(post[i].exper);
            result.push('</a>');
            result.push('}');
上面使用数组装载for循环的语句和一些字符串,看起来完美,但事情没那么简单,这样子的数组都是字符串,无法转换,我们换一下

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>');
}
这样子看来没问题了,那么我们怎么解析运行呢?这里要用到new Function()对象实例,可能很多人对function熟悉,但其实内部还是要经过new Function得到一个实例。我们可以使用另外一种方式创建函数

var function_name = new function(arg1, arg2, ..., argN, function_body)
在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串。

是不是很熟悉?再看例子

function sayHi(sName, sMessage) {
  alert("Hello " + sName + sMessage);
}
上面是普通的不能在普通的函数声明,那么new Function的

var sayHi 
= 
new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);");

是不是很像?恩,一般情况下,我们很少使用new Function去声明函数,因为麻烦。对其感兴趣可以看下这个链接  http://www.w3school.com.cn/js/pro_js_functions_function_object.asp

回到正题来,我们要用new Function构建一个函数来运行我们的数组,我们先来看下直接运行相关代码样子

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

new function例子完整输出

<div class="render">
            <% for(var i = 0; i < post.length; i++) {+
                <a href="#"><% post[i].expert %></a> + 
            <% } %>
        </div>
    
        <script>
            let tmpl = document.querySelector('.render').innerText;
            let data = {
                "name": "Barret Lee",
                "info": { "age": "20"}
            }
            
            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
            document.querySelector('.render').innerText = fn({"name": "barretlee", "age": "20"});
        </script>

fn函数中传入一个对象,返回一个字符串集合,join() 方法用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的。所以思路就有了,我们可以把逻辑部分和非逻辑部分的代码链接成一个字符串,然后利用类似fn的函数直接编译代码。为了能够识别所有元素,我们要使用exec代替replace。

exec() 方法用于检索字符串中的正则表达式的匹配,返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。

以下为w3cShool解释

说明
exec() 方法的功能非常强大,它是一个通用的方法,而且使用起来也比 test() 方法以及支持正则表达式的 String 对象的方法更为复杂。
如果 exec() 找到了匹配的文本,则返回一个结果数组。否则,返回 null。此数组的第 0 个元素是与正则表达式相匹配的文本,第 1 个元素是与 RegExpObject 的第 1 个子表达式相匹配的文本(如果有的话),第 2 个元素是与 RegExpObject 的第 2 个子表达式相匹配的文本(如果有的话),以此类推。除了数组元素和 length 属性之外,exec() 方法还返回两个属性。index 属性声明的是匹配文本的第一个字符的位置。input 属性则存放的是被检索的字符串 string。我们可以看得出,在调用非全局的 RegExp 对象的 exec() 方法时,返回的数组与调用方法 String.match() 返回的数组是相同的。
但是,当 RegExpObject 是一个全局正则表达式时,exec() 的行为就稍微复杂一些。它会在 RegExpObject 的 lastIndex 属性指定的字符处开始检索字符串 string。当 exec() 找到了与表达式相匹配的文本时,在匹配后,它将把 RegExpObject 的 lastIndex 属性设置为匹配文本的最后一个字符的下一个位置。这就是说,您可以通过反复调用 exec() 方法来遍历字符串中的所有匹配文本。当 exec() 再也找不到匹配的文本时,它将返回 null,并把 lastIndex 属性重置为 0。

http://www.w3school.com.cn/jsref/jsref_exec_regexp.asp

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

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

对比下两者区别?第一种只会识别<%name%>,那么<%age%>呢?提示:请注意,无论 RegExpObject 是否是全局模式,exec() 都会把完整的细节添加到它返回的数组中。这就是 exec() 与 String.match() 的不同之处,后者在全局模式下返回的信息要少得多。因此我们可以这么说,在循环中反复地调用 exec() 方法是唯一一种获得全局模式的完整模式匹配信息的方法。

<div class="render">
                <% for(var i = 0; i < this.posts.length; i++) {%>
                    <a href="#"><% this.posts[i].expert %></a> 
                <% } %>
            </div>
        
            <script>
                var tmpl = document.querySelector('.render').innerText;
                var data = {
                    "posts": [{
                        "expert": "content 1",
                        "time": "yesterday"
                    },{
                        "expert": "content 2",
                        "time": "today"
                    },{
                        "expert": "content 3",
                        "time": "tomorrow"
                    },{
                        "expert": "",
                        "time": "eee"
                    }]
                };
                function tplEngine(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);
                };
                tplEngine(tmpl,data)
                document.querySelector('.render').innerHTML = tplEngine(tmpl,data);
            </script>

还有点没搞懂,明天继续




参考链接:http://www.cnblogs.com/hustskyking/p/principle-of-javascript-template.html