美文网首页我爱编程
简单的模版引擎

简单的模版引擎

作者: DHFE | 来源:发表于2018-06-11 23:03 被阅读11次

参考文章:只有20行Javascript代码!手把手教你写一个页面模板引擎 - 文章 - 伯乐在线

AbsurdJS

先来一个小case。
给出一个歌曲数组:

    var songs = [
        {name:"刚刚好",singer:"薛之谦",url:"http://music.163.com/xxx"},
        {name:"最佳歌手",singer:"许嵩",url:"http://music.163.com/xxx"},
        {name:"初学者",singer:"薛之谦",url:"http://music.163.com/xxx"},
        {name:"绅士",singer:"薛之谦",url:"http://music.163.com/xxx"},
        {name:"我们",singer:"陈伟霆",url:"http://music.163.com/xxx"},
        {name:"画风",singer:"后弦",url:"http://music.163.com/xxx"},
        {name:"We Are One",singer:"郁可唯",url:"http://music.163.com/xxx"},
    ]

需求:将数组内信息写入


这个很简单了,当时我想到的方法也是遍历嘛。

    var songs = [
        {name:"刚刚好",singer:"薛之谦",url:"http://music.163.com/xxx"},
        {name:"最佳歌手",singer:"许嵩",url:"http://music.163.com/xxx"},
        {name:"初学者",singer:"薛之谦",url:"http://music.163.com/xxx"},
        {name:"绅士",singer:"薛之谦",url:"http://music.163.com/xxx"},
        {name:"我们",singer:"陈伟霆",url:"http://music.163.com/xxx"},
        {name:"画风",singer:"后弦",url:"http://music.163.com/xxx"},
        {name:"We Are One",singer:"郁可唯",url:"http://music.163.com/xxx"},
    ]

    for (var i=0,j=songs.length;i<j;i++) {
        $(".hotSong").append(createHtml(i));
    }
    
    function createHtml(index) {
        var htmls = "";
        htmls += "<li><a href='" + songs[index]["url"] + "'>";
        htmls += "<div class='song-name'>"+ songs[index]["name"] +"</div>";
        htmls += "<div>-</div>";
        htmls += "<div class='singer'>" + songs[index]["singer"] + "</div>";
        htmls += "</a></li>";
        
        return $(htmls);
    }

但是,这种方法存在一些问题,我们来看一看。

  1. 如果用户传了这么个东西上来呢?:
        {name:"初学者<script>alert('0')<\/script>",singer:"薛之谦",url:"http://music.163.com/xxx"},

运行一下....


emmmmm

那么第一个问题就是:
跨站脚本攻击(XSS Cross Site Scripting)
XSS 跨站脚本攻击 的防御解决方案
xss攻击原理与解决方法


那么我们换一种方法,通过构造DOM节点来push到页面上。

    for (var i in songs) {
        var node = document.querySelector(".hotSong");
        node.appendChild(createHtml(i));
    }


    function createHtml(i) {
        var fragment = document.createDocumentFragment();

        var songNameDiv = document.createElement("div");
        var songNameDivText = document.createTextNode(songs[i]["name"]);
        songNameDiv.appendChild(songNameDivText);
        var lineDiv = document.createElement("div");
        lineDiv.appendChild(document.createTextNode("-"));
        var singerDiv = document.createElement("div");
        var singerDivText = document.createTextNode(songs[i]["singer"]);
        singerDiv.appendChild(singerDivText);
        
        var a = document.createElement("a");
        a.href = songs[i]["href"];
        a.appendChild(songNameDiv);
        a.appendChild(lineDiv);
        a.appendChild(singerDiv);


        var li = document.createElement("li");
        li.appendChild(a);
        
        fragment.appendChild(li);

        return fragment;
    }

效果:


内嵌的script没有执行,因为它被识别为HTML了。

那么构造HTML的缺点是什么?
当然是烦琐复杂的代码,需要不断的构造节点,加入节点,不利于操作。

试试如果用jQuery改造能简单多少呢?

    for (var i in songs) {
        $(".hotSong").append(createHtml(i));
    }


    function createHtml(i) {
        var $li = $("<li></li>");
        var $a = $("<a></a>").attr("href",songs[i]["url"]);
        var $songNameDiv = $("<div></div>").attr("class","song-name").text(songs[i]["name"]);
        var $lineDiv = $("<div></div").text("-");
        var $singerDiv = $("<div></div>").attr("class","singer").text(songs[i]["singer"]);

        $a.append($singerDiv,$lineDiv,$songNameDiv);
        $li.append($a);

        return $li;
    }
