只有 20 行的 JavaScript 模板引擎实例详解


Posted in Javascript onMay 11, 2020

本文实例讲述了 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 = /<%([^%>]+)?%>/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.namethis.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行。

后记(译者注)

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

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

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

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
JavaScript中null与undefined分析
Jul 25 Javascript
js 调用父窗口的具体实现代码
Jul 15 Javascript
jQuery事件用法实例汇总
Aug 29 Javascript
JavaScript字符串对象substr方法入门实例(用于截取字符串)
Oct 16 Javascript
js实现固定显示区域内自动缩放图片的方法
Jul 18 Javascript
javascript事件模型介绍
May 31 Javascript
Bootstrap 实现查询的完美方法
Oct 26 Javascript
详解Angular的数据显示优化处理
Dec 26 Javascript
React手稿之 React-Saga的详解
Nov 12 Javascript
JS使用for in有序获取对象数据
May 19 Javascript
vue addRoutes路由动态加载操作
Aug 04 Javascript
前端canvas中物体边框和控制点的实现示例
Aug 05 Javascript
ES6使用新特性Proxy实现的数据绑定功能实例
May 11 #Javascript
JavaScript异步操作的几种常见处理方法实例总结
May 11 #Javascript
Nuxt默认模板、默认布局和自定义错误页面的实现
May 11 #Javascript
Vue.js获取手机系统型号、版本、浏览器类型的示例代码
May 10 #Javascript
vue总线机制(bus)知识点详解
May 10 #Javascript
vue路由跳转传递参数的方式总结
May 10 #Javascript
javascript单张多张图无缝滚动实例代码
May 10 #Javascript
You might like
收集的DedeCMS一些使用经验
2007/03/17 PHP
PHP时间戳格式全部汇总 (获取时间、时间戳)
2016/06/13 PHP
php socket通信简单实现
2016/11/18 PHP
实例讲解通过​PHP创建数据库
2019/01/20 PHP
一段实时更新的时间代码
2006/07/07 Javascript
jQuery源码分析-03构造jQuery对象-工具函数
2011/11/14 Javascript
jquery 滚动条事件简单实例
2013/07/12 Javascript
JavaScript中使用Object.create()创建对象介绍
2014/12/30 Javascript
分享一个自己写的简单的javascript分页组件
2015/02/15 Javascript
删除javascript所创建子节点的方法
2015/05/21 Javascript
jQuery插件HighCharts绘制2D金字塔图效果示例【附demo源码下载】
2017/03/09 Javascript
Vue2.0父组件与子组件之间的事件发射与接收实例代码
2017/09/19 Javascript
基于 Vue.js 2.0 酷炫自适应背景视频登录页面实现方式
2018/01/17 Javascript
vue里面使用mui的弹出日期选择插件实例
2018/09/16 Javascript
详解Angular Forms中自定义ngModel绑定值的方式
2018/12/10 Javascript
Element Cascader 级联选择器的使用示例
2020/07/27 Javascript
详解JavaScript中分解数字的三种方法
2021/01/05 Javascript
Node.js中的异步生成器与异步迭代详解
2021/01/31 Javascript
[05:09]DOTA2-DPC中国联赛2月22日Recap集锦
2021/03/11 DOTA
Python+django实现文件上传
2016/01/17 Python
详解用Python实现自动化监控远程服务器
2019/05/18 Python
Python生成器实现简单&quot;生产者消费者&quot;模型代码实例
2020/03/27 Python
解决Python spyder显示不全df列和行的问题
2020/04/20 Python
Python docutils文档编译过程方法解析
2020/06/23 Python
CSS3 渐变(Gradients)之CSS3 线性渐变
2016/07/08 HTML / CSS
使用css创建三角形 使用CSS3创建3d四面体原理及代码(html5实践)
2013/01/06 HTML / CSS
英国骑行、跑步、游泳、铁人三项运动装备专卖店:Wiggle
2016/08/23 全球购物
英国Zoro工具:手动工具,电动工具和个人防护用品
2016/11/02 全球购物
护理工作感言
2014/01/16 职场文书
学生宿舍管理制度
2014/01/30 职场文书
司机检讨书
2014/02/13 职场文书
倡导文明标语
2014/06/16 职场文书
化工专业自荐书
2014/06/16 职场文书
党员剖析材料范文
2014/09/30 职场文书
优秀高中学生评语
2014/12/30 职场文书
解决IDEA翻译插件Translation报错更新TTK失败不能使用
2022/04/24 Python