为什么需要模板引擎
关于前端的模板引擎,我用一个公式来解释
模板引擎 模板 + 数据 ========> html页面
模板引擎就像是html的解析生成器,将对应的模板填充完数据之后生成静态的html页面。它可以在浏览器端(比如angular中指令所用的模板)也可以在服务器端执行,不过一般用于服务器端。因为它的一个作用是抽象公共页面来重用,如果在服务端填充数据,可以减少回填数据给页面的ajax请求,从而提升浏览器端整体页面渲染速度。
初级玩家:表达式
数据:
{
title: 'Express',
obj:{
version: 'v4.3',
category: 'node',
"date~": '2016'
}
}
模板:
<p>{{title}}</p>
<p>{{obj.version}}</p>
<p>{{obj/category}}</p>
<p>{{obj.date~}}</p>
handlebars中变量都添加双花括号来表示(类似Angular),对比ejs的”<%%>”来说看起来没什么区别,其实这是很人性化的,想一下你键盘上的位置,再考虑按这几个字符的难易程度你就懂了。其中要访问变量的属性值时可以用类似json格式的”.”,也可以用”/“。
其中变量名不可包含以下字符。如果包含则不被解析,如上的”“。
空格 ! " # % & ' ( ) * + , . / ; < = > @ [ / ] ^ ` { | } ~
但可以用 “ , ‘ , [] 来转译这些特殊字符。
这一条规则意味着 “&&”,”||”,”!”这类逻辑判断是不能出现在表达式中的! (看着这一条是不是觉得弱爆了,要不然怎么叫若逻辑模板引擎呢~哈哈,不过当然有另外的解决办法)。
中级玩家:helper
if else
{{#if author}} <h1>{{firstName}} {{lastName}}</h1> {{else}} <h1>Unknown Author</h1> {{/if}}
{ {#if isActive} } <img src="star.gif" alt="Active"> { {else if isInactive} } <img src="cry.gif" alt="Inactive"> { {/if} }
和一般的编程语言的 if-else 代码块是差不多的,不过再次重申由于上面提到的特殊字符,所以if条件中是不能有逻辑表达式的,只能是变量或者值。
unless
还是因为上面提到的那些字符,handlebars不支持逻辑非(“!”),所以又有了一个与if相反的helper
{ {#unless license} } <h3 class="warning">WARNING: This entry does not have a license!</h3> { {/unless} }
上面这段代码就等价于
{ {#if license} } { {else} } <h3 class="warning">WARNING: This entry does not have a license!</h3> { {/if} }
each
都知道each相当于for循环。不过有些地方需要注意:
- 可以用相对路径的方式来获取上一层的上下文。(上下文概念跟js中的上下文差不多,比如在each passage代码块内,每一次循环上下文一次是passage[0],passage[1]…)
- 一些默认变量,@first/@last 当该对象为数组中第一个/最后一个时返回真值。如果数组成员为值而非对象,@index表示当前索引值,可以用@key或者this获取当前值
- 可以用
as |xxx|
的形式给变量起别名,循环中通过别名可以引用父级变量值。当然也可以通过相对路径的方式引用父级变量。
{ {#each passage} }
{ {#each paragraphs} }
{ {@../index} }:{ {@index} }:{ {this} }</p>
{ {else} }
<p class="empty">No content</p>
{ {/each} }
{ {/each} }
{ {#each array as |value, key|} } { {#each child as |childValue, childKey|} } { {key} } - { {childKey} }. { {childValue} } { {/each} } { {/each} }
同时也可以用来遍历对象,这时@key表示属性名,this表示对应的值
{ {#each object} } { {@key} }: { {this} } { {/each} }
with
类似js中的with,可以配合分页使用,限定作用域。
{ {#with author as |myAuthor|} } <h2>By { {myAuthor.firstName} } { {myAuthor.lastName} }</h2> { {else} } <p class="empty">No content</p> { {/with} }
接下,俺们来实际操作一下:
handlebars_example.html页面 HTML:
<script id="entry-template-1" type="text/x-handlebars-template">
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
with:
{{#with withauthor as |myAuthor|}}
<h2>By {{myAuthor.firstName}} {{myAuthor.lastName}}</h2>
{{else}}
<p class="empty">No content</p>
{{/with}}
if-else:
{{#if author}}
<h1>{{author}}</h1>
{{else}}
<h1>Unknown Author</h1>
{{/if}}
if-else-if:
{{#if isActive}}
<h1>{{isActive}}</h1>
{{else if isInactive}}
<h1>{{isInactive}}</h1>
{{else}}
<h1>Unknown Author</h1>
{{/if}}
</script>
<script id="entry-template-2" type="text/x-handlebars-template">
{{#each passage}}
{{#each paragraphs}}
<p>{{@../index}}:{{@index}}:{{this}}</p>
{{#each this}}
{{@key}}--{{this}}
{{/each}}
{{else}}
<p class="empty">No content</p>
{{/each}}
{{/each}}
</script>
handlebars_example.html页面 JS
# 上述实例实现过程
var context = {"title": "My New Post", "body": "This is my first post!","author":"liming","isInactive":"true","withauthor":{"firstName":"ming","lastName":"li"}};
var source = $("#entry-template-1").html();
var template = Handlebars.compile(source);
var html = template(context);
var context = {"passage":[{"paragraphs":[{"ppkey1":"ppval1"},{"ppkey1":"ppval2"},{"ppkey1":"ppval3"}]},{"pkey":"pval1"},{"pkey":"pval2"
var source = $("#entry-template-2").html();
var template = Handlebars.compile(source);
var html = template(context);
console.log(html);
<div class="entry">
<h1>My New Post</h1>
<div class="body">
This is my first post!
</div>
</div>
with:
<h2>By ming li</h2>
if-else:
<h1>liming</h1>
if-else-if:
<h1>true</h1>
each-this-object:
<p>0:0:[object Object]</p>
ppkey1--ppval1
<p>0:1:[object Object]</p>
ppkey1--ppval2
<p>0:2:[object Object]</p>
ppkey1--ppval3
<p class="empty">No content</p>
<p class="empty">No content</p>
lookup
这个用于以下这种并列数组的情况,可以按照索引来找兄弟变量对应的值。理解起来有些困难,直接看代码
{ groups: [ {id: 1, title: "group1"}, {id: 2, title: "group2"}, ], users: [ {id:1, login: "user1", groupId: 1}, {id:2, login: "user2", groupId: 2}, {id:3, login: "user3", groupId: 1} ], infos: [ 'a','b','c' ] }
<table> { {#each users} } <tr data-id="{ {id} }"> <td>{ {login} }</td> <td data-id="{ {groupId} }">{ {lookup ../infos @index} }</td> </tr> { {/each} } </table>
结果:
user1 a user2 b user3 c
这里在users数组中按照索引值引用infos数组中对应的值,如果想引用groups中的groupId呢?很简单,用with。
<table> { {#each users} } <tr data-id="{ {id} }"> <td>{ {login} }</td> <td data-id="{ {groupId} }">{ {#with (lookup ../groups @index)} }{ {title} }{ {/with} }</td> </tr> { {/each} } </table>
自定义helper
内置的helper不够强大,所以通常需要写js代码自定义helper,先看一个简单的单行helper。
行级helper
传值
数值、字符串、布尔值这种常规数据可以直接传入,同时也可以传递JSON对象(但只能传一个),以key=value这种形式写在后面,最后就可以通过参数的hash属性来访问了。
注:以 key=value 的形式数据,必须写在最后,key=value 串中不能再有 单独的 “数值、字符串、布尔值”
模板
{{agree_button "My Text" true 111 class="my-class" visible=true conter=4 }}
代码
Handlebars.registerHelper('agree_button', function() {
console.log(arguments[0]);//==>"My Text"
console.log(arguments[1]);//==> true
console.log(arguments[2]);//==> 111
console.log(arguments[3].hash);//==>{class:"my-class",visible:true,conter:4}
}
传变量
传变量时可以用this指针来指代它访问属性,通过逻辑判断后可以返回一段html代码,不过不建议这样做。考虑以后的维护性,这种html代码和js代码混合起来的维护性是比较差的,如果要抽象成组件,还是使用分页比较好。
模板:
{{agree_button person}}
注册helper:
Handlebars.registerHelper('agree_button', function(p) {
console.log(p===this);//==> true
/*
person:
Object
blog:"blog_value"
name::"name_value"
*/
var blog = Handlebars.Utils.escapeExpression(this.person.blog),
name = Handlebars.Utils.escapeExpression(this.person.name);
return new Handlebars.SafeString("<button type='button'>my blog is:"+blog+",my name is:"+ name + "</button>");
});
数据:
var context = {"person":{"blog":"blog_value","name:":"name_value"}};
html页面:
<button type='button'>my blog is:blog_value,my name is:</button>
当内容只想做字符串解析的时候可以用 escapeExpression 和 SafetString 函数。
块级helper
块级helper获取参数的方式跟之前差不多,只是最后多了一个参数,这个参数有两个函数 fn
和 revers
可以和 else
搭配使用。后面将会讲解。
模板:
{ {#list nav} } <a href="{ {url} }">{ {title} }</a> { {/list} }
注册helper:
Handlebars.registerHelper('list', function(context, options) { var ret = "<ul>"; for(var i=0, j=context.length; i<j; i++) { ret = ret + "<li>" + options.fn(context[i]) + "</li>"; } return ret + "</ul>"; });
数据:
{ nav: [ { url: "https://url1", title: "blog" }, { url: "https://url2", title: "github" }, ] }
html页面:
<ul> <li> <a href="https://url1">blog</a> </li> <li> <a href="https://url2">github</a> </li> </ul>
自定义helper
each的index变量比较常用,但是它是从0开始的,往往不符合业务中的需求,这里写个helper来扩展一下。
方案一:
Handlebars.registerHelper("addOne",function(index,options){
return parseInt(index)+1 ;
});
方案二:
Handlebars.registerHelper('eval', function(str, options){
var reg = /\{\{.*?\}\}/g;
var result = false;
var variables = str.match(reg);
var context = this;
//如果是each
if(options.data){
context.first = context.first||options.data.first;
context.last = context.last||options.data.last;
context.index = context.index||options.data.index;
context.key = context.key||options.data.key;
}
$.each(variables, function(i,v){
var key = v.replace(/{{|}}/g,"");
var value = typeof context[key]==="string"?('"'+context[key]+'"'):context[key];
str = str.replace(v, value);
});
try{
result = eval(str);
return new Handlebars.SafeString(result);
}catch(e){
return new Handlebars.SafeString('');
console.log(str,'--Handlerbars Helper "eval" deal with wrong expression!');
}
});
模板:
{{#each list}} {{eval '{{index}}+1'}} {{/each}}
上面说到if不支持复杂的表达式,如果是“&&”操作还可以用子表达式来实现,更加复杂的就不好办了,这里我写了一个helper来实现。
注册helper:
/*
主要思想是使用eval执行想要的逻辑。以拼接字符的模式来进行逻辑判断理论上可以如同EL表达式一样处理页面上的大部分逻辑。
如:{{#expression a '==' b '&&' c '>' 0}} ...{{else}}.. {{/expression}}
*/
Handlebars.registerHelper('expression', function(str,options) {
# 过滤出以 {{expression string}} 表达的数组,最终variables显示 :["{{state}}", "{{number}}"]
var reg = /\{\{.*?\}\}/g;
var result = false;
var variables = str.match(reg);
#console.log(variables);
var context = this;
$.each(variables,function(i,v){
#console.log(v);
var key = v.replace(/{{|}}/g,"");
var value = typeof context[key]==="string"?('"'+context[key]+'"'):context[key];
str = str.replace(v, value);
});
#用this可以取到当前的上下文主体,此处就是我们的定义好的数据对象了。
#另外一个比较重要的就是options.fn方法,此方法可以将你传入的上下文主体编译到模板,返回编译后的结果,
#在helper中,我们把this传了进去,于是在模板中也可以引用到它。最终options.fn返回编译后的结果。
#也可以为options.fn传入其他的上下文对象,比如你要写一个迭代器,可以把数组的元素依次传入。
#另一个方法,options.inverse,它是取相反的意思,对应了我们模板中的{{else}}标签,
#它会编译{{else}}中的的内容并返回结果,如果我们的helper中需要带else逻辑,用它就可以了。
try{
result = eval(str);
if (result) {
return options.fn(this);
} else {
return options.inverse(this); # 输出:no sub
}
}catch(e){
console.log(str,'--Handlerbars Helper "ex" deal with wrong expression!');
return options.inverse(this);
}
});
html页面:
{{#expression "{{state}}==='sub' && {{number}}>10" }}
sub
{{else}}
no sub
{{/expression}}
context:
var context={"state":"sub","number":2}
先将整个逻辑表达式作为一个字符串传入,然后替换其中的变量值,最后用eval函数来解析表达式,同时增加异常处理。
来源:oschina
链接:https://my.oschina.net/u/2601064/blog/789774