jquery

感觉少挺多哈。


我们可否实现这样一个功能的函数?


        function stringFormat(string) {

            var params = [].slice.call(arguments, 1);        // 截取string后的代替参数 
            var regex = /\{(\d+)\}/g;

            string = string.replace(regex, function () {
                var index = arguments[1];
                return params[index];
            })
            return string;
        }
    console.log(stringFormat("Hi , {0} , {1} , {2}","3","2","1"));  // Hi , 3 , 2 , 1

    console.log(stringFormat("Hi , {0}","Frank"));                  // Hi , Frank
    
    console.log(stringFormat("Hi , {1}","Frank","Tomy"));           // Hi , Tomy

    console.log(stringFormat("Hi , {0} and {1}","Frank","Tomy"));   // Hi , Frank and Tomy

以第一个console test为例,我们传入的参数是"Hi , {0} , {1} , {2}","3","2","1",用括号加以区别,("Hi , {0} , {1} , {2}") , ("3","2","1")),第一个参数"Hi , {0} , {1} , {2}"中,大括号就是一个标记,目的就是将后续的替换值代替大括号所占位置,接下来逐步分析;

形参string保存了实参"Hi , {0} , {1} , {2}","3","2","1",在函数内部,我们可以在arguments类数组对象中找到它们,从实参的格式不难看出,只有第一个参数为原始字符串值,而后面的参数为替换值,"Hi , {0} , {1} , {2}"为原始值,"3","2","1"为依次要替换的值,所以需要取得除第一个参数外后面所有的参数,使用数组slice方法来实现。

var params = [].slice.call(arguments,1);
// Array(3) ["3", "2", "1"]

然后构造正则,识别原始值中的标记点{num}

var regex = /\{(\d+)\}/g;

测试一下:

    var reg = /\{(\d+)\}/g
    var text = "abcd{1}{2}cdff{adbc123}{-1}{0}";
    console.log(text.match(reg));       // Array(3) ["{1}", "{2}", "{0}"]

这里是重点,先上一个MDN关于replace方法的链接:replace

因为最终需要替换字符串原始值,所以选择使用replace方法。

        string.replace(regex,function() {
            console.log(arguments);
            var index = arguments[1];
            return params[index];
        })

首先我们明确一下,string指的是什么,是stringFormat("Hi , {0} , {1} , {2}" , "3","2","{4}")参数里面的所有的值吗?当然不是,因为形参只有一个string,多余的实参会被arguments对象储存起来,但是函数内部的string指的是"Hi , {0} , {1} , {2}",逗号后的取不到。

在这个函数中,首先arguments对象发生改变,函数首先会对调用它的string使用传入的regex正则进行筛选,每一个被筛选中的值都会使arguments变化。我把每一次的arguments对象打印了出来看一看。


在replace中设置一个count,打印count得知replace运行了三次。

以第一次执行的argument对象为例:
arguments:Arguments(4) ["{0}", "0", 5, "Hi , {0} , {1} , {2}"]

  • arguments[0]:匹配到的字串
  • arguments[1]:/\{(\d+)\}/g小括号的意义就在于此,表示分组匹配的字符串。
  • arguments[2]:取到的index值,不信数数"Hi , {0} , {1} , {2}"
  • arguments[3]:被匹配的原字符串。

第二次,第三次同理。

        string = string.replace(regex,function() {
            var index = arguments[1];
            return params[index];
        })

每一次,我们都会得到对象中的arguments[1],即替换标记,储存在index内。
然后用params对象的相同index位置的值替换它。

var params = [].slice.call(arguments,1);
// Array(3) ["3", "2", "1"]

试一个长一点的:
stringFormat("{0}今天去游乐场,{1}陪他去,路上碰到了{2},之后{3}一起结伴回家","小明","小红","小刚","小明小红小刚")

大体思路:

  • 标记点设定(需要方便正则查找),如{0}
  • 保存替换值,如params数组。
  • replace方法,正则取出index值,如{1} => parmas[1]

注意点:
标记点和替换参数必须对应上

