Handlebars 基础入门篇

拼 HTML 字符串时,条件、循环、嵌套对象一多,引号和拼接就会把结构拆散。Handlebars 把「要长什么样」写成模板,把「填什么数据」交给输入对象,渲染时用表达式把二者对上。

Handlebars 是一种简单的模板语言。它用模板和输入对象生成 HTML 或其他文本。模板看起来像普通文本,中间嵌着 Handlebars 表达式。表达式由一对花括号包住;执行模板时,这些位置会被输入对象里的值替换。

<p>{{firstname}} {{lastname}}</p>

Handlebars 是轻量的语义化模板:

  • 语义化模板: 提供把数据填进结构所需的能力,模板本身仍可读成「最终长什么样」。
  • 兼容 Mustache: 与 Mustache 模板基本兼容。多数情况下,已有 Mustache 模板可以直接放到 Handlebars 里用。
  • 编译后执行: Handlebars 会把模板编译成 JavaScript 函数,执行时走的是这份函数,而不是每次解析模板字符串。

本文示例与仓库 DEMO 对齐,浏览器侧使用 demo/handlebars-v4.7.6.js(完整版,可在页面里 compile)。同目录还有 handlebars.runtime-v4.7.6.js,只给已经预编译的模板用。

安装 Handlebars

安装方式取决于语言和环境。引擎本身用 JavaScript 写成。

npm 或 yarn(推荐)

npm install handlebars
# 或者
yarn add handlebars

然后通过 require 使用:

const Handlebars = require("handlebars")
const template = Handlebars.compile("Name: {{name}}")
console.log(template({ name: "张三" }))

现行也可以用 ESM:

import Handlebars from "handlebars"

内置在 npm 包中的浏览器构建

浏览器用的预构建文件在 node_modules/handlebars/dist/。走构建工具时,让打包器解析 handlebars 即可;若要自己拷文件,把 dist 里对应版本拷到静态目录。预编译模板时,编译端和运行端应使用同一主版本,避免 runtime 对不上。

下载 Handlebars

下面这类独立 JS 方便本地打开页面、不必先搭构建。它们不是生产环境的首选分发方式。

  • 编译模板(完整版): 页面里调用 Handlebars.compile 时用这一份。本仓库 DEMO 使用 demo/handlebars-v4.7.6.js
  • 预编译模板(runtime): 只运行已经 precompile 过的模板时用 demo/handlebars.runtime-v4.7.6.js,体积更小,但没有编译器。

社区也曾提供同版本的独立构建,例如:

现行也可以从 npm 包的 dist 目录,或 CDN(如 jsDelivr 上的 handlebars)取文件。生产环境优先锁定与预编译器相同的版本。

编译和预编译

Handlebars.compile(template, options)

编译一份模板,得到可以立即调用的函数:

const template = Handlebars.compile("{{foo}}")
template({})

options 会改变编译与执行行为,常用的包括:

  • noEscape:为 true 时,不再对输出做 HTML 转义。
  • strict:严格模式。访问不到的字段会抛错,而不是静默当成空;{{^foo}} 这类反向块也会受影响。
  • preventIndent:默认情况下,缩进后的 partial 调用会让整段 partial 输出跟着缩进,写 <pre> 时容易踩坑;设为 true 可关掉。

更完整的选项见 编译和预编译

Handlebars.precompile(template, options)

预编译模板,得到可以送到客户端的规格对象(或源码字符串)。客户端不再编译,只执行:

const templateSpec = Handlebars.precompile("{{foo}}")

参数与 Handlebars.compile 相同,另外还有:

  • srcName:为输入文件生成 source map。此时返回结构为 {code, map}code 是模板定义,map 是源映射。
  • destName:可选,与 srcName 一起用,给 source map 提供目标文件名。

Handlebars.template(templateSpec)

Handlebars.precompile 的结果装成可执行模板。浏览器若只引入 runtime,走的就是这条路径:

const template = Handlebars.template(templateSpec)
template({})

基础用法

DEMO 用 URL 参数 type 切换示例:handlebarsTest1 对应 ?type=1,依此类推到 type=10。页面脚本把返回的 HTML 写进 .box_01

编译字符串

把模板写成字符串,compile 之后传入数据对象:

handlebarsTest1: function () {
  let template = Handlebars.compile(`Hellow, <span>{{text}}</span>`)
  let templateStr = template({
    text: "Handlebars!",
  })
  console.log(templateStr)
  return templateStr
}

编译字符串 DEMO

编译 HTML 模板

