本文实例讲述了 JavaScript 模板引擎。分享给大家供大家参考,具体如下:
原文链接: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 = '<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 = /<%([^%>]+)"nofollow noreferrer" target="_blank" href="http://www.w3schools.com/jsref/jsref_regexp_exec.asp">exec所做的工作:var re = /<%([^%>]+)"htmlcode">[ "<%name%>", " name ", index: 21, input: "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>" ]我们取得了正确的匹配结果,但正如你所看到的,只匹配到了一个标识片段
<%name%>
,所以我们需要一个while
循环去取得所有的标识片段。var re = /<%([^%>]+)"htmlcode">var TemplateEngine = function(tpl, data) { var re = /<%([^%>]+)"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 = /<%([^%>]+)"' + 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"' + 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 = /<%([^%>]+)"' + 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 = /<%([^%>]+)"' + 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行。
后记(译者注)
这是我第一次完整地翻译文章,语句多有错漏还请多多谅解,今后将继续努力,争取把更多优质的文章翻译分享。
由于对前端的框架、模板引擎一类的工具特别感兴趣,非常希望能够学习当中的原理,于是乎找了个相对简单的模板引擎开刀进行研究,google后看到了这篇文章觉得非常优秀,一步步讲解生动且深入,代码经过本人测试均能正确得到文章描述的结果。
模板引擎有多种设计思路,本文仅仅为其中的一种,其性能等参数还有待测试和提高,仅供学习使用。
谢谢大家~感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
标签:JavaScript,模板引擎
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新动态
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]