资讯专栏INFORMATION COLUMN

[译] 只有 20 行的 JavaScript 模板引擎

leon / 2411人阅读

摘要:原文链接译者吐槽只收藏不点赞都是耍流氓前言我仍旧在为我的预处理器进行开发工作。它原本是一个预处理器,但之后它扩展成为了预处理器,很快它将支持到的转换。

原文链接:JavaScript template engine in just 20 lines

(译者吐槽:只收藏不点赞都是耍流氓)

前言

我仍旧在为我的JS预处理器AbsurdJS进行开发工作。它原本是一个CSS预处理器,但之后它扩展成为了CSS/HTML预处理器,很快它将支持JS到CSS/HTML的转换。它就像一个模板引擎一样能够生成HTML代码,也就是说它能够用数据填充模板当中的标识片段。

因此,我希望去写一个可以满足我当前需求的模板引擎。AbsurdJS主要作为NodeJS的模块使用,但同时它也可以在客户端使用。为了这个目的,我无法使用市面上已经存在的模板引擎,因为它们几乎全都依赖于NodeJS,并且难以在浏览器中使用。我需要一个更小,纯JS写成的模板引擎。我浏览了这篇由John Resig写的博客,似乎这正是我需要的东西。我把当中的代码稍作修改,并且浓缩到了20行。

这段代码的运行原理非常有趣,我将在这篇文章中一步一步为大家展示John的wonderful idea。

1、提取标识片段

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

var TemplateEngine = function(tpl, data) {
    // magic here ...
}
var template = "

Hello, my name is <%name%>. I"m <%age%> years old.

"; console.log(TemplateEngine(template, { name: "Krasimir", age: 29 }));

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

Hello, my name is Krasimir. I"m 29 years old.

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

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

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

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

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

[
    "<%name%>",
    " name ", 
    index: 21,
    input: 
    "

Hello, my name is <%name%>. I"m <%age%> years old.

" ]

我们取得了正确的匹配结果,但正如你所看到的,只匹配到了一个标识片段<%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 = "

Hello, my name is <%this.name%>. I"m <%this.profile.age%> years old.

";

这看似不可能完成?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 
"

Hello, my name is " + this.name + ". I"m " + this.profile.age + " years old.

";

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

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

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

return
"My skills:" + 
for(var index in this.skills) { +
"" + 
this.skills[index] +
"" +
}

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

var r = [];
r.push("My skills:"); 
for(var index in this.skills) {
r.push("");
r.push(this.skills[index]);
r.push("");
}
return r.join("");

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

var TemplateEngine = function(tpl, data) {
    var re = /<%([^%>]+)?%>/g,
        code = "var r=[];
",
        cursor = 0, match;
    var add = function(line) {
        code += "r.push("" + line.replace(/"/g, """) + "");
";
    }
    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 = "

Hello, my name is <%this.name%>. I"m <%this.profile.age%> years old.

"; console.log(TemplateEngine(template, { name: "Krasimir Tsonev", profile: { age: 29 } }));

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

var r=[];
r.push("

Hello, my name is "); r.push("this.name"); r.push(". I"m "); r.push("this.profile.age"); return r.join("");

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

var add = function(line, js) {
    js? code += "r.push(" + line + ");
" :
        code += "r.push("" + line.replace(/"/g, """) + "");
";
}
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("

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(/[
	
]/g, "")).apply(data);

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

3、代码优化

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

var template = 
"My skills:" + 
"<%for(var index in this.skills) {%>" + 
"<%this.skills[index]%>" +
"<%}%>";
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("");
r.push(this.skills[index]);
r.push("");
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=[];
",
    cursor = 0;
