您的位置:首页 > Web前端

Handlebars玩起来

2016-11-17 00:00 316 查看
摘要: 模板引擎就像是html的解析生成器,将对应的模板填充完数据之后生成静态的html页面。

为什么需要模板引擎

关于前端的模板引擎,我用一个公式来解释

模板引擎  模板 + 数据 ========> 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>

当内容只想做字符串解析的时候可以用 escapeExpressionSafetString 函数。

块级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函数来解析表达式,同时增加异常处理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息