stringFormat("{0}今天去游乐场,{1}陪他去,路上碰到了{2},之后{3}一起结伴回家","小明","小红","小明小红小刚")

这里我没有传入{3}的对应值,就成了这样。
小明今天去游乐场,小红陪他去,路上碰到了小明小红小刚,之后undefined一起结伴回家
替换参数括号

stringFormat("{0}今天去游乐场,{1}陪他去,路上碰到了{2},之后{3}一起结伴回家",("小明","小红","小刚","小明小红小刚"))

小明小红小刚今天去游乐场,undefined陪他去,路上碰到了undefined,之后undefined一起结伴回家
对替换参数加大括号,导致params数组中只有一个值,那么也会出现错误。


但是目前来说,还不能称之为一个模版引擎,只能做一对一的替换。


“升级“之后,就叫做模版引擎了。

    var TemplateEngine = function(tpl,data) {
        var regex = /<%([^%>]+)?%>/g;
        while (match = regex.exec(tpl)) {
            console.log(match);
            tpl = tpl.replace(match[0],data[match[1]])
        }
        return tpl;
    }

    var template = "<p>Hello, My name is <%name%>. I'm <%age%> years old.</p>";

    var data = {
        name: "DH",
        age: 21
    }

    var string = TemplateEngine(template,data);
    console.log(string);
    // <p>Hello, My name is DH. I'm 21 years old.</p>

template是原始模版文本,data对象是替换数据源,template()函数就是主要的功能实现函数了。

有两个方法需要先了解,第一个就是replace,这个不多说,另外一个是正则的exec()方法。
regex.exec()
MDN——RegExp.prototype.exec()

同样的,在原始模版文本上,我们依然可以找到标记点,<%name%><%age%>,这是我们的正则表达式所决定的,原始模版文本将会作为TemplateEngine()函数的参数之一传入。

/<%([^%>]+)?%>/g

  • /<%%>/g:以<%开始,%>结束。
  • [^%>]:这个字符集合中,字符不能为%>(不能是这两个字符中的任何一个)
  • [^%>]+:这个集合可重复0次或多次。
  • ([^%>]+):对其分组
    ...

RegExr

总之,正则是为了获取到原始文本中的标记点的,相辅相成。

        while (match = regex.exec(tpl)) {
            console.log(match);
            tpl = tpl.replace(match[0],data[match[1]])
        }

regex.exec每一次遍历原始文本,每匹配到一个字串,都会返回一个数组,返回数组也能让while进行下去,当不再有字串符合,将返回undefined,那么while条件不成立,跳出循环。

每一次匹配到的如下:


之所以在正则中对其分组,也是为了获取<%string%>中的关键字string

MDN手册

tpl = tpl.replace(match[0],data[match[1]])
接下来就使用replace进行替换了,match就是每一次匹配打印的数组中的匹配字串中的分组内容。将会按照这个内容会到data数组中查找替换字串。

    var data = {
        name: "DH",
        age: 21
    }

over。

但是到这里还是有一些问题。
不信把data改成如下的形式试试。

{
    name: "Krasimir Tsonev",
    profile: { age: 29 }
}

作者后续给出了更佳的解决方案。(结合文章)

当时自己也看不懂,一个一个console看参数最后才理清,留一些备注,后续仅作为个人理解代码纪录吧。

    var TemplateEngine = function (tpl, data) {
        var re = /<%([^%>]+)?%>/g,      // 标记点正则
            code = 'var r=[];\n',       // 使用code数组保存所有的字符串
            cursor = 0;                 // 正则表达式的匹配项,以及它们的位置

        var add = function (line, js) {     // add函数用于(识别并)接收一个字符串 || JS执行代码(由while提供参数js),拼接为完整JS代码。
            if (js) {
                code += 'r.push(' + line + ');\n';  // JS走此处逻辑
            } else {
                code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';       // HTML走此处逻辑            
                // 需要把 code 包含的双引号字符进行转义(escape)。否则生成的函数代码会出错                
            }

            // "<p>Hello, my name is "|| "this.name"
            // r.push("<p>Hello, my name is ")  ||  r.push("this.name");
        }

        while (match = re.exec(tpl)) {              // 检测JS标记点
            add(tpl.slice(cursor, match.index));    // 截至到JS标记点(cursor)之前所有的非JS(即HTML)字串作为参数传入add
            add(match[1],true);                 
            // add("this.name") || add("this.profile.age") , true表示此处为JS,不需要引号

            cursor = match.index + match[0].length; // 更新cursor,准备下一段HTML传入
            // cursor = 标记点字串开始位置 + 整个标记点的字串长度;  cursor = 21 + 13 <%this.name%>.length
        }

        add(tpl.substr(cursor, tpl.length - cursor));   // 将最后的HTML Code取出。
        code += 'return r.join("");';                   // 末尾工序,拼接 return r.join("")

        console.log(code);
        // var r=[];
        // r.push("<p>Hello, my name is ");
        // r.push(this.name);
        // r.push(". I'm ");
        // r.push(this.profile.age);
        // r.push(" years old.</p>");
        // return r.join("");

        return new Function(code.replace(/[\t\r\n]/g,'')).apply(data);
        // 然后将完整的字串(code)传入函数类中,并apply了data,使得this正确指向相关数据,最后得出完整HTML Code
    }

    // 简单版,不支持复杂语句(if,for...)
    var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
    console.log(TemplateEngine(template, {
        name: "Krasimir Tsonev",
        profile: { age: 29 }
    }));
    
    // 最终: <p>Hello, my name is Krasimir Tsonev. I'm 29 years old.</p>

