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,体积更小,但没有编译器。
社区也曾提供同版本的独立构建,例如:
- https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars-v4.7.6.js
- https://s3.amazonaws.com/builds.handlebarsjs.com/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
}
编译 HTML 模板
模板也可以放在页面里,用 type="text/x-handlebars-template" 的 <script> 包住。浏览器不会当 JS 执行这段内容,脚本再取出 innerHTML 去编译:
<script id="html-template" type="text/x-handlebars-template"></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
}
表达式
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
}
路径表达式
用句点分隔的路径深入嵌套对象:
handlebarsTest4: function () {
let template = Handlebars.compile(`{{person.firstname}} {{person.lastname}}`)
let templateStr = template({
person: {
firstname: "Li",
lastname: "zhao"
}
})
console.log(templateStr)
return templateStr
}
更改上下文
下面几种写法会改掉「当前正在看哪一层数据」:
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。
HTML 转义
{{expression}} 的输出会做 HTML 转义:&、<、>、"、' 等会变成对应实体(例如 & 变成 &),避免把数据里的标记当成 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 的大写形式。
使用时要注意:
- 返回值的转义
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) 是子表达式;href、cssStyle 是 Hash 参数。link 先转义各段,再 SafeString 输出 <a>。
块助手(block helper)是另一类:模板写成 {{#name}}...{{/name}},助手函数通过 options.fn(context)、options.inverse(context) 决定渲染哪一段。内置的 if、each、with 都是块助手。自定义块助手同样走 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
}
代码片段可以使用自定义上下文,也可以用 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。
其他用法:
- 缺省代码片段: 渲染未注册的片段会抛错。写成
{{#> 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 里使用。
内置助手代码
if
用 if 按条件渲染块。参数为 false、undefined、null、""、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
unless 与 if 相反:表达式为假时才渲染主块。
{{#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 指定级别,支持 debug、info、warn 和 error(默认 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.DEBUGHandlebars.logger.INFOHandlebars.logger.WARNHandlebars.logger.ERROR(默认)