JavaScript模板引擎原理与用法详解


Posted in Javascript onDecember 24, 2018

本文实例讲述了JavaScript模板引擎原理与用法。分享给大家供大家参考,具体如下:

一、前言

什么是模板引擎,说的简单点,就是一个字符串中有几个变量待定。比如:

var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';

通过模板引擎函数把数据塞进去,

var data = {
  "name": "Barret Lee",
  "age": "20"
};
var result = tplEngine(tpl, data);
//Hei, my name is Barret Lee, and I'm 20 years old.

那这玩意儿有什么作用呢?其实他就是一个预处理器(preprocessor),搞php开发的童鞋对Smarty必然是十分熟悉,Smarty是一个php模板引擎,tpl中待处理的字符通过数据匹配然后输出相应的html代码,加之比较给力的缓存技术,其速度和易用性是非常给力的!JS Template也是一样的,我们的数据库里保存着数以千万计的数据,而每一条数据都是通过同一种方式输入,就拿上面的例子来说,我们不可能在数据库里存几千条"Hei, my name...",而是只保存对应的name和age,通过模板输出结果。

JS模板引擎应该做哪些事情?看看下面一串代码:

var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' + 
  'var post = posts[i]; %>' +
  '<% if(!post.expert){ %>' +
    '<span>post is null</span>' +
  '<% } else { %>' +
    '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post.expert %> at <% post.time %></a>' +
  '<% } %>' +
'<% } %>';

一个基本的模板引擎至少可以保证上面的代码可以正常解析。如送入的数据是:

var data = {
  "posts": [{
    "expert": "content 1",
    "time": "yesterday"
  },{
    "expert": "content 2",
    "time": "today"
  },{
    "expert": "content 3",
    "time": "tomorrow"
  },{
    "expert": "",
    "time": "eee"
  }]
};

可以输出:

<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 1 at yesterday</a>
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 2 at today</a>
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 3 at tomorrow</a>
<span>post is null</span>

下面就具体说说这个模板引擎的原理是啥样的。

二、JS模板引擎的实现原理

1.正则抠出要匹配的内容

针对这一串代码,通过正则获取内容

var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var data = {
  "name": "Barret Lee",
  "age": "20"
};

最简单的方式就是通过replace函数了:

var result = tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
  return data[s1];
});

通过正则替换,我们很轻松的拿到了result,你可以去试一试,他正是我们想要的结果。但是这里又有了一个问题,改一下data和tpl,

var tpl = 'Hei, my name is <%name%>, and I\'m <%info.age%> years old.';
var data = {
  "name": "Barret Lee",
  "info": { age": "20"}
};

再用上面的方式去获取结果,呵呵,不行了吧~ 这里data["info.age"]本身就是undefined,所以我们需要换一种方式来处理这个问题,那就是将它转换成真正的JS代码。如:

return 'Hei, my name is ' + data.name + ', and I\'m ' + data.info.age' + ' years old.'

但是接着又有一个问题来了,当我们的代码中出现for循环和if的时候,上面的转换明显是不起作用的,如:

var tpl = 'Posts: ' +
     '<% for(var i = 0; i < post.length; i++) {'+
      '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post[i].expert %></a>' +
     '<% } %>'

如果继续采用上面的方式,得到的结果便是:

return 'Posts: ' +
    for(var i = 0; i < post.length; i++) { +
     '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >' + post[i].exper + '</a>' +
    }

这显然不是我们愿意看到的,稍微观察一下上面的结构,如果可以返回一个这样的结果也挺不错哦:

'Posts: '
for(var i = 0; i < post.length; i++) {
  '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >' + post[i].exper + '</a>'
}

但是我们需要得到的是一个字符串,而不是上面这样零散的片段,因此可以把这些东西装入数组中。

2.装入数组

var r = [];
r.push('Posts: ' );
r.push(for(var i = 0; i < post.length; i++) {);
r.push('<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >');
r.push(post[i].exper);
r.push('</a>');
r.push(});

有人看到上面的代码就要笑了,第三行和最后一行代码的逻辑明显是不正确的嘛,那肿么办呢?呵呵,很简单,不放进去就行了呗,

var r = [];
r.push('Posts: ' );
for(var i = 0; i < post.length; i++) {
  r.push('<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >');
  r.push(post[i].exper);
  r.push('</a>');
}

这样的逻辑就十分完善了,不存在太多的漏洞,但是这个转化的过程是如何实现的?我们必须还是要写一个解析的模板函数出来。

3.分辨js逻辑部分

var r = [];
tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
  //完蛋了,这里貌似又要回到上面那可笑的逻辑有错误的一步啦... 该怎么处理比较好?
});

完蛋了,这里貌似又要回到上面那可笑的逻辑有错误的一步啦... 该怎么处理比较好?我们知道,JS给我们提供了构造函数的“类”,

var fn = new Function("data",
  "var r = []; for(var i in data){ r.push(data[i]); } return r.join(' ')");
fn({"name": "barretlee", "age": "20"}); // barretlee 20