最后一个改进可以使我们的模板引擎更为强大。我们可以直接在模板中使用复杂逻辑。

    var TemplateEngine = function (tpl, data) {
        var re = /<%([^%>]+)?%>/g;                                          // 标记点正则
        var reExp = /(^()?(if|for|else|switch|case|break|{|}))(.*)?/g;      // 判断if,for,else等关键字正则

        var code = 'var r=[];\n';       // 使用code数组保存所有的字符串
        var cursor = 0;                 // 正则表达式的匹配项,以及它们的位置

        var add = function (line, js) {     // add函数用于(识别并)接收一个字符串 || JS执行代码(由while提供参数js),拼接为完整JS代码。
            console.log(line);
            if (js) {
                if (line.match(reExp)) {    // 是否为if,for等语句,是则直接加入其中,直接执行,不需要包含在push内部
                    code += line + "\n";
                } else {
                    code += 'r.push(' + line + ');\n';  // 普通的JS语句,如this.name等
                }
            } else {
                code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';   // HTML语句逻辑走此处
                // 需要把 code 包含的双引号字符进行转义(escape)。否则生成的函数代码会出错  
            }

        }

        while (match = re.exec(tpl)) {              // 检测JS标记点
            add(tpl.slice(cursor, match.index));    // 截至到JS标记点(cursor)之前所有的非JS(即HTML)字串作为参数传入add
            add(match[1],true);                 

            cursor = match.index + match[0].length; // 更新cursor,准备下一段HTML传入
            // cursor = 标记点字串开始位置 + 整个标记点的字串长度;
        }

        add(tpl.substr(cursor, tpl.length - cursor));   // 将最后的HTML Code取出。
        code += 'return r.join("");';                   // 末尾工序,拼接 return r.join("")

        console.log(code);  // 以下就是进入new Function的JS语句,注意if
        /*  打印code
        var r = [];
        r.push("My skills:");
        if (this.showSkills) {
            r.push("");
            for (var index in this.skills) {
                r.push("<a href=\"#\">");
                r.push(this.skills[index]);
                r.push("</a>");
            }
            r.push("");
        } else {
            r.push("<p>none</p>");
        }
        r.push("");
        return r.join("");
        */

       
        return new Function(code.replace(/[\t\r\n]/g,'')).apply(data);
        // 完整的字串传入函数类中,并apply了data,使得this正确指向相关数据,最后得出完整HTML Code
    }

    // test,加入了更复杂的if语句
    var template = "";
    template += 'My skills:';
    template += '<%if(this.showSkills) {%>';
    template += '<%for(var index in this.skills) {%>';
    template += '<a href="#"><%this.skills[index]%></a>';
    template += '<%}%>';
    template += '<%} else {%>';
    template += '<p>none</p>';
    template += '<%}%>';

    console.log(TemplateEngine(template, {
        skills: ["js", "html", "css"],
        showSkills: true
    }));
    // 最终结果(true):My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>
    // 最终结果(false):My skills:<p>none</p>

相关文章

网友评论

    本文标题:简单的模版引擎

    本文链接:https://www.haomeiwen.com/subject/hurvsftx.html