模板也可以放在页面里,用 type="text/x-handlebars-template"<script> 包住。浏览器不会当 JS 执行这段内容,脚本再取出 innerHTML 去编译:

<script id="html-template" type="text/x-handlebars-template">
  <div>
      <h1>{{title}}</h1>
      <div>
          Hellow, I am <span>{{body}}</span>.
      </div>
  </div>
</script>
handlebarsTest2: function () {
  let htmlTemplate = document.querySelector('#html-template').innerHTML
  let template = Handlebars.compile(htmlTemplate)
  let templateStr = template({
    title: "Handlebars!",
    body: "html template body!"
  })
  console.log(templateStr)
  return templateStr
}

编译 HTML 模板 DEMO

表达式

Handlebars 表达式是模板的基本单位。表达式里可以使用路径、参数、Hash 参数、助手、块助手、内置助手以及 @data 变量。

表达式基本用法

一对花括号括住要插入的名字,编译时从当前上下文取同名属性:

handlebarsTest3: function () {
  let template = Handlebars.compile(`<p>{{firstname}} {{lastname}}</p>`)
  let templateStr = template({
    firstname: "Li",
    lastname: "zhao"
  })
  console.log(templateStr)
  return templateStr
}

表达式基本用法 DEMO

路径表达式

用句点分隔的路径深入嵌套对象:

handlebarsTest4: function () {
  let template = Handlebars.compile(`{{person.firstname}} {{person.lastname}}`)
  let templateStr = template({
    person: {
      firstname: "Li",
      lastname: "zhao"
    }
  })
  console.log(templateStr)
  return templateStr
}

路径表达式 DEMO

更改上下文

下面几种写法会改掉「当前正在看哪一层数据」:

  • with:把指定对象设为当前上下文,块里直接写它的属性名。
  • each:遍历数组,块内上下文变成当前元素。
  • ../:回到父级上下文。
handlebarsTest5: function () {
  let template = Handlebars.compile(`
        {{#with author}}
            <p>Author: <span>{{firstname}} {{lastname}}!</span></p>
        {{/with}}
        {{#each people}}
            <p>{{../prefix}} <span>{{firstname}}</span> </p>
        {{/each}}
      `)
  let templateStr = template({
    people: [
      { firstname: "li" },
      { firstname: "Li" },
    ],
    prefix: "Hello, ",
    author: { firstname: "li", lastname: "zhao" }
  })
  console.log(templateStr)
  return templateStr
}

