Handlebars学习记录

学习缘由

Handlebars模板引擎从去年就关注了,因为使用类HTML格式/类Vue/类Angular的语法我比较习惯,所以就定在了学习计划中。目前公司后台渲染项目都是使用Handlebars模板引擎,所以这篇博文简单做下学习记录。

模板引擎是做什么的

模板引擎所做的工作简单的说就是:

输入模板字符串 + 数据 => 得到渲染过的字符串

需要重申的是,得到的是经过数据填充的静态字符串。此外,根据页面的结构再拼接其余部分HTML片段,最后在发送到客户端。因此,在需要SEO场景条件下比较合适,比如:社交、新闻、博客、站点导航等网站是必须使用的。

此外,模板引擎除了Handlebarsjs还有:Jade templatingUnderscore TemplatesMustache等。除非现有的各类模板引擎不适合自己的业务,建议在选择开源项目的时候还是选择口碑好一些、目前还在维护的项目。

Basic Usage

一、JS模板编译

可以将模板字符串写在*.hbs中,也可以是在<script>中定义

二、插值

语法和Angular、Vue很像:{{value}}{{{Html}}}

  • {{value}}:在当前的上下文中找value这个属性并将值填充到这个位置。其实,这也可以理解为一个内置helper,意思是查找value这个名称的helper
  • {{{Html}}}:同上,但是内容原样输出不转义。

这里需要注意在传入的变量中不能有特殊字符:

! " # % & ' ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~

