只有 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 相关文章推荐
使用TextRange获取输入框中光标的位置的代码
Mar 08 Javascript
从阶乘函数对比Javascript和C#的异同
May 31 Javascript
40个新鲜出炉的jQuery 插件和免费教程[上]
Jul 24 Javascript
解析JavaScript中的标签语句
Jun 19 Javascript
JavaScript操作表单实例讲解(上)
Jun 20 Javascript
手机端实现Bootstrap简单图片轮播效果
Oct 13 Javascript
JavaScript数据结构与算法之二叉树实现查找最小值、最大值、给定值算法示例
Mar 01 Javascript
layer.open回调获取弹出层参数的实现方法
Sep 10 Javascript
vue 更改连接后台的api示例
Nov 11 Javascript
基于Vue sessionStorage实现保留搜索框搜索内容
Jun 01 Javascript
React+EggJs实现断点续传的示例代码
Jul 07 Javascript
Nuxt.js 静态资源和打包的操作
Nov 06 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
php编程实现获取excel文档内容的代码实例
2011/06/28 PHP
PHP 过滤页面中的BOM(实现代码)
2013/06/29 PHP
Laravel 5.3 学习笔记之 安装
2016/08/28 PHP
php微信支付之公众号支付功能
2018/05/30 PHP
PHP实现单例模式建立数据库连接的方法分析
2020/02/11 PHP
让getElementsByName适应IE和firefox的方法
2007/09/24 Javascript
js 页面输出值
2008/11/30 Javascript
Jquery实现图片左右自动滚动示例
2013/09/25 Javascript
javascript背景时钟实现方法
2015/06/18 Javascript
JS实现的新浪微博大厅文字内容滚动效果代码
2015/11/05 Javascript
Bootstrap3.0建站教程(一)之bootstrap表单元素排版
2016/06/01 Javascript
如何提高javascript加载速度
2016/12/26 Javascript
javascript简单写的判断电话号码实例
2017/05/24 Javascript
利用angular、react和vue实现相同的面试题组件
2018/02/19 Javascript
JS监听滚动和id自动定位滚动
2018/12/18 Javascript
微信小程序动态添加view组件的实例代码
2019/05/23 Javascript
javascript实现文字跑马灯效果
2020/06/18 Javascript
在nuxt中使用路由重定向的实例
2020/11/06 Javascript
解决js中的setInterval清空定时器不管用问题
2020/11/17 Javascript
element 动态合并表格的步骤
2020/12/31 Javascript
python中使用sys模板和logging模块获取行号和函数名的方法
2014/04/15 Python
Python格式化压缩后的JS文件的方法
2015/03/05 Python
python函数形参用法实例分析
2015/08/04 Python
python中异常报错处理方法汇总
2016/11/20 Python
详解python之协程gevent模块
2018/06/14 Python
Python开发最牛逼的IDE——pycharm
2018/08/01 Python
python 用for循环实现1~n求和的实例
2019/02/01 Python
centos7之Python3.74安装教程
2019/08/15 Python
python取均匀不重复的随机数方式
2019/11/27 Python
python中取绝对值简单方法总结
2020/07/24 Python
python利用faker库批量生成测试数据
2020/10/15 Python
Turnbull & Asser官网:英国皇室御用的顶级定制衬衫
2019/01/31 全球购物
优秀毕业生自荐信范文
2014/01/01 职场文书
本科生就业推荐信
2014/05/19 职场文书
体育课外活动总结
2014/07/08 职场文书
Mysql多层子查询示例代码(收藏夹案例)
2022/03/31 MySQL