JavaScript模版引擎的基本实现方法浅析


Posted in Javascript onFebruary 15, 2016

模板分离了数据与展现,使得展现的逻辑和效果更易维护。利用javascript的Function对象,一步步构建一个极其简单的模板转化引擎

模板简介
模板通常是指嵌入了某种动态编程语言代码的文本,数据和模板通过某种形式的结合,可以变化出不同的结果。模板通常用来定义显示的形式,能够使得数据展现更为丰富,而且容易维护。例如,下面是一个模板的例子:

<ul>
 <% for(var i in items){ %>
 <li class='<%= items[i].status %>'><%= items[i].text %></li>
 <% } %>
</ul>

如果有如下items数据:

items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]

通过某种方式的结合,可以产生下面的Html代码:

<ul>
 <li class='done'>text1<li>
 <li class='pending'>text2<li>
 <li class='pending'>text3<li>
 <li class='processing'>text4<li>
</ul>

如果不使用模板,想要达到同样的效果,即将上面的数据展现成结果的样子,需要像下面这样做:

var temp = '<ul>';
for(var i in items){
 temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
}
temp += '</ul>';

可以看出使用模板有如下好处:

简化了html的书写
通过编程元素(比如循环和条件分支),对数据的展现更具有控制的能力
分离了数据与展现,使得展现的逻辑和效果更易维护
模板引擎
通过分析模板,将数据和模板结合在一起输出最后的结果的程序称为模板引擎,模板有很多种,相对应的模板引擎也有很多种。一种比较古老的模板称为ERB,在很多的web框架中被采用,比如:ASP.NET 、 Rails … 上面的例子就是ERB的例子。在ERB中两个核心的概念:evaluate和interpolate。表面上evaluate是指包含在<% %>中的部分,interpolate是指包含在<%= %>中的部分。从模板引擎的角度,evaluate中的部分不会直接输出到结果中,一般用于过程控制;而interpolate中的部分将直接输出到结果中。

从模板引擎的实现上看,需要依赖编程语言的动态编译或者动态解释的特性,以简化实现和提高性能。例如:ASP.NET利用.NET的动态编译,将模板编译成动态的类,并利用反射动态执行类中的代码。这种实现实际上是比较复杂的,因为C#是一门静态的编程语言,但是使用javascript可以利用Function,以极少的代码实现一个简易的模板引擎。本文就来实现一个简易的ERB模板引擎,以展现javascript的强大之处。

模板文本转化
针对上面的例子,回顾一下使用模板和不使用模板的差别:

模板写法:

<ul>
 <% for(var i in items){ %>
 <li class='<%= items[i].status %>'><%= items[i].text %></li>
 <% } %>
</ul>

非模板写法:

var temp = '<ul>';
for(var i in items){
 temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
}
temp += '</ul>';

仔细观察,实际上这两种方法十分“相似”,能够找到某种意义上的一一对应。如果能够将模板的文本变成代码执行,那么就能实现模板转化。在转化过程中有两个原则:

遇到普通的文本直接当成字符串拼接
遇到interpolate(即<%= %>),将其中的内容当成变量拼接在字符串中
遇到evaluate(即<% %>),直接当成代码
将上面的例子按照上述原则进行变换,再添加一个总的函数:

var template = function(items){
 var temp = '';
 //开始变换
 temp += '<ul>';
 for(var i in items){
 temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
 }
 temp += '</ul>';
}

最后执行这个函数,传入数据参数即可:

var result = template(items);

javascript动态函数
可见上面的转化逻辑其实十分简单,但是关键的问题是,模板是变化的,这意味着生成的程序代码也必须是在运行时生成并执行的。好在javascript有许多动态特性,其中一个强大的特性就是Function。 我们通常使用function关键字在js中声明函数,很少用Function。在js中function是字面语法,js的运行时会将字面的function转化成Function对象,所以实际上Function提供了更为底层和灵活的机制。

用 Function 类直接创建函数的语法如下:

var function_name = new Function(arg1, arg2, ..., argN, function_body)

例如:

//创建动态函数 
var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);");
//执行 
sayHi('Hello','World');

函数体和参数都能够通过字符串来创建!So cool!有了这个特性,可以将模板文本转化成函数体的字符串,这样就可以创建动态的函数来动态的调用了。

实现思路
首先利用正则式来描述interpolate和evaluate,括号用来分组捕获:

var interpolate_reg = /<%=([\s\S]+?)%>/g;
var evaluate_reg = /<%([\s\S]+?)%>/g;

为了对整个模板进行连续的匹配将这两个正则式合并在一起,但是注意,所有能够匹配interpolate的字符串都能匹配evaluate,所以interpolate需要有较高的优先级:

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g

