教你使用javascript简单写一个页面模板引擎


Posted in Javascript onMay 05, 2015

于是我又想着能不能写一些简单的代码来完善这个模板引擎,又能与其它现有的逻辑协同工作。AbsurdJS本身主要是以NodeJS的模块的形式发布的,不过它也会发布客户端版本。考虑到这些,我就不能直接使用现有的引擎了,因为它们大部分都是在NodeJS上运行的,而不能跑在浏览器上。我需要的是一个小巧的,纯粹以Javascript编写的东西,能够直接运行在浏览器上。当我某天偶然发现John Resig的这篇博客,我惊喜地发现,这不正是我苦苦寻找的东西嘛!我稍稍做了一些修改,代码行数差不多20行左右。其中的逻辑非常有意思。在这篇文章中我会一步一步重现编写这个引擎的过程,如果你能一路看下去的话,你就会明白John的这个想法是多么犀利!

最初我的想法是这样子的:

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)表示不只匹配一个,而是匹配所有符合的片段。Javascript里面有很多种使用正则表达式的方法,我们需要的是根据正则表达式输出一个数组,包含所有的字符串,这正是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>"
]

不过我们可以看见,返回的数组仅仅包含第一个匹配项。我们需要用while循环把上述逻辑包起来,这样才能得到所有的匹配项。

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

如果把上面的代码跑一遍,你就会看见<%name%> 和 <%age%>都被打印出来了。

下面,有意思的部分来了。识别出模板中的匹配项后,我们要把他们替换成传递给函数的实际数据。最简单的办法就是使用replace函数。我们可以像这样来写:

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

好了,这样就能跑了,但是还不够好。这里我们以data["property"]的方式使用了一个简单对象来传递数据,但是实际情况下我们很可能需要更复杂的嵌套对象。所以我们稍微修改了一下data对象:

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

不过直接这样子写的话还不能跑,因为在模板中使用<%profile.age%>的话,代码会被替换成data[‘profile.age'],结果是undefined。这样我们就不能简单地用replace函数,而是要用别的方法。如果能够在<%和%>之间直接使用Javascript代码就最好了,这样就能对传入的数据直接求值,像下面这样:

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); // outputs 3

fn可是一个货真价实的函数。它接受一个参数,函数体是console.log(arg + 1);。上述代码等价于下面的代码:

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

通过这种方法,我们可以根据字符串构造函数,包括它的参数和函数体。这不正是我们想要的嘛!不过先别急,在构造函数之前,我们先来看看函数体是什么样子的。按照之前的想法,这个模板引擎最终返回的应该是一个编译好的模板。还是用之前的模板字符串作为例子,那么返回的内容应该类似于:

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

当然啦,实际的模板引擎中,我们会把模板切分为小段的文本和有意义的Javascript代码。前面你可能看见我使用简单的字符串拼接来达到想要的效果,不过这并不是100%符合我们要求的做法。由于使用者很可能会传递更加复杂的Javascript代码,所以我们这儿需要再来一个循环,如下:

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的文章里写的逻辑,把所有的字符串放在一个数组里,在程序的最后把它们拼接起来。

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;
  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中去。有一个地方需要特别注意,那就是需要把code包含的双引号字符进行转义(escape)。否则生成的函数代码会出错。如果我们运行上面的代码,我们会在控制台里面看见如下的内容:

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不应该有引号啊,再来改改。

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

占位符的内容和一个布尔值一起作为参数传给add函数,用作区分。这样就能生成我们想要的函数体了。

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("");

剩下来要做的就是创建函数并且执行它。因此,在模板引擎的最后,把原本返回模板字符串的语句替换成如下的内容:

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

我们甚至不需要显式地传参数给这个函数。我们使用apply方法来调用它。它会自动设定函数执行的上下文。这就是为什么我们能在函数里面使用this.name。这里this指向data对象。

模板引擎接近完成了,不过还有一点,我们需要支持更多复杂的语句,比如条件判断和循环。我们接着上面的例子继续写。

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循环的那一行不应该被直接放到数组里面,而是应该作为脚本的一部分直接运行。所以我们在把内容添加到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';
}

这里我们新增加了一个正则表达式。它会判断代码中是否包含if、for、else等等关键字。如果有的话就直接添加到脚本代码中去,否则就添加到数组中去。运行结果如下:

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;
  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行!

