详解Javascript模板引擎mustache.js
本文详细介绍前端流行模板引擎mustache.js的基础用法与核心特性。从实际业务场景出发,对比传统字符串拼接与模板引擎的差异,阐述mustache.js在结构清晰度和维护性上的优势。深入解析{{prop}}、{{#prop}}、{{^prop}}等核心标签的渲染逻辑,包括条件判断、列表迭代、上下文栈管理及函数回调机制,帮助开发者快速掌握并应用于前后端分离项目。
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较为流行,GitHub上有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是一个简单易用的引擎实现,下文将逐一介绍常用模板配置并配合实用例子说明,希望能让你更喜欢这个工具:)
mustache基础用法与配置
2. mustache的用法 mustache使用非常简单,先通过script标签引入其JS文件,然后按以下步骤操作: 1)定义模板字符串 定义模板有2种方式,方式一即前文部分所见,直接用[...].join('')方式在JS代码中定义,方式二直接将模板内容用script定义在HTML中:
<script id="tpl" type="text/html">
Hello {{name}}!
</script>
然后在编译模板前,通过获取tpl的innerHTML定义原始模板串:
var tpl = document.getElementById('tpl').innerHTML.trim();
具体采用哪种方式定义模板,可参考以下建议: 若此模板用于多个页面,推荐将模板定义在JS代码中;若此模板仅用于当前页面,推荐直接定义到script标签中,管理更方便。 2)预编译模板 假设原始模板串已定义好,并用tpl变量引用,即可通过以下代码预编译模板:
Mustache.parse(tpl);
需注意,经预编译后的tpl已非原模板串,连数据类型都变为数组类型,这都是预编译的结果。 3)渲染模板 渲染方式很简单:
var htmlAfterRendered = Mustache.render(tpl1, obj);
obj引用的是一个数据源对象,mustache会将模板中那些属性标签,根据约定规则,替换成对象内容。htmlAfterRendered即为替换后的字符串,你可用它完成所需的DOM操作。
mustache核心标签与逻辑-less设计
3. mustache的思想 mustache的核心是标签和logic-less。从前文代码可见,定义模板时使用了{{name}}这样的标记,还有{{#systems}}{{/systems}},这就是mustache的标签,只不过它用{{}}替代了<>,以免与HTML标签的<>混淆。logic-less,可翻译为轻逻辑,因为在定义模板时不会用到if-else,不会有循环式编码,一切皆用标签解决,它的标签非常简单,但能应付所有场景,阅读完本文后,你会惊讶地发现,只要用以下几个标签几乎就能解决所有问题: {{prop}} {{{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>"
//}-
</script>
mustache渲染{{prop}}标签的逻辑是:
- 1)若prop引用的值是null或undefined,则渲染成空串;
- 2)若prop引用的是一个函数,则在渲染时自动执行此函数,并将此函数的返回值作为渲染结果,假如此返回值为null或者undefined,那么渲染结果仍为空串,否则把返回值转成字符串作为渲染结果(注意最后一个用例,直接把函数代码渲染出来了);
- 3)其他场景,直接把prop引用的值转成字符串作为渲染结果。
由于默认情况下,mustache在渲染prop时,都是对prop的原始值进行URL编码或者HTML编码之后再输出的,所以有一个用例的渲染结果,把英文的单引号,转成了HTML实体符:
console.log(Mustache.render(tpl1, {
prop: function () {
return 'it\'s a fun'
}
}));//-it's a fun-
若要阻止这种编码行为,只要把标签形式改成{{{prop}}}就可以了:
<script id="tpl1" type="text/html">
-{{{prop}}}-
</script>
console.log(Mustache.render(tpl1, {
prop: function () {
return 'it\'s a fun'
}
}));//-it's a fun-
{{#prop}}标签的条件与迭代渲染
5. {{#prop}}{{/prop}}标签 这对标签的作用非常强大,可同时完成if-else和for-each以及动态渲染的模板功能。在这对标签之间,可定义其他模板内容,嵌套所有标签。接下来看看mustache如何利用这对标签完成这三个模板功能。
1) if-else渲染
只有prop属性在数据源对象上存在,并且不为falsy值(JavaScript 6个falsy值:null,undefined,NaN,0,false,空字符串),并且不为空数组的情况下,标签之间的内容才会被渲染,否则都不会被渲染:
<script id="tpl2" type="text/html">
-{{#prop}}content{{/prop}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
Mustache.parse(tpl2);
//测试falsy值
console.log(Mustache.render(tpl2, {prop: ''}));//--
console.log(Mustache.render(tpl2, {prop: 0}));//--
console.log(Mustache.render(tpl2, {prop: null}));//--
console.log(Mustache.render(tpl2, {prop: undefined}));//--
console.log(Mustache.render(tpl2, {prop: false}));//--
console.log(Mustache.render(tpl2, {prop: NaN}));//--
//测试空数组
console.log(Mustache.render(tpl2, {prop: []}));//--
//测试不存在的属性
console.log(Mustache.render(tpl2, {prop2:
true
}));//--
//测试function
console.log(Mustache.render(tpl2, {
prop: function () {
}
}));//--
console.log(Mustache.render(tpl2, {
prop: function () {
return false;
}
}));//--
console.log(Mustache.render(tpl2, {
prop: function() {
return [];
}
}));//--
//测试简单对象
console.log(Mustache.render(tpl2, {prop: {name: 'jason'}}));//-content-
//测试日期对象
console.log(Mustache.render(tpl2, {prop: new Date()}));//-content-
//测试boolean number string
console.log(Mustache.render(tpl2, {prop: true}));//-content-
console.log(Mustache.render(tpl2, {prop: 1.2}));//-content-
console.log(Mustache.render(tpl2, {prop: 'yes'}));//-content-
//测试返回非falsy,非空数组的function
console.log(Mustache.render(tpl2, {
prop: function () {
return 'it\'s a fun'
}
}));//-content-
</script>
以上用例中特殊点的就是prop属性引用的是一个函数的时候,{{#prop}}会自动调用此函数,并将函数的返回值作为if-else渲染逻辑的判断依据,也就是说如果此函数返回的是falsy值或者是空数组的时候,那么这对标签之间的内容还是不会显示。
2)for-each渲染
当prop属性所引用的是一个非空数组时,这对标签之间的内容将会根据数组大小进行迭代,并且当数组元素为对象时,还会把该对象作为每一次迭代的上下文,以便迭代时的标签可直接引用数组元素上的属性:
<script id="tpl2" type="text/html">
-{{#prop}}{{name}},{{/prop}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
Mustache.parse(tpl2);
console.log(Mustache.render(tpl2, {prop: [{name: 'jason'}, {name: 'frank'}]}));//-jason,frank,-
</script>
从测试结果可见,{{#prop}}{{/prop}}之间的模板内容根据prop所引用的数组迭代了两次,并且在这对标签内部直接通过{{name}}标签,输出了数组元素对象上的name属性对应的值。
若prop属性所引用的是一个函数,但此函数返回值是一个数组类型,那么仍会进行for-each渲染:
<script id="tpl2" type="text/html">
-{{#prop}}{{name}},{{/prop}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
Mustache.parse(tpl2);
console.log(Mustache.render(tpl2, {
prop: function(){
return [{name: 'jason'}, {name: 'frank'}];
}
}));//-jason,frank,-
</script>
3) 动态渲染
当prop属性所引用的是一个函数,并且此函数的返回值还是一个函数的话,mustache会再次调用此返回的函数,并给它传递2个参数:text表示原来的模板内容,render表示mustache内部的执行渲染的对象,以便在此函数内部可通过这render对象,结合原来的模板内容,自定义渲染的逻辑,并将函数的返回值作为渲染结果(此返回值渲染的逻辑跟{{prop}}标签完全一样):
<script id="tpl2" type="text/html">
-{{#prop}}content{{/prop}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
Mustache.parse(tpl2);
console.log(Mustache.render(tpl2, {
prop: function(){
return function (text, render) {
return "<b>" + render(text) + "</b>"
};
}
}));//-<b>content</b>-
</script>
{{^prop}}标签与渲染上下文栈
6. {{^prop}}{{/prop}}标签 这对标签,与{{#prop}}{{/prop}}的if-else渲染执行相反逻辑,即只有在prop属性不存在,或者引用的是一个falsy值,或者是一个空数组的时候才会显示标签之间的内容,否则不会显示:
<script id="tpl2" type="text/html">
-{{^prop}}content{{/prop}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
Mustache.parse(tpl2);
//测试falsy值
console.log(Mustache.render(tpl2, {prop: ''}));//-content-
console.log(Mustache.render(tpl2, {prop: 0}));//-content-
console.log(Mustache.render(tpl2, {prop: null}));//-content-
console.log(Mustache.render(tpl2, {prop: undefined}));//-content-
console.log(Mustache.render(tpl2, {prop: false}));//-content-
console.log(Mustache.render(tpl2, {prop: NaN}));//-content-
// 测试空数组
console.log(Mustache.render(tpl2, {prop: []}));//-content-
// 测试不存在的属性
console.log(Mustache.render(tpl2, {prop2: true}));//-content-
//测试function
console.log(Mustache.render(tpl2, {
prop: function () {
}
}));//-content-
console.log(Mustache.render(tpl2, {
prop: function () {
return false;
}
}));//-content-
console.log(Mustache.render(tpl2, {
prop: function () {
return [];
}
}));//-content-
//测试简单对象
console.log(Mustache.render(tpl2, {prop: {name: 'jason'}}));//--
//测试日期对象
console.log(Mustache.render(tpl2, {prop: new Date()}));//--
// 测试非空数组
console.log(Mustache.render(tpl2, {prop: [{name: 'jason'},{name: 'tom'}]}));//--
//测试boolean number string
console.log(Mustache.render(tpl2, {prop: true}));//--
console.log(Mustache.render(tpl2, {prop: 1.2}));//--
console.log(Mustache.render(tpl2, {prop: 'yes'}));//--
//测试返回非falsy,非空数组的function
console.log(Mustache.render(tpl2, {
prop: function () {
return 'it\'s a fun'
}
}));//--
//测试返回function的function
console.log(Mustache.render(tpl2, {
prop: function () {
return function(text,render){
return '<b>' + render(text) +'</b>'
}
}
}));//--
</script>
7. 渲染上下文 mustache有一个渲染上下文栈的概念,在模板渲染开始时,把数据源对象作为当前渲染上下文,并压入上下文栈。在遇到{{#prop}}标签时,若prop引用的是一个对象或者是一个非空的对象数组,或者prop引用的是一个函数,并且此函数返回的是一个对象或者是一个非空的对象数组,就会把这个对象或者数组的元素作为当前渲染上下文,并压入上下文栈,当此标签渲染完毕时,才会把该上下文弹出,恢复上一层标签所使用的上下文。由于{{#prop}}标签可多层嵌套,所以在有的模板渲染时,会有多层上下文存在。mustache在解析标签时,根据标签名称查找当前上下文对象是否存在该属性,若不存在就会到上层上下文对象中查找,只要在某一层找到,就会用该层上下文对象的值来渲染。
<script id="tpl2" type="text/html">
-{{#person}}{{#student}}{{#address}}address: {{home}},age: {{age}}{{/address}}{{/student}}{{/person}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
var obj2 = {
age: 20,
person: {
student: {
address: {
home: 'xxxxx'
}
}
}
};
console.log(Mustache.render(tpl2, obj2));//-address: xxxxx,age: 20-
</script>
上例中,在渲染{{#address}}{{/address}}时,上下文对象已变成obj2.person.student.address所引用的对象,所以{{home}}渲染时用到的就是obj2.person.student.address.home属性,而{{age}}渲染时,由于obj2.person.student.address不存在age属性,所以就会到上层上下文中查找,一直到obj2对象才找到,于是就把obj2.age当成了渲染结果。
还有一种方法,不用通过{{#prop}}创建新上下文,也可做到递归渲染属性的属性:
<script id="tpl2" type="text/html">
-address: {{person.student.address.home}},age: {{age}}-
</script>
<script>
var tpl2 = document.getElementById('tpl2').innerHTML.trim();
var obj2 = {
age: 20,
person: {
student: {
address: {
home: 'xxxxx'
}
}
}
};
console.log(Mustache.render(tpl2, obj2));//-address: xxxxx,age: 20-
</script>
此方法其实很好理解,只要知道当前上下文对象,再根据属性操作串person.student.address.home,当然就能找到需要的值了。
本文介绍了一个非常好用的前端模板引擎,涵盖的内容包含了在日常工作肯定会用到的知识点,希望大家喜欢。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