设计一个函数用于转化模板,输入参数为模板文本字串和数据对象

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g
//text: 传入的模板文本字串
//data: 数据对象
var template = function(text,data){ ... }

使用replace方法,进行正则的匹配和“替换”,实际上我们的目的不是要替换interpolate或evaluate,而是在匹配的过程中构建出“方法体”:

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g
//text: 传入的模板文本字串
//data: 数据对象
var template = function(text,data){
 var index = 0;//记录当前扫描到哪里了
 var function_body = "var temp = '';";
 function_body += "temp += '";
 text.replace(matcher,function(match,interpolate,evaluate,offset){
 //找到第一个匹配后,将前面部分作为普通字符串拼接的表达式
 function_body += text.slice(index,offset);
 
 //如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组
 if(evaluate){
  function_body += "';" + evaluate + "temp += '";
 }
 //如果是<%= ... %>拼接字符串,interpolate就是捕获的分组
 if(interpolate){
  function_body += "' + " + interpolate + " + '";
 }
 //递增index,跳过evaluate或者interpolate
 index = offset + match.length;
 //这里的return没有什么意义,因为关键不是替换text,而是构建function_body
 return match;
 });
 //最后的代码应该是返回temp
 function_body += "';return temp;";
}

至此,function_body虽然是个字符串,但里面的内容实际上是一段函数代码,可以用这个变量来动态创建一个函数对象,并通过data参数调用:

var render = new Function('obj', function_body);
return render(data);

这样render就是一个方法,可以调用,方法内部的代码由模板的内容构造,但是大致的框架应该是这样的:

function render(obj){
 var temp = '';
 temp += ...
 ...
 return temp;
}

注意到,方法的形参是obj,所以模板内部引用的变量应该是obj:

<script id='template' type='javascript/template'>
 <ul>
 <% for(var i in obj){ %>
  <li class="<%= obj[i].status %>"><%= obj[i].text %></li>
 <% } %>
 </ul>
</script>

看似到这里就OK了,但是有个必须解决的问题。模板文本中可能包含\r \n \u2028 \u2029等字符,这些字符如果出现在代码中,会出错,比如下面的代码是错误的:

temp += '
 <ul>
 ' + ... ;

我们希望看到的应该是这样的代码:

temp += '\n \t\t<ul>\n' + ...;

这样需要把\n前面的\转义成\\即可,最终变成字面的\\n。

另外,还有一个问题是,上面的代码无法将最后一个evaluate或者interpolate后面的部分拼接进来,解决这个问题的办法也很简单,只需要在正则式中添加一个行尾的匹配即可:

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g;

相对完整的代码

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g


//模板文本中的特殊字符转义处理
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
var escapes = {
  "'":   "'",
  '\\':   '\\',
  '\r':   'r',
  '\n':   'n',
  '\t':   't',
  '\u2028': 'u2028',
  '\u2029': 'u2029'
 };

//text: 传入的模板文本字串
//data: 数据对象
var template = function(text,data){
 var index = 0;//记录当前扫描到哪里了
 var function_body = "var temp = '';";
 function_body += "temp += '";
 text.replace(matcher,function(match,interpolate,evaluate,offset){
 //找到第一个匹配后,将前面部分作为普通字符串拼接的表达式
 //添加了处理转义字符
 function_body += text.slice(index,offset)
  .replace(escaper, function(match) { return '\\' + escapes[match]; });

 //如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组
 if(evaluate){
  function_body += "';" + evaluate + "temp += '";
 }
 //如果是<%= ... %>拼接字符串,interpolate就是捕获的分组
 if(interpolate){
  function_body += "' + " + interpolate + " + '";
 }
 //递增index,跳过evaluate或者interpolate
 index = offset + match.length;
 //这里的return没有什么意义,因为关键不是替换text,而是构建function_body
 return match;
 });
 //最后的代码应该是返回temp
 function_body += "';return temp;";
 var render = new Function('obj', function_body);
 return render(data);
}

调用代码可以是这样:

<script id='template' type='javascript/template'>
 <ul>
 <% for(var i in obj){ %>
  <li class="<%= obj[i].status %>"><%= obj[i].text %></li>
 <% } %>
 </ul>
</script>

...

var text = document.getElementById('template').innerHTML;
var items = [
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
];
console.log(template(text,items));

可见,我们只用了很少的代码就实现了一个简易的模板。