{{#each people}} 里面写 {{firstname}} 取的是当前人的名字;{{../prefix}} 才能拿到循环外面的 prefix

更改上下文 DEMO

HTML 转义

{{expression}} 的输出会做 HTML 转义:&<>"' 等会变成对应实体(例如 & 变成 &amp;),避免把数据里的标记当成 HTML 插入页面。

不转义要用三对花括号:

不转义:{{{expression}}}
HTML 转义:{{expression}}

助手若要返回一段「已经拼好、应当按 HTML 插入」的字符串,需先对来自外部的片段做 Handlebars.escapeExpression,再包成 Handlebars.SafeString。只包 SafeString、不转义外部输入,会把 XSS 缺口留在模板里。

助手代码

助手用来补语言本身没有的能力:格式化、拼接标签、比较等。调用 Handlebars.registerHelper 之后,任意上下文里都能用这个名字,相当于给模板注册了一个函数。

handlebarsTest6: function () {
  let template = Handlebars.compile(`<span>{{firstname}} {{uCase lastname}}</span>`)
  Handlebars.registerHelper('uCase', s => s.toUpperCase())
  let templateStr = template({
    firstname: "Li",
    lastname: "zhao"
  })
  console.log(templateStr)
  return templateStr
}

{{uCase lastname}}lastname 传给 uCase。DEMO 里会得到 zhao 的大写形式。

助手代码 DEMO

使用时要注意:

  • 返回值的转义
    • Handlebars.SafeString(string):标记为安全 HTML,渲染时不再转义。
    • Handlebars.escapeExpression(string):把字符串转成可当文本插入 HTML 的形式。也可用 Handlebars.Utils.escapeExpression
  • 多个参数: 助手可接收一个或多个参数,参数之间用空格分开。
  • Hash 参数: 写成 name=value 的键值对会出现在最后一个参数 options.hash 里。
  • 子表达式: 用圆括号包一层助手调用,其返回值再传给外层助手。
handlebarsTest7: function () {
  let template = Handlebars.compile(`{{link (uCase people.name) "Welcome to visit!" href=people.url cssStyle="color: #0aa;"}}`)
  Handlebars.registerHelper('uCase', s => s.toUpperCase())
  Handlebars.registerHelper("link", (n, tx, options) => {
    var name = Handlebars.escapeExpression(n)
    var text = Handlebars.escapeExpression(tx)
    let href = Handlebars.escapeExpression(options.hash.href)
    let cssStyle = Handlebars.escapeExpression(options.hash.cssStyle)
    return new Handlebars.SafeString(`<a style="${cssStyle}" href="${href}">Hellow, ${name}! ${text}</a>`)
  })
  let templateStr = template({
    people: {
      name: 'lizh',
      url: "https://www.baidu.com/"
    }
  })
  console.log(templateStr)
  return templateStr
}

这里 (uCase people.name) 是子表达式;hrefcssStyle 是 Hash 参数。link 先转义各段,再 SafeString 输出 <a>

助手代码 DEMO2

块助手(block helper)是另一类:模板写成 {{#name}}...{{/name}},助手函数通过 options.fn(context)options.inverse(context) 决定渲染哪一段。内置的 ifeachwith 都是块助手。自定义块助手同样走 registerHelper,不要和下面的代码片段(partial)混为一谈。

代码片段(Partials)

Handlebars 用代码片段做模板复用。用 registerPartial 注册,用 {{> name}} 插入。动态名字可以写成 {{> (lookup . 'partName')}},用当前上下文里的字段去找片段名。

handlebarsTest8: function () {
  let template = Handlebars.compile(`
        {{> firstPartial}}
        {{> (lookup . 'partName')}}
      `)
  Handlebars.registerPartial('firstPartial', '<p>Hellow, <span>{{first}}</span></p>')
  Handlebars.registerPartial('secondPartial', '<p>Hellow, <span>{{second}}</span></p>')
  let templateStr = template({
    partName: 'secondPartial',
    first: '我是第一块代码片段。',
    second: '我是第二块代码片段。',
  })
  console.log(templateStr)
  return templateStr
}

代码片段 DEMO

代码片段可以使用自定义上下文,也可以用 Hash 把父级字段传进去:

handlebarsTest9: function () {
  let template = Handlebars.compile(`
        {{> firstPartial myOtherContext }}
        {{#each people}}
          {{> secondPartial prefix=../prefix firstname=firstname lastname=lastname}}
        {{/each}}
      `)
  Handlebars.registerPartial('firstPartial', '<p><span>{{information}}</span></p>')
  Handlebars.registerPartial('secondPartial', '<p><span>{{prefix}}, {{firstname}} {{lastname}}</span></p>')
  let templateStr = template({
    myOtherContext: { information: "Interesting!" },
    people: [
      { firstname: "li", lastname: "Knappmeier" },
      { firstname: "Li", lastname: "zhao" }
    ],
    prefix: "Hello"
  })
  console.log(templateStr)
  return templateStr
}

{{> firstPartial myOtherContext }}myOtherContext 当作该片段的上下文,所以片段里写 {{information}}each 里用 Hash 把 ../prefix 和当前项的姓名传给 secondPartial

代码片段传参 DEMO

其他用法:

  • 缺省代码片段: 渲染未注册的片段会抛错。写成 {{#> name}}...{{/name}} 时,若 name 未注册,就渲染中间这段,用来兜底。
  • 嵌套代码片段: @partial-block 指向调用方夹在 {{#> name}}{{/name}} 之间的那一块。
  • 局部代码片段: registerPartial 注册的片段对当前 Handlebars 环境里的模板都可见。{{#*inline "name"}}...{{/inline}} 只在当前块范围内定义一份片段。
handlebarsTest10: function () {
  let template = Handlebars.compile(`
        {{#> firstPartial }}
          <span>sorry, 此代码片段未注册!</span>
        {{/firstPartial}}
        {{#*inline "secondPartial"}}
          <span>{{firstname}}。 这是局部注册的 secondPartial 代码片段!</span>
        {{/inline}}
        {{#each people}}
          <p>Hellow, {{> secondPartial}}</p>
        {{/each}}
      `)
  Handlebars.registerPartial('firstPartial', 'firstPartial: {{> @partial-block }}')
  let templateStr = template({
    people: [
      { firstname: "li" },
      { firstname: "Li" },
    ]
  })
  console.log(templateStr)
  return templateStr
}

这段里 firstPartial 已经注册,因此不会走到「未注册」的兜底文案;它会输出自己的前缀,再插入 @partial-block(也就是调用处夹着的那行 <span>sorry...)。secondPartial#*inline 定义,只在这份模板的 each 里使用。

代码片段嵌套与局部注册 DEMO

内置助手代码

if

if 按条件渲染块。参数为 falseundefinednull""0[] 时,Handlebars 不渲染该块,可接 {{else}}

{{#if author}}
  <h1>{{firstName}} {{lastName}}</h1>
{{else}}
  <h1>Without author!</h1>
{{/if}}

子表达式

可以把自定义助手放到条件里:

{{#if (isEqual 1 2)}}
  true
{{else}}
  false
{{/if}}
Handlebars.registerHelper('isEqual', function (v1, v2) {
  return v1 === v2
})

unless

unlessif 相反:表达式为假时才渲染主块。

{{#unless author}}
  <h1>Without author!</h1>
{{else}}
  <h1>{{firstName}} {{lastName}}</h1>
{{/unless}}

each

each 遍历列表。块内用 this(或当前元素的属性名)引用正在迭代的项。

{{#each people}}
  <p>{{this}}</p>
{{else}}
  No content!
{{/each}}
  • {{else}}:列表为空时显示
  • {{@index}}:当前循环索引(从 0 起)
  • {{@key}}:当前键名(遍历对象时)
  • @first:是否第一项
  • @last:是否最后一项
  • {{@../index}}:访问父级循环的索引

with

with 改变块内上下文。{{else}} 仅在传入值为空时渲染。

{{#with person}}
  {{firstname}} {{lastname}}
{{else}}
  No city found
{{/with}}

with 还可配合块参数,给当前块起一个稳定名字:

{{#with city as |city|}}
  {{#with city.location as |loc|}}
    {{city.name}}: {{loc.north}} {{loc.east}}
  {{/with}}
{{/with}}

内层写 {{city.name}} 时,不会因为上下文已经切到 location 而丢了外层的 city

lookup

lookup 用 Handlebars 变量做动态取值。

{{#each people}}
  {{.}} lives in {{lookup ../cities @index}}
{{/each}}
{
  people: ["li", "Li"],
  cities: ["Darmstadt", "San Francisco"]
}

也可以在子表达式里用 lookup,把上下文切到另一个对象:

{{#each persons as |person|}}
  {{name}} lives in {{#with (lookup ../cities [resident-in])~}}
    {{name}} ({{country}})
  {{/with}}
{{/each}}
{
  persons: [
    { name: "li", "resident-in": "darmstadt" },
    { name: "Li", "resident-in": "san-francisco" }
  ],
  cities: {
    darmstadt: { name: "Darmstadt", country: "Germany" },
    "san-francisco": { name: "San Francisco", country: "USA" }
  }
}

[resident-in] 用来读取带连字符的属性名;lookup ../cities [resident-in] 先取出城市 key,再进入对应城市对象。

log

log 在执行模板时把上下文状态打到日志里:

{{log 'firstname' firstname 'lastname' lastname}}

level 指定级别,支持 debuginfowarnerror(默认 info):

{{log "debug logging" level="debug"}}
{{log "info logging" level="info"}}
{{log "info logging is the default"}}
{{log "logging a warning" level="warn"}}
{{log "logging an error" level="error"}}

@data 变量

下面这些 @data 变量由 Handlebars 及内建助手提供。

@root

模板开始执行时的根上下文。除非特意改掉,渲染过程中各处看到的 @root 都指向这份初始数据。深度嵌套或代码片段里,用 ../ 够不着根对象时,可以用它。

{{#each array}}
  {{@root.foo}}
{{/each}}

@first

each 第一次迭代时为 true

{{#each array}}
  {{#if @first}}
    First item!
  {{/if}}
{{/each}}

@index

从零开始的当前迭代次数,由 each 设置。

{{#each array}}
  {{@index}}
{{/each}}

@key

当前迭代的键。遍历对象时由 each 设置。

{{#each object}}
  {{@key}}
{{/each}}

@last

each 迭代到最后一项时为 true

{{#each array}}
  {{#if @last}}
    Last item!
  {{/if}}
{{/each}}

@level

设定 log 的输出级别。传入 data.level 后,日志按该级别过滤。

template({}, { data: { level: Handlebars.logger.WARN } })

取值可以是:

  • Handlebars.logger.DEBUG
  • Handlebars.logger.INFO
  • Handlebars.logger.WARN
  • Handlebars.logger.ERROR(默认)

参考资料

Handlebars 中文文档

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-09-03 01:55:15

results matching ""

    No results matching ""