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 相关文章推荐
JavaScript 三种创建对象的方法
Oct 16 Javascript
jquery鼠标放上去显示悬浮层即弹出定位的div层
Apr 25 Javascript
jquery中JSON的解析方式
Mar 16 Javascript
jquery中radio checked问题
Mar 16 Javascript
Eclipse引入jquery报错如何解决
Dec 01 Javascript
jQuery如何封装输入框插件
Aug 19 Javascript
原生js实现键盘控制div移动且解决停顿问题
Dec 05 Javascript
基于Bootstrap实现城市三级联动
Nov 23 Javascript
jQuery封装animate.css的实例
Jan 04 jQuery
jQuery实现下拉菜单动态添加数据点击滑出收起其他功能
Jun 14 jQuery
深入理解vue-class-component源码阅读
Feb 18 Javascript
微信jssdk踩坑之签名错误invalid signature
May 19 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
解析PHP中$_FILES的使用以及注意事项
2013/07/05 PHP
php实现根据字符串生成对应数组的方法
2014/09/22 PHP
PHP实现将视频转成MP4并获取视频预览图的方法
2015/03/12 PHP
php实现数字补零的方法总结
2018/09/12 PHP
理解Javascript_07_理解instanceof实现原理
2010/10/15 Javascript
腾讯UED 漂亮的提示信息效果代码
2011/09/12 Javascript
深入理解JavaScript系列(10) JavaScript核心(晋级高手必读篇)
2012/01/15 Javascript
JavaScript高级程序设计(第3版)学习笔记3 js简单数据类型
2012/10/11 Javascript
JQuery中DOM事件合成用法实例分析
2015/06/13 Javascript
JavaScript中用于生成随机数的Math.random()方法
2015/06/15 Javascript
基于JavaScript Array数组方法(新手必看篇)
2016/08/20 Javascript
使用Node.js给图片加水印的方法
2016/11/15 Javascript
利用node.js写一个爬取知乎妹纸图的小爬虫
2017/05/03 Javascript
vue父组件向子组件动态传值的两种方法
2017/11/11 Javascript
jQuery利用cookie 实现本地收藏功能(不重复无需多次命名)
2019/11/07 jQuery
[56:17]NB vs Infamous 2019国际邀请赛淘汰赛 败者组 BO3 第三场 8.22
2019/09/05 DOTA
Python使用正则匹配实现抓图代码分享
2015/04/02 Python
利用Python开发微信支付的注意事项
2016/08/19 Python
Django中如何防范CSRF跨站点请求伪造攻击的实现
2019/04/28 Python
python同时替换多个字符串方法示例
2019/09/17 Python
Windows+Anaconda3+PyTorch+PyCharm的安装教程图文详解
2020/04/03 Python
pandas参数设置的实用小技巧
2020/08/23 Python
python对输出的奇数偶数排序实例代码
2020/12/04 Python
css3打造一款漂亮的卡哇伊按钮
2013/03/20 HTML / CSS
用CSS3实现背景渐变的方法
2015/07/14 HTML / CSS
德国消费电子产品购物网站:Guter Kauf
2020/09/15 全球购物
高等教育学自荐书范文
2014/02/10 职场文书
《我爱祖国》演讲稿1000字
2014/09/26 职场文书
党员干部四风问题整改措施思想汇报
2014/10/12 职场文书
2014年政风行风工作总结
2014/11/22 职场文书
话题作文之学会尊重
2019/12/16 职场文书
如何在Python项目中引入日志
2021/05/31 Python
使用@Value值注入及配置文件组件扫描
2021/07/09 Java/Android
Javascript设计模式之原型模式详细
2021/10/05 Javascript
canvas实现贪食蛇的实践
2022/02/15 Javascript
PostgreSQL 插入INSERT、删除DELETE、更新UPDATE、事务transaction
2022/04/12 PostgreSQL