遗留的问题
还有几个细节的问题需要注意:

  • 因为<%或者%>都是模板的边界字符,如果模板需要输出<%或者%>,那么需要设计转义的办法
  • 如果数据对象中包含有null,显然不希望最后输出'null',所以需要在function_body的代码中考虑null的情况
  • 在模板中每次使用obj的形参引用数据,可能不太方便,可以在function_body添加with(obj||{}){...},这样模板中可以直接使用obj的属性
  • 可以设计将render返回出去,而不是返回转化的结果,这样外部可以缓存生成的函数,以提高性能
Javascript 相关文章推荐
Mootools 1.2教程 正则表达式
Sep 15 Javascript
jQuery中after的两种用法实例
Jul 03 Javascript
jquery中交替点击事件的实现代码
Feb 14 Javascript
javascript事件冒泡和事件捕获详解
May 26 Javascript
javascript合并表格单元格实例代码
Jan 03 Javascript
jQuery隐藏和显示效果实现
Apr 06 Javascript
vue,angular,avalon这三种MVVM框架优缺点
Apr 27 Javascript
js事件源window.event.srcElement兼容性写法(详解)
Nov 25 Javascript
Vue.js鼠标悬浮更换图片功能
May 17 Javascript
vue中element组件样式修改无效的解决方法
Feb 03 Javascript
JS实现的贪吃蛇游戏案例详解
May 01 Javascript
更强大的vue ssr实现预取数据的方式
Jul 19 Javascript
在ASP.NET MVC项目中使用RequireJS库的用法示例
Feb 15 #Javascript
一道常被人轻视的web前端常见面试题(JS)
Feb 15 #Javascript
获取阴历(农历)和当前日期的js代码
Feb 15 #Javascript
极易被忽视的javascript面试题七问七答
Feb 15 #Javascript
在JavaScript中使用JSON数据
Feb 15 #Javascript
三分钟带你玩转jQuery.noConflict()
Feb 15 #Javascript
轻松搞定jQuery.noConflict()
Feb 15 #Javascript
You might like
动易数据转成dedecms的php程序
2007/04/07 PHP
PHP程序级守护进程的实现与优化的使用概述
2013/05/02 PHP
在Laravel框架里实现发送邮件实例(邮箱验证)
2016/05/20 PHP
浅谈PHP中pack、unpack的详细用法
2018/03/12 PHP
简单实用jquery版三级联动select示例
2013/07/04 Javascript
js实现省市联动效果的简单实例
2014/02/10 Javascript
jQuery插件EnPlaceholder实现输入框提示文字
2015/06/05 Javascript
基于Jquery制作图片文字排版预览效果附源码下载
2015/11/18 Javascript
JS根据浏览器窗口大小实时动态改变网页文字大小的方法
2016/02/25 Javascript
JavaScript获取当前时间向前推三个月的方法示例
2017/02/04 Javascript
深入理解ES6 Promise 扩展always方法
2017/09/26 Javascript
基于 Vue 实现一个酷炫的 menu插件
2017/11/14 Javascript
微信小程序使用checkbox显示多项选择框功能【附源码下载】
2017/12/11 Javascript
微信小程序 MinUI组件库系列之badge徽章组件示例
2018/08/20 Javascript
浅谈Vue.js 关于页面加载完成后执行一个方法的问题
2019/04/01 Javascript
vue.js自定义组件实现v-model双向数据绑定的示例代码
2020/01/08 Javascript
[05:13]2018DOTA2亚洲邀请赛主赛事第二日战况回顾 LGD、VG双雄携手晋级
2018/04/05 DOTA
python判断一个集合是否包含了另外一个集合中所有项的方法
2015/06/30 Python
浅谈python中的getattr函数 hasattr函数
2016/06/14 Python
对Python中的@classmethod用法详解
2018/04/21 Python
Django实战之用户认证(初始配置)
2018/07/16 Python
Django配置MySQL数据库的完整步骤
2019/09/07 Python
python列表插入append(), extend(), insert()用法详解
2019/09/14 Python
Numpy与Pytorch 矩阵操作方式
2019/12/27 Python
Python写捕鱼达人的游戏实现
2020/03/31 Python
python查询MySQL将数据写入Excel
2020/10/29 Python
草莓网英国官网:Strawberrynet UK
2017/02/12 全球购物
全球领先的中国制造商品在线批发平台:DHgate
2020/01/28 全球购物
影视艺术学院毕业生自荐信
2013/11/13 职场文书
《中华少年》教学反思
2014/02/15 职场文书
口才训练演讲稿范文
2014/09/16 职场文书
横店影视城导游词
2015/02/06 职场文书
导游词范文
2015/02/13 职场文书
物业客服专员岗位职责
2015/04/07 职场文书
教你使用Python pypinyin库实现汉字转拼音
2021/05/27 Python
Python OpenCV 图像平移的实现示例
2021/06/04 Python