详解Javascript模板引擎mustache.js
本文总结它的使用方法和一些使用心得,内容不算很高深,纯粹是入门内容,看看即可。不过要是你还没有用过此类的javascript引擎库,那么本文还是值得你一读的,相信在你了解完它强大的功能和简单用法之后,一定会迫不及待地将之用于你的工作当中。
1. 从一个简单真实的需求讲起
目前公司做了一个统一的开发平台,后台封装了MVC的接口和数据增删改查的接口,前端我自己用bootstrap+手写各类组件的方式弄了一套开发框架;集成了CAS,在CAS的基础上,首先做了一套统一权限管理系统,这个系统是我们开发平台的第一个子系统,用来管理配置所有子系统的菜单和授权以及管理整个公司的组织结构和用户,后来我们又陆陆续续地开发了业务系统A和业务系统B。由于这三个子系统对应的是三个java工程,最终部署的时候,在tomcat里部署了三个应用,现在有一个需求是:
- 1)在每个系统里登录之后,点击系统名称,可以展开一个下拉菜单,显示所有有权限的子系统;
- 2)然后用户点击其它子系统,就可以切换到所选中的系统去,到了其它系统之后,由于都做了这个下拉菜单,所以也可以再从该系统切换回来;
- 3)如果用户只有一个系统的权限,则不显示下拉菜单。
需求其实挺简单,原型大概是这个样子:
功能实现方法是,在每个子系统登录完成之后,调用获取系统列表的接口,用js渲染一个下拉菜单出来,该接口返回的格式为:
data: [ { "sortOrder": 1, "isCurrent": true, "systemHttpUrl": "http://xxxx:8080/permission", "systemName": "统一权限管理系统" }, { "sortOrder": 2, "isCurrent": false, "systemHttpUrl": "http://xxxx:8080/systemA", "systemName": "业务系统A" }, { "sortOrder": 3, "isCurrent": false, "systemHttpUrl": "http://xxxx:8080/systemB", "systemName": "业务系统B" } ]
如果我们不采用模板引擎,那么传统的方式去解析这个数据并把它转变成html串的方法通常是:
function data2Html(data) { data = data || []; var html = ['<ul class="nav navbar-nav navbar-left nav-system">', ' <li class="dropdown">', ' <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown" title="切换系统">'], l = data.length; if(l < 2) { l == 1 && html.push(data[0].systemName || ''); html.push('</a></li></ul>'); return html.join(''); } var curSysAry = data.filter(function(s){ return s.isCurrent; }); html.push(curSysAry[0].systemName + ' <i class="fa fa-caret-down"></i></a><ul class="dropdown-menu">'); data.sort(function(a, b){ return a.sortOrder - b.sortOrder;}); for(var i = 0; i < l; i++) { i && html.push('<li role="separator" class="divider"></li>'); html.push('<li><a href="' + data[i].systemHttpUrl + '" target="_self">' + data[i].systemName + '</a></li>'); } html.push('</ul></li></ul>'); return html.join(''); }
这种拼接字符串的方式有诸多弊端:
- 1)麻烦,尤其是拼接逻辑复杂,拼接的串很长时;
- 2)不易维护,稍有不慎就会弄错标签的对应关系;
- 3)结构不清晰。
能够简化这个场景的工具就是模板引擎,模板引擎的技术后台最先有,如果你用过jsp,就一定知道jsp也就是一个模板,用来解析呈现数据用的,其它后台模板引擎还有velocity和freemarker等等。前端的模板引擎也有很多,mustache.js算是比较流行的一个,git上有8000多个赞,如果这个问题我们用mustache.js来做,就可以变成这样:
//通过一些根据属性名称对应的标记定义模板 var _template = [ '<ul class="nav navbar-nav navbar-left nav-system">', ' <li class="dropdown">', ' <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown" title="切换系统">', ' {{curSystemName}} {{#multiple}}<i class="fa fa-caret-down"></i>{{/multiple}}', ' </a>', ' {{#multiple}}<ul class="dropdown-menu">', ' {{#systems}}', ' {{^first}}<li role="separator" class="divider"></li>{{/first}}', ' <li>', ' <a href="{{{systemHttpUrl}}}" target="_self">{{systemName}}</a>', ' </li>', ' {{/systems}}', ' </ul>{{/multiple}}', ' </li>', '</ul>' ].join(''); //初始化这个模板 Mustache.parse(_template); function data2Html(data) { data = data || []; var curSysAry = data.filter(function(s){ return s.isCurrent; }); data.sort(function(a, b){ return a.sortOrder - b.sortOrder;}); data = data.map(function(s, i){s.first = i == 0; return s}); //模板渲染成字符串 return Mustache.render(_template, { curSystemName: curSysAry.length ? curSysAry[0].systemName : '', multiple: !!data.length, systems: data }); }
对比两个代码,会发现后面的代码,相对于前面的有以下这些优点:
- 1)结构清晰,所有待渲染的html都定义在一个位置,而且没有任何拼接的现象;
- 2)逻辑清晰,那些在模板里的标记,实际上与模板渲染时传进去的对象的属性名称都是对应的;
- 3)易维护,要增删标签都只用调整模板对应的数组就行了。
通过这个例子,应该能对模板引擎有了一个大概的认识,这类工具在前端开发中越来越普遍,尤其是前后端分离的应用中用的更多,已经是这类应用的基础架构的内容了。mustache.js是一个非常简单易用的引擎实现,接下来的内容将会对这个工具常用的模板配置一一介绍并配合实用的例子说明,希望能让你更喜欢这个工具:)
2. mustache的用法
mustache的使用非常简单,先通过script标签引入它的js文件,然后按狼蚁网站SEO优化的步骤操作:
1)定义模板字符串
定义模板有2种方式,方式一就是在前面部分中看到的,直接用[...].join('')的方式在js代码中定义,方式二直接把模板内容用script定义在html中:
<script id="tpl" type="text/html"> Hello {{name}}! </script>
然后在编译模板之前,通过获取tpl的innerHTML定义原始模板串:
var tpl = document.getElementById('tpl').innerHTML.trim();
具体要用哪种方式来定义模板,可以参考狼蚁网站SEO优化的建议:
如果这个模板要用于多个页面,推荐把模板定义在js代码中;如果这个模板只用于当前页面,推荐直接定义到script标签中,管理更方便。
2)预编译模板
假设原始模板串已经定义好,并用tpl变量来引用,就可以通过狼蚁网站SEO优化的代码来预编译模板:
Mustache.parse(tpl);
要注意的是,经过预编译之后的tpl已经不再是原来的模板串了,连数据类型都变成数组类型了,这都是预编译的结果。
3)渲染模板
渲染方式很简单:
var htmlAfterRendered = Mustache.render(tpl1, obj);
obj引用的是一个数据源对象,mustache会把模板中那些属性标签,根据约定的规则,替换成对象的内容。htmlAfterRendered就是替换之后的字符串,你可以用它完成你需要的DOM操作。
3. mustache的思想
mustache的核心是标签和logic-less。从前面的代码中可以看到定义模板时,使用了{{name}}这样的标记,还有{{#systems}}{{/systems}},这就是mustache的标签,只不过它用{{}}替代了<>,以免跟html标签的<>混淆。logic-less,可以翻译为轻逻辑,因为在定义模板的时候不会用到if-else,不会有循环式的编码,一切都用标签来解决,它的标签非常简单,但是能应付所有场景,阅读完本文之后,你会惊讶地发现,只要用以下几个标签几乎就能解决所有的问题:
{{prop}}
{{{prop}}}
{{#prop}}{{/prop}}
{{^prop}}{{/prop}}
4. {{prop}}标签
这个标签是mustache模板里用的最多的,可以将数据源对象上prop属性对应的值,转换成字符串进行输出,以下是同一个属性,对应不同类型的值,在经过mustache渲染之后输出结果的测试(前后那根短横线的作用是为了让这个标签的渲染结果看起来更清楚):
<script id="tpl1" type="text/html"> -{{prop}}- </script> <script> var tpl1 = document.getElementById('tpl1').innerHTML.trim(); Mustache.parse(tpl1); //测试falsy值 console.log(Mustache.render(tpl1, {prop: ''}));//-- console.log(Mustache.render(tpl1, {prop: 0}));//-0- console.log(Mustache.render(tpl1, {prop: null}));//-- console.log(Mustache.render(tpl1, {prop: undefined}));//-- console.log(Mustache.render(tpl1, {prop: false}));//-false- console.log(Mustache.render(tpl1, {prop: NaN}));//-NaN- //测试简单对象 console.log(Mustache.render(tpl1, {prop: {name: 'jason'}}));//-[object Object]- //测试数组 console.log(Mustache.render(tpl1, {prop: [{name: 'jason'}, {name: 'frank'}]}));//-[object Object],[object Object]- //测试日期对象 console.log(Mustache.render(tpl1, {prop: new Date()}));//-Mon Jan 18 2016 15:38:46 GMT+0800 (中国标准时间)- //测试自定义toString的简单对象 var obj1 = {name: 'jason'}; obj1.toString = function () { return this.name; }; console.log(Mustache.render(tpl1, {prop: obj1}));//-jason- //测试boolean number string console.log(Mustache.render(tpl1, {prop: true}));//-true- console.log(Mustache.render(tpl1, {prop: 1.2}));//-1.2- console.log(Mustache.render(tpl1, {prop: 'yes'}));//-yes- //测试function console.log(Mustache.render(tpl1, { prop: function () { } }));//-- console.log(Mustache.render(tpl1, { prop: function () { return 'it\'s a fun' } }));//-it's a fun- console.log(Mustache.render(tpl1, { prop: function () { return false; } }));//-false- console.log(Mustache.render(tpl1, { prop: function(){ return function (text, render) { return "<b>" + render(text) + "</b>" }; } })); //-function (text, render) { // return "<b>" + render(text) + "</b>" /