知道了这个就好办了,我们可以把逻辑部分和非逻辑部分的代码链接成一个字符串,然后利用类似fn的函数直接编译代码。而/<%([^%>]+)?%>/g,这一个正则只能把逻辑部分匹配出来,要想把所有的代码都组合到一起,必须还得匹配非逻辑部分代码。replace函数虽然很强大,他也可以完成这个任务,但是实现的逻辑比较晦涩,所以我们换另外一种方式来处理。

先看一个简单的例子:

var reg = /<%([^%>]+)?%>/g;
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var match = reg.exec(tpl);
console.log(match);

看到的是:

[
  0: "<%name%>",
  1: name,
  index: 16,
  input: "Hei, my name is <%name%>, and I'm <%age%> years old."
  length: 2
]

这。。。我们可是想得到所有的匹配啊,他竟然只获取了name而忽略了后面的age,好吧,对正则稍微熟悉点的童鞋一定会知道应该这样处理:

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

关于正则表达式的内容就不在这里细说了,有兴趣的同学可以多去了解下match,exec,search等正则的相关函数。这里主要是靠match的index属性来定位遍历位置,然后利用while循环获取所有的内容。

4.引擎函数

所以我们的引擎函数雏形差不多就出来了:

var tplEngine = function(tpl, data){
  var reg = /<%([^%>]+)?%>/g,
      code = 'var r=[];\n',
      cursor = 0; //主要的作用是定位代码最后一截
  var add = function(line) {
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
  };
  while(match = reg.exec(tpl)) {
    add(tpl.slice(cursor, match.index)); //添加非逻辑部分
    add(match[1]); //添加逻辑部分 match[0] = "<%" + match[1] + "%>";
    cursor = match.index + match[0].length;
  }
  add(tpl.substr(cursor, tpl.length - cursor)); //代码的最后一截 如:" years old."
  code += 'return r.join("");'; // 返回结果,在这里我们就拿到了装入数组后的代码
  console.log(code);
  return tpl;
};

这样一来,测试一个小demo:

 

var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' + 
    'var post = posts[i]; %>' +
    '<% if(!post.expert){ %>' +
      '<span>post is null</span>' +
    '<% } else { %>' +
      '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post.expert %> at <% post.time %></a>' +
    '<% } %>' +
  '<% } %>';
tplEngine(tpl, data);

返回的结果让人很满意:

var r=[];
r.push("");
r.push(" for(var i = 0; i < this.posts.length; i++) {var post = posts[i]; ");
r.push("");
r.push(" if(!post.expert){ ");
r.push("<span>post is null</span>");
r.push(" } else { ");
r.push("<a href=\"#\">");
r.push(" post.expert ");
r.push(" at ");
r.push(" post.time ");
r.push("</a>");
r.push(" } ");
r.push("");
r.push(" } ");
r.push("");
return r.join("");

不过我们并需要for,if,switch等这些东西也push到r数组中去,所以呢,还得改善下上面的代码,如果在line中发现了包含js逻辑的代码,我们就不应该让他进门:

regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var add = function(line, js) {
  js? code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n' :
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};

所以我们只剩下最后一步工作了,把data扔进去!

5.把data扔进去

没有比完成这东西更简单的事情啦,通过上面对Function这个函数的讲解,大家应该也知道怎么做了。

return new Function(code).apply(data);

使用apply的作用就是让code中的一些变量作用域绑定到data上,不然作用域就会跑到global上,这样得到的数据索引就会出问题啦~ 当然我们可以再优化一下:

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

把回车换行以及tab键都给匹配掉,让代码更加干净一点。那么最终的代码就是:

