自定义标签可以有内容,也可以嵌套使用。本文描述了如何实现一个由开始标签,结束标签和之间的标签内容的自定义标签。
渲染标签内容
我们先实现一个简单的 wrap 标签,它会把内容包装在 <div class="wrapper"></div> 元素里:
{% wrap %}
{{ "hello world!" | capitalize }}
{% endwrap %}
期望输出:
<div class='wrapper'>
Hello world!
</div>
首先 注册 一个名为 wrap 的标签,把内容解析到 this.tpls 数组里。parse(tagToken, remainTokens) 中,
tagToken是当前 Token{% wrap %},remainTokens是当前模板中后续所有 Token 的数组。
我们要做的是从 remainTokens 里拿出来/.shift() 足够的标签直到遇到 endwrap(其实可以是任意名字,但按照惯例应该叫 endwrap)。如果到模板结尾都没遇到 endwrap,需要抛出一个标签未关闭的 Error。
engine.registerTag('wrap', {
parse(tagToken, remainTokens) {
this.tpls = []
let closed = false
while(remainTokens.length) {
let token = remainTokens.shift()
// 得到了结束标签,停止解析
if (token.name === 'endwrap') {
closed = true
break
}
// 把 Token 解析成 Template
// parseToken() 可能会消耗多个 Token
// 例如 {% if %}...{% endif %}
let tpl = this.liquid.parser.parseToken(token, remainTokens)
this.tpls.push(tpl)
}
if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`)
},
* render(context, emitter) {
emitter.write("<div class='wrapper'>")
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
emitter.write("</div>")
}
})
.renderTemplates() 可能是异步的,因此需要 yield 来等它完成。更多关于 LiquidJS 异步的信息可以参考 同步和异步。render() 的其他部分比较直观,这是 JSFiddle 版本:http://jsfiddle.net/por0zcn1/3/。
使用 ParseStream
对于像 for 和 if 这样的复杂标签,parse() 会变得很复杂。使用 ParseStream 工具可以按事件风格来组织 parse() 的逻辑。下面是用 ParseStream 重写过的 parse(),实现了和上面例子中完全一样的功能。
parse(tagToken, remainTokens) {
this.tpls = []
this.liquid.parser.parseStream(remainTokens)
.on('template', tpl => this.tpls.push(tpl))
// 注意这里不能用箭头函数,因为我们需要 `this`
.on('tag:endwrap', function () { this.stop() })
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start()
}
这是 JSFiddle 链接:http://jsfiddle.net/por0zcn1/4/。简单起见,下面的例子都借助 ParseStream 来实现。
操作上下文
上面的 wrap 标签看起来没什么用,反正没它也可以很容易地渲染那部分内容。我们现在来实现一个 repeat 标签,把内容渲染两次(还可以加个参数让它渲染任意次):
{% repeat %}
{{ repeat.i }}. {{ "hello world!" | capitalize }}
{% endrepeat %}`
期望输出:
1. Hello world!
2. Hello world!
你可能注意到了在 repeat 上下文里有个额外的变量 repeat.i,这就需要我们操作 上下文。
上下文上下文 定义了 Liquid 模板中每个变量的值。在 LiquidJS 中,
Context由一个Scope的栈组成。Scope 就是一个普通对象,就像传给engine.render(tpl, scope)的scope一样。
每次进入新的 上下文 时,我们需要 push 一个新的 Scope。当结束渲染并退出 上下文 时,再把 Scope 从 上下文 pop 出来。见下面的实现:
engine.registerTag('repeat', {
parse(tagToken, remainTokens) {
this.tpls = []
this.liquid.parser.parseStream(remainTokens)
.on('template', tpl => this.tpls.push(tpl))
.on('tag:endrepeat', function () { this.stop() })
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start()
},
* render(context, emitter) {
const repeat = { i: 1 }
context.push({ repeat })
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
repeat.i++
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
context.pop()
}
})
parse() 部分和 wrap 标签完全相同,在 render() 部分我们通过调用两次 .renderTemplates(this.tpls) 来重复渲染内容。这是 JSFiddle 链接:http://jsfiddle.net/por0zcn1/2/。
成对使用 Push 和 Pop必须成对地使用
context.push()和context.pop()。如果忘记pop()会导致Scope泄露给后面的模板内容,也可能损坏 上下文 栈。.