Javascript 相关文章推荐
Javascript图像处理—虚拟边缘介绍及使用方法
Dec 27 Javascript
JS实现金额转换(将输入的阿拉伯数字)转换成中文的实现代码
Sep 30 Javascript
jQuery 获取兄弟元素的几种不错方法
May 23 Javascript
select多选 multiple的使用示例
Jun 16 Javascript
浅析javascript 定时器
Dec 23 Javascript
jquery实现鼠标经过显示下划线的渐变下拉菜单效果代码
Aug 24 Javascript
Web Uploader文件上传插件使用详解
May 10 Javascript
微信小程序使用wxParse解析html的方法教程
Jul 06 Javascript
layui对工具条进行选择性的显示方法
Sep 19 Javascript
vue实现跳转接口push 转场动画示例
Nov 01 Javascript
JavaScript实现简单随机点名器
Nov 21 Javascript
手写实现JS中的new
Nov 07 Javascript
关于延迟加载JavaScript
May 05 #Javascript
Javascript闭包(Closure)详解
May 05 #Javascript
javascript实现仿IE顶部的可关闭警告条
May 05 #Javascript
JS实现点击按钮后框架内载入不同网页的方法
May 05 #Javascript
JS实现随机乱撞彩色圆球特效的方法
May 05 #Javascript
jquery实现图片随机排列的方法
May 04 #Javascript
jquery实现的美女拼图游戏实例
May 04 #Javascript
You might like
PHP变量的定义、可变变量、变量引用、销毁方法
2013/12/20 PHP
PHP中的Streams详细介绍
2014/11/12 PHP
jQuery 使用手册(二)
2009/09/23 Javascript
自写简单JS判断是否已经弹出页面
2010/10/20 Javascript
IE和Firefox的Javascript兼容性总结[推荐收藏]
2011/10/19 Javascript
JavaScript高级程序设计 阅读笔记(十四) js继承机制的实现
2012/08/14 Javascript
ko knockoutjs动态属性绑定技巧应用
2012/11/14 Javascript
jquery form 隐藏的input 选择
2014/04/29 Javascript
关于javascript中dataset的问题小结
2015/11/16 Javascript
jQuery对象的链式操作用法分析
2016/05/10 Javascript
AngulaJS路由 ui-router 传参实例
2017/04/28 Javascript
vue2.0使用v-for循环制作多级嵌套菜单栏
2018/06/25 Javascript
JavaScript解决浮点数计算不准确问题的方法分析
2018/07/09 Javascript
为jquery的ajax请求添加超时timeout时间的操作方法
2018/09/04 jQuery
JS判断两个数组或对象是否相同的方法示例
2019/02/28 Javascript
JS常用排序方法实例代码解析
2020/03/03 Javascript
用python实现百度翻译的示例代码
2018/03/09 Python
Python常见数据结构之栈与队列用法示例
2019/01/14 Python
使用Python做定时任务及时了解互联网动态
2019/05/15 Python
pybind11在Windows下的使用教程
2019/07/04 Python
解决Python中pandas读取*.csv文件出现编码问题
2019/07/12 Python
python的移位操作实现详解
2019/08/21 Python
Django Admin后台添加数据库视图过程解析
2020/04/01 Python
Python基于Twilio及腾讯云实现国际国内短信接口
2020/06/18 Python
pytorch实现查看当前学习率
2020/06/24 Python
使用css实现android系统的loading加载动画
2019/07/25 HTML / CSS
AmazeUI 网格的实现示例
2020/08/13 HTML / CSS
AE美国鹰美国官方网站:American Eagle Outfitters
2016/08/22 全球购物
意大利香水和彩妆护肤品购物网站:Ditano
2017/08/13 全球购物
面向对象编程OOP的优点
2013/01/22 面试题
电子商务应届生自我鉴定
2014/01/13 职场文书
生物学专业求职信
2014/07/23 职场文书
教师党的群众路线教育实践活动个人整改方案
2014/10/31 职场文书
毕业生自荐材料范文
2014/12/30 职场文书
学校施工安全责任书
2015/01/29 职场文书
SQL SERVER实现连接与合并查询
2022/02/24 SQL Server