var tplEngine = function(tpl, data) {
  var reg = /<%([^%>]+)?%>/g,
    regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
    code = 'var r=[];\n',
    cursor = 0;
  var add = function(line, js) {
    js? (code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n') :
      (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
    return add;
  }
  while(match = reg.exec(tpl)) {
    add(tpl.slice(cursor, match.index))(match[1], true);
    cursor = match.index + match[0].length;
  }
  add(tpl.substr(cursor, tpl.length - cursor));
  code += 'return r.join("");';
  return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
};

三、应用场景

毕竟是前端代码,所以写出来是要为前端服务的,平时我们处理的一般是一个html的模板,通常的情况下,模板代码是放在script标签或者textarea中,所以首先是要获取到这里头的东西,然后再来做解析。

var barretTpl = function(str, data) {
  //获取元素
  var element = document.getElementById(str);
  if (element) {
    //textarea或input则取value,其它情况取innerHTML
    var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;
    return tplEngine(html, data);
  } else {
    //是模板字符串,则生成一个函数
    //如果直接传入字符串作为模板,则可能变化过多,因此不考虑缓存
    return tplEngine(str, data);
  }
  var tplEngine = function(tpl, data) {
    // content above
  };
};

这样一来就更加简单了,使用方式就是 barretTpl(str, data), 这里的str可以是模板代码,也可以是一个DOM元素的id~

四、优化以及功能拓展

总共就三四十行代码,完成的东西肯定是一个简洁版的,不过对于一个简单的页面而言,这几行代码已经足够使用了,如果还想对他做优化,可以从这几个方面考虑:

  • 优化获取的模板代码,比如去掉行尾空格等
  • 符号转义,如果我们想输出<span>hehe</span>类似这样的源代码,在push之前必须进行转义
  • 代码缓存,如果一个模板会经常使用,可以将它用一个数组缓存在barretTpl闭包内
  • 用户自己设置分隔符

更多关于JavaScript相关内容可查看本站专题:《javascript面向对象入门教程》、《JavaScript切换特效与技巧总结》、《JavaScript查找算法技巧总结》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

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

Javascript 相关文章推荐
Jquery中显示隐藏的实现代码分析
Jul 26 Javascript
用dtree实现树形菜单 dtree使用说明
Oct 17 Javascript
HTML复选框和单选框 checkbox和radio事件介绍
Dec 12 Javascript
解析JavaScript中的不可见数据类型
Dec 02 Javascript
file控件选择上传文件确定后触发的js事件是哪个
Mar 17 Javascript
jQuery中extend()和fn.extend()方法详解
Jun 03 Javascript
JavaScript中函数表达式和函数声明及函数声明与函数表达式的不同
Nov 15 Javascript
Avalonjs 实现简单购物车功能(实例代码)
Feb 07 Javascript
jQuery使用动画队列自定义动画操作示例
Jun 16 jQuery
Angular动画实现的2种方式以及添加购物车动画实例代码
Aug 09 Javascript
JS获取月的第几周和年的第几周实例代码
Dec 05 Javascript
微信小程序如何实现全局重新加载
Jun 05 Javascript
jQuery实现的简单日历组件定义与用法示例
Dec 24 #jQuery
原生js实现Flappy Bird小游戏
Dec 24 #Javascript
node错误处理与日志记录的实现
Dec 24 #Javascript
详解如何在vscode里面调试js和node.js的方法步骤
Dec 24 #Javascript
@angular前端项目代码优化之构建Api Tree的方法
Dec 24 #Javascript
微信小程序获取用户openid的实现
Dec 24 #Javascript
vue-router启用history模式下的开发及非根目录部署方法
Dec 23 #Javascript
You might like
兼容PHP5的PHP目录管理函数库
2008/07/10 PHP
PHP计划任务之关闭浏览器后仍然继续执行的函数
2010/07/22 PHP
Netbeans 8.2将支持PHP7 更精彩
2016/06/13 PHP
PHP模型Model类封装数据库操作示例
2019/03/14 PHP
用Javascript读取中文COOKIE的解决办法
2007/02/15 Javascript
一个css与js结合的下拉菜单支持主流浏览器
2014/10/08 Javascript
jQuery+CSS3实现树叶飘落特效
2015/02/01 Javascript
php利用curl获取远程图片实现方法
2015/10/26 Javascript
jQuery判断浏览器并动态调整select宽度的方法
2016/03/02 Javascript
JS动态创建元素的两种方法
2016/04/20 Javascript
JS+Canvas 实现下雨下雪效果
2016/05/18 Javascript
基于BootStrap的Metronic框架实现页面链接收藏夹功能按钮移动收藏记录(使用Sortable进行拖动排序)
2016/08/29 Javascript
通过AngularJS实现图片上传及缩略图展示示例
2017/01/03 Javascript
node文件批量重命名的方法示例
2017/10/23 Javascript
详解React native fetch遇到的坑
2018/08/30 Javascript
在vue里使用codemirror遇到的问题
2018/11/01 Javascript
jQuery分组选择器简单用法示例
2019/04/04 jQuery
p5.js码绘“跳动的小正方形”的实现代码
2019/10/22 Javascript
如何在JS文件中获取Vue组件
2020/09/16 Javascript
python 性能提升的几种方法
2016/07/15 Python
Python爬虫实战:分析《战狼2》豆瓣影评
2018/03/26 Python
set在python里的含义和用法
2019/06/24 Python
python基于event实现线程间通信控制
2020/01/13 Python
Python中join()函数多种操作代码实例
2020/01/13 Python
Python通过len函数返回对象长度
2020/10/22 Python
Habitat家居英国官方网站:沙发、家具、照明、厨房和户外
2019/12/12 全球购物
美国专业消费电子及摄影器材网站:B&H Photo Video
2019/12/18 全球购物
生物学学生自我评价
2014/01/17 职场文书
违反课堂纪律检讨书
2014/01/19 职场文书
入党自荐书范文
2015/03/05 职场文书
离职员工给领导和同事的感谢信
2015/11/03 职场文书
2016年国庆节新闻稿范文
2015/11/25 职场文书
《卧薪尝胆》读后感3篇
2019/12/26 职场文书
聊聊pytorch测试的时候为何要加上model.eval()
2021/05/23 Python
Java字符缓冲流BufferedWriter
2022/04/09 Java/Android
Win11怎么添加用户?Win11添加用户账户的方法
2022/07/15 数码科技