var add = function(line, js) {
    js? code += line.match(reExp) ? line + "
" : "r.push(" + line + ");
" :
        code += "r.push("" + line.replace(/"/g, """) + "");
";
}

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

var r=[];
r.push("My skills:");
for(var index in this.skills) {
r.push("");
r.push(this.skills[index]);
r.push("");
}
r.push("");
return r.join("");

理所当然的正确执行啦:

My skills:jshtmlcss

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

var template = 
"My skills:" + 
"<%if(this.showSkills) {%>" +
    "<%for(var index in this.skills) {%>" + 
    "<%this.skills[index]%>" +
    "<%}%>" +
"<%} else {%>" +
    "

none

" + "<%}%>"; 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=[];
", cursor = 0, match;
    var add = function(line, js) {
        js? (code += line.match(reExp) ? line + "
" : "r.push(" + line + ");
") :
            (code += line != "" ? "r.push("" + line.replace(/"/g, """) + "");
" : "");
        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(/[
	
]/g, "")).apply(options);
}

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

后记(译者注)

这是我第一次完整地翻译文章,语句多有错漏还请多多谅解,今后将继续努力,争取把更多优质的文章翻译分享。

由于对前端的框架、模板引擎一类的工具特别感兴趣,非常希望能够学习当中的原理,于是乎找了个相对简单的模板引擎开刀进行研究,google后看到了这篇文章觉得非常优秀,一步步讲解生动且深入,代码经过本人测试均能正确得到文章描述的结果。

模板引擎有多种设计思路,本文仅仅为其中的一种,其性能等参数还有待测试和提高,仅供学习使用。
谢谢大家~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79633.html

相关文章

  • JavaScript 究竟是如何工作的?(第一部分)

    摘要:文章的第二部分涵盖了内存管理的概念,不久后将发布。的标准化工作是由国际组织负责的,相关规范被称为或者。随着分析器和编译器不断地更改字节码,的执行性能逐渐提高。 原文地址:How Does JavaScript Really Work? (Part 1) 原文作者:Priyesh Patel 译者:Chor showImg(https://segmentfault.com/img...

    Youngdze 评论0 收藏0
  • 】当不使用JavaScript框架时

    摘要:在其他方面,我们只需要考虑针对特定任务时所使用框架的成本。当我们必须使用或不应该使用框架时我强烈主张要了解编写某个工具的目的。 非常有价值的建议:哪些框架是合理的,哪些并不合理。 作者:Tod Hansmann 来源:https://opensource.com/articl...翻译:疯狂的技术宅说明:本专栏文章首发于公众号:jingchengyideng 。 showImg(htt...

    blair 评论0 收藏0
  • 只有20Javascript代码!手把手教你写一个页面模板引擎

    摘要:整个引擎实现只有不到行代码。不知道你有木有听说过一个基于的页面预处理器,叫做。最初我只是打算写一个的预处理器,不过后来扩展到了和,可以用来把代码转成和代码。最后一个改进可以使我们的模板引擎更为强大。 导读:AbsurdJS 作者写的一篇教程,一步步教你怎样用 Javascript 实现一个纯客户端的模板引擎。整个引擎实现只有不到 20 行代码。如果你能从头看到尾的话,还能有不少收获的。...

    Luosunce 评论0 收藏0
  • 正则表达式

    摘要:最全正则表达式总结验证号手机号中文邮编身份证地址等是正则表达式的缩写,作用是对字符串执行模式匹配。学习目标了解正则表达式语法在中使用正则表达式在中使 JS高级技巧 本篇是看的《JS高级程序设计》第23章《高级技巧》做的读书分享。本篇按照书里的思路根据自己的理解和经验,进行扩展延伸,同时指出书里的一些问题。将会讨论安全的类型检测、惰性载入函数、冻结对象、定时器等话题。1. 安全的类型检测...

    yibinnn 评论0 收藏0
  • 搞懂JavaScript的Function.prototype.bind[]

    摘要:搞懂的译可能是初学的人最不关心的函数,当你意识到需要保持在其他函数中的上下文,实际上你需要的是。这就是问题所在。整合事件绑定和一个重大提高就是,和等等。然而,并没有原生添加事件到多个节点的方式。能力有限,如有疑问,纰漏,速指出,感谢你 搞懂JavaScript的Function.prototype.bind[译] Ben Howdle binding可能是初学Javascript的人最...

    Pandaaa 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<