如果有的话需要用[]包起来(也可以是'或者"):

{{#each articles.[10].[#comments]}}
  <h1>{{subject}}</h1>
  <div>
    {{body}}
  </div>
{{/each}}

三、Helpers

Helper使用前需要注册,一般用于对数据的再加工,比如:

  • 改变数据格式
  • 给数据外边套一些Markup片段
  • Markup片段保存/传送(style/script)

1. 单行 Helper

单行的Helper可以是双大括号{{xx}},或者是{{{xx}}}包裹,区别在于:

  • {{xx}}:内部返回的模板字符串会转义处理,标签显示为字符串
  • {{{xx}}}:和上面的插值处理方式一样,返回的字符串不转义
{{{name data1 data2 hashKey1=hashValue1 hashKey2=hashValue2}}}
参数(格式固定)

参数含义有位置和书写格式确定,使用空格分离,分为三类:

  • name:helper的名称
  • data1/data2:传入helper的数据,可以是多个,写在data后面空格隔开就行
  • hash:options对应最后参数,以key=value形式书写,这个在helper定义的option.hash拿到,如果未定义则为{},不需要为空判断。
注册Helper
Handlebars.registerHelper('name', function(data1, data2, options) {
    // options.name: 当前helper的名称, 例如: debug
    // options.hash[hashKey1] === hashValue1
    // options.hash[hashKey2] === hashValue2
    // options.fn(this): 块级helper内部的模板函数
    // options.inverse: else分支的模板函数
    // options.data.root: 当前页面填充的数据对象
    // return 的字符串就是渲染的Markup片段
});
示例
{{{link "See more..." href=story.url class="story"}}}

Handlebars.registerHelper('link', function(text, options) {
  var attrs = [];

  for (var prop in options.hash) {
    attrs.push(
        Handlebars.escapeExpression(prop) + '="'
        + Handlebars.escapeExpression(options.hash[prop]) + '"');
  }

  return new Handlebars.SafeString(
    "<a " + attrs.join(" ") + ">" + Handlebars.escapeExpression(text) + "</a>"
  );
});

2. 块级 Helper

带有内部模板结构的写法,参数和注册方法同上,具体写法如下:

{{#name data1 data2 hashKey1=hashValue1 hashKey2=hashValue2}}
  内部模板
{{/name}}
示例
{{#bold}}{{body}}{{/bold}}

Handlebars.registerHelper('bold', function(options) {
  return new Handlebars.SafeString(
      '<div class="mybold">'
      + options.fn(this)
      + '</div>');
});

3. 输出原始 Blocks

书写原始的mustache语句块:

示例
// 模板
{{{{raw-helper}}}}
  {{bar}}
{{{{/raw-helper}}}}

// 对应的Helper函数
Handlebars.registerHelper('raw-helper', function(options) {
  return options.fn();
});

// 结果
{{bar}}

4. 已经注册好的常用Helper:

if/else/unless/with/each/log/blockHelperMissing/helperMissing

5. 嵌入子Helper(Subexpressions)

意思是,在父Helper中传入子Helper的模板String,语法是这样的:

{{outer-helper (inner-helper 'abc') 'def'}}
  • 内部使用名称为inner-helper的helper,传入参数:abc
  • 外部使用名称为outer-helper的helper,传入参数:生成的模板字符串def

6. 删除多余换行符(Inline)

正常书写block是有换行符的,Inline模式可以将换行符都删除。注意加~的位置!

示例
// 模板
{{#each nav ~}}
  <a href="{{url}}">
    {{~#if test}}
      {{~title}}
    {{~^~}}
      Empty
    {{~/if~}}
  </a>
{{~/each}}

// 数据
{
  nav: [
    {url: 'foo', test: true, title: 'bar'},
    {url: 'bar'}
  ]
}

// 结果
<a href="foo">bar</a><a href="bar">Empty</a>

7. 输出原始数据

是的,原封不动的输出,有两种方式:

  • 单行模式,以\开头
\这里填写你要输出的原始String
  • block模式,raw变量(true\false)控制是否显示内部内容
{{{{raw}}}}
这里填写你要输出的原始String
{{{{/raw}}}}

8. each Helper的as语法

遍历的同时提取value-key,便于内部使用,下面的例子一目了然。

{{#each array as |value key|}}
  {{#each child as |childValue childKey|}}
    {{key}} - {{childKey}}. {{childValue}}
  {{/each}}
{{/each}}

四、Partials

这个可以理解为子模板,用于模板复用,子模板继承当前的上下文变量环境。使用前需要注册,这个和Helper一致。

Handlebars.registerPartial('myPartial', '{{name}}')
  • 模板中的name可以理解为为this.name
  • 第二个参数必须是字符串(有些不友好)

Partials使用场景包括:Footer(友情链接)、Header(Logo登录)、Nav(导航)等。

1. 单行模板

{{> Partial名称 key1=value2}}
  • 注册的Partials将会填充在这个位置
  • 支持传入参数,参数可在内部直接使用(和Helper完全不同,不影响上下文this)
  • 注意!没有三个括号的写法(这个和Helper完全不同)
动态模板

通过chooseHelper这个Helper帮忙选择使用哪个Partial,chooseHelper应该返回已注册的Partial名称

括号中使用Helper的语法

{{> (chooseHelper) }}

2. 块级模板

块级需要加#标。如果使用Vue,这部分和slot的概念很像。这里分三类:

fallback方案
{{#> myPartial }}
  Failover content(如果myPartial未定义显示这里的东西)
{{/myPartial}}
内部通过@partial-block获取外部block内全部模板
// 模板内容
{{#> layout }}
   <p> layout中的默认全部slot</p>
{{/layout}}

// layout模板内容
{{> @partial-block }}

// 结果
<p> layout中的默认全部slot</p>
将子模板嵌入具体位置(具有名字的slot方案)

这个概念和Vue的具名Slot概念一致,直接贴代码,简单来说:各回各家,各找各妈。

// 模板
{{#> layout}}
  {{#*inline "nav"}}
    My Nav
  {{/inline}}
  {{#*inline "content"}}
    My Content
  {{/inline}}
{{/layout}}

// layout模板内容
<div class="nav">
  {{> nav}}
</div>
<div class="content">
  {{> content}}
</div>

// 结果
<div class="nav">
  My Nav
</div>
<div class="content">
  My Content
</div>

五、注释

{{!-- 这段注释只在源码中显示 --}}
{{! 这段注释只在源码中显示 }}
<!-- 这段注释会在最终的HTML中显示 -->

六、APIs

Handlerbar提供的API分三类:

  • Base:包括模板编译、注册/注销各类Helper/Partial、模板转移等。
  • 工具类:判断变量类型
  • 数据变量:@root、@first、@last、@key等

需要说明的API:

Handlebars.escapeExpression(string)

字符转义函数,将string中的 &, <, >, ", ', `, = 变为转义字符. 保证传入的字符串是安全的。例如:

hbs.Utils.escapeExpression('<script>alert(1)</script>')
// 转化为
<script>alert(1)</script>
// 在html中显示字符串
<script>alert(1)</script>

如果上面具有攻击性的字符串未转义,将在浏览器中弹出alert对话框。

new Handlebars.SafeString(string)

标记内容是安全的,单独使用并不起什么作用:

new hbs.SafeString('<script>alert(1)</script>')
// 返回
SafeString { string: '<script>alert(1)</script>' }

需要和上面的escapeExpression组合使用:

Handlebars.registerHelper('link', function(text, url) {
  text = Handlebars.Utils.escapeExpression(text);
  url  = Handlebars.Utils.escapeExpression(url);

  var result = '<a href="' + url + '">' + text + '</a>';

  return new Handlebars.SafeString(result);
});

模板渲染过程

在Express中默认使用的``hbs```模块渲染Handlerbars模板,渲染过程是这样子的:

  • 查找路由对应的模板,进行编译,并将编译的结果保存为 A
  • 查找layout.hbs,如果
    - 存在:对layout.hbs进行编译,其中{{{body}}}标签替换成 A,并返回最终编译结果B
    - 不存在:返回A

知识图谱

Handlerbars结构.png

使用建议

  • 前端使用了自定义的Helper/Partial需要和后端同步,不要忘记。
  • 如果是静态内容为主,那就直接服务端渲染好了,首屏加载速度快,不建议使用复杂的Helper/Partial,View层专注展示即可,数据格式在填充模板前处理好。非主要部分使用异步加载,比如简书的投稿管理/评论/推荐阅读使用的是Vue做异步内容渲染的。
  • 如果是数据交互、动态的应用界面(比如中后台系统、查询系统),那就不应该用拼模板的思路去做,而是用做应用的架构(MV*、组件树、组件/模块、数据驱动视图)思路去做。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容