A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

1、提取标识片段

这是我们在开始的时候将要获得的东西:

var TemplateEngine = function(tpl, data) {    // magic here ...}var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';console.log(TemplateEngine(template, {    name: "Krasimir",    age: 29}));

一个简单的函数,传入模板数据作为参数,正如你所想象的,我们想要得到以下的结果:

<p>Hello, my name is Krasimir. I'm 29 years old.</p>

我们要做的第一件事就是获取模板中的标识片段<%...%>,然后用传入引擎中的数据去填充它们。我决定用正则表达式去完成这些功能。正则不是我的强项,所以大家将就一下,如果有更好的正则也欢迎向我提出。

var re = /<%([^%>]+)?%>/g;

我们将会匹配所有以<%开头以%>结尾的代码块,末尾的g(global)表示我们将匹配多个。有许多的方法能够用于匹配正则,但是我们只需要一个能够装载字符串的数组就够了,这正是exec所做的工作:

var re = /<%([^%>]+)?%>/g;var match = re.exec(tpl);

在控制台console.log(match)可以看到:

[    "<%name%>",    " name ",     index: 21,    input:     "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"]

我们取得了正确的匹配结果,但正如你所看到的,只匹配到了一个标识片段<%name%>,所以我们需要一个while循环去取得所有的标识片段。

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

运行,发现所有的标识片段已经被我们获取到了。

2、数据填充与逻辑处理

在获取了标识片段以后,我们就要对它们进行数据的填充。使用.replace方法就是最简单的方式:

var TemplateEngine = function(tpl, data) {    var re = /<%([^%>]+)?%>/g, match;    while(match = re.exec(tpl)) {        tpl = tpl.replace(match[0], data[match[1]])    }    return tpl;}data = {    name: "Krasimir Tsonev",    age: 29}

OK,正常运行。但很明显这并不足够,我们当前的数据结构非常简单,但实际开发中我们将面临更复杂的数据结构:

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

出现错误的原因,是当我们在模板中输入<%profile.age%>的时候,我们得到的data["profile.age"]是undefined的。显然.replace方法是行不通的,我们需要一些别的方法把真正的JS代码插入到<%和%>当中,就像以下栗子:

var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';

这看似不可能完成?John使用了new Function,即通过字符串去创建一个函数的方法去完成这个功能。举个栗子:

var fn = new Function("arg", "console.log(arg + 1);");fn(2); // 输出 3

fn是个真正的函数,它包含一个参数,其函数体为console.log(arg + 1)。以上代码等价于下列代码:

var fn = function(arg) {    console.log(arg + 1);}fn(2); // 输出 3

通过new Function,我们得以通过字符串去创建一个函数,这正是我们所需要的。在创建这么一个函数之前,我们需要去构造这个它的函数体。该函数体应当返回一个最终拼接好了的模板。沿用前文的模板字符串,想象一下这个函数应当返回的结果:

return "<p>Hello, my name is " + this.name + ". I\'m " + this.profile.age + " years old.</p>";

显然,我们把模板分成了文本和JS代码。正如上述代码,我们使用了简单的字符串拼接的方式去获取最终结果,但是这个方法无法100%实现我们的需求,因为之后我们还要处理诸如循环之类的JS逻辑,像这样:

var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<a href=""><%this.skills[index]%></a>' +'<%}%>';

如果使用字符串拼接,结果将会变成这样:

return'My skills:' + for(var index in this.skills) { +'<a href="">' + this.skills[index] +'</a>' +}

理所当然这会报错。这也是我决定参照John的文章去写逻辑的原因——我把所有的字符串都push到一个数组中,在最后才把它们拼接起来:

var r = [];r.push('My skills:'); for(var index in this.skills) {r.push('<a href="">');r.push(this.skills[index]);r.push('</a>');}return r.join('');

下一步逻辑就是整理得到的每一行代码以便生成函数。我们已经从模板中提取出了一些信息,知道了标识片段的内容和位置,所以我们可以通过一个指针变量(cursor)去帮助我们取得最终的结果:

var TemplateEngine = function(tpl, data) {    var re = /<%([^%>]+)?%>/g,        code = 'var r=[];\n',        cursor = 0, match;    var add = function(line) {        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';    }    while(match = re.exec(tpl)) {        add(tpl.slice(cursor, match.index));        add(match[1]);        cursor = match.index + match[0].length;    }    add(tpl.substr(cursor, tpl.length - cursor));    code += 'return r.join("");'; // <-- return the result    console.log(code);    return tpl;}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 }}));

变量code以声明一个数组为开头,作为整个函数的函数体。正如我所说的,指针变量cursor表示我们正处于模板的哪个位置,我们需要它去遍历所有的字符串,跳过填充数据的片段。另外,add函数的任务是把字符串插入到code变量中,作为构建函数体的过程方法。这里有一个棘手的地方,我们需要跳过标识符<%%>,否则当中的JS脚本将会失效。如果我们直接运行上述代码,结果将会是下面的情况:

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

呃……这不是我们想要的。this.name和this.profile.age不应该带引号。我们改进一下add函数:

var add = function(line, js) {    js? code += 'r.push(' + line + ');\n' :        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';}var match;while(match = re.exec(tpl)) {    add(tpl.slice(cursor, match.index));    add(match[1], true); // <-- say that this is actually valid js    cursor = match.index + match[0].length;}

标识片段中的内容将通过一个boolean值进行控制。现在我们得到了一个正确的函数体:

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

接下来我们要做的就是生成这个函数并且运行它。在这个模板引擎的末尾,我们用以下代码去代替直接返回一个tpl对象:

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

我们甚至不需要向函数传递任何的参数,因为apply方法已经为我们完整了这一步工作。它自动设置了作用域,这也是为什么this.name可以运行,this指向了我们的data。

3、代码优化

大致上已经完成了。最后一件事情,我们需要支持更多复杂的表达式,像if/else表达式和循环等。让我们用同样的例子去尝试运行下列代码:

var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<a href="#"><%this.skills[index]%></a>' +'<%}%>';console.log(TemplateEngine(template, {    skills: ["js", "html", "css"]}));

结果将会报错,错误为Uncaught SyntaxError: Unexpected token for。仔细观察,通过code变量我们可以找出问题所在:

var r=[];r.push("My skills:");r.push(for(var index in this.skills) {);r.push("<a href=\"\">");r.push(this.skills[index]);r.push("</a>");r.push(});r.push("");return r.join("");

包含着for循环的代码不应该被push到数组当中,而是直接放在脚本里面。为了解决这个问题,在把代码push到code变量之前我们需要多一步的判断:

var re = /<%([^%>]+)?%>/g,    reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,    code = 'var r=[];\n',    cursor = 0;var add = function(line, js) {    js? code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n' :        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';}

我们添加了一个新的正则。这个正则的作用是,如果一段JS代码以if, for, else, switch, case, break, |开头,那它们将会直接添加到函数体中;如果不是,则会被push到code变量中。下面是修改后的结果:

var r=[];r.push("My skills:");for(var index in this.skills) {r.push("<a href=\"#\">");r.push(this.skills[index]);r.push("</a>");}r.push("");return r.join("");

理所当然的正确执行啦:

My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>

接下来的修改会给予我们更强大的功能。我们可能会有更加复杂的逻辑会放进模板中,像这样:

var template = 'My skills:' + '<%if(this.showSkills) {%>' +    '<%for(var index in this.skills) {%>' +     '<a href="#"><%this.skills[index]%></a>' +    '<%}%>' +'<%} else {%>' +    '<p>none</p>' +'<%}%>';console.log(TemplateEngine(template, {    skills: ["js", "html", "css"],    showSkills: true}));

进行过一些细微的优化之后,最终的版本如下:

var TemplateEngine = function(html, options) {    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0, match;    var add = function(line, js) {        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');        return add;    }    while(match = re.exec(html)) {        add(html.slice(cursor, match.index))(match[1], true);        cursor = match.index + match[0].length;    }    add(html.substr(cursor, html.length - cursor));    code += 'return r.join("");';    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);}

优化后的代码甚至少于15行。


【转载】仅作分享,侵删

1 个回复

倒序浏览
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马