创建自定义标签

创建自定义标签

Edge 的大部分功能都基于其公开的标签 API 实现,你也可以使用它来进一步扩展 Edge 的能力。

在实现自定义标签时,你需要处理抽象语法树(AST),因此具备相关知识会很有帮助。

不过,如果你是刚开始接触 AST,我们建议访问 astexplorer.net,选择 acorn 作为解析器,并试用该工具。你不需要在抽象语法树领域拿到博士学位。大多数时候,你会在实践中逐渐了解它的用法。

在本指南中,我们将涵盖以下主题。

  • 创建并注册一个自定义 Edge 标签。
  • 通过 Hello World 入门,了解如何解析并重新打印 AST。
  • 实现一个条件式的 Edge 标签。
  • 在条件块内部创建局部作用域的变量。

注册一个 Edge 标签

一个标签必须实现 TagContract 接口,你可以使用 edge.registerTag 方法注册它。

在下面的示例中,我们创建一个名为 reverse 的标签,它会向输出写入一句 "Hello greeting"。

import edge from 'edge.js'
import { TagContract } from 'edge.js/types'
/**
* 定义一个标签
*/
const reverse: TagContract = {
block: false,
seekable: true,
tagName: 'reverse',
compile(parser, buffer, token) {
buffer.outputRaw('Hello from reverse tag')
}
}
/**
* 将其注册到 Edge
*/
edge.registerTag(reverse)
/**
* 使用该标签
*/
const output = await edge.renderRaw('@reverse()')
console.log(output) // I am the reverse tag
  • blockblock 标志创建一个块级 Edge 标签,必须与开始和结束语句一起使用。例如:@if@each 标签就是块级的。
  • seekable:一个 seekable 标签接受一个或多个参数。如果你创建的标签永远不会接受参数,请将该标志设为 false
  • tagName:标签的唯一名称。
  • compile:compile 方法将标签转换为 JavaScript 输出。你将在这里花费大部分时间。

生成 AST

让我们让 reverse 标签接受一个字符串值,并生成它接受的参数的 AST。

const reverse: TagContract = {
block: false,
seekable: true,
tagName: 'reverse',
compile(parser, buffer, token) {
const expression = parser.utils.transformAst(
parser.utils.generateAST(token.properties.js_arg, token.loc, token.filename),
token.filename,
parser
)
console.log(JSON.stringify(expression, null, 2))
}
}

现在,让我们尝试向 @reverse 标签传入不同的值,并观察 expression 变量的控制台输出。


原始字符串

@reverse('hello world')
{
"type": "Literal",
"start": 0,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 13
}
},
"value": "hello world",
"raw": "'hello world'"
}

变量引用

@reverse(username)
{
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "state"
},
"computed": false,
"property": {
"type": "Identifier",
"start": 0,
"end": 8,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 8
}
},
"name": "username"
}
}

函数调用

@reverse(getUserName())
{
"type": "CallExpression",
"start": 0,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 13
}
},
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "state"
},
"computed": false,
"property": {
"type": "Identifier",
"start": 0,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 11
}
},
"name": "getUserName"
}
},
"arguments": [],
"optional": false
}

正如你所注意到的,AST 会根据 @reverse 标签的输入值而不同。你可以使用 expression.type 属性来禁止某些情况,也可以随意变更 AST。

将 AST 转换为字符串

一旦变更了 AST,你就可以使用 .stringify() 方法将其转换为有效的 JavaScript 表达式。

const reverse: TagContract = {
block: false,
seekable: true,
tagName: 'reverse',
compile(parser, buffer, token) {
const expression = parser.utils.transformAst(
parser.utils.generateAST(token.properties.js_arg, token.loc, token.filename),
token.filename,
parser
)
console.log(parser.utils.stringify(expression)))
}
}

完成 reverse 标签的实现

让我们收尾 reverse 标签的实现。我们将接受传给标签的参数,反转其输出值,并将其写入输出。

const reverse: TagContract = {
block: false,
seekable: true,
tagName: 'reverse',
compile(parser, buffer, token) {
const expression = parser.utils.transformAst(
parser.utils.generateAST(token.properties.js_arg, token.loc, token.filename),
token.filename,
parser
)
const outputExpression = `${parser.utils.stringify(expression)}.split("").reverse().join("")`
buffer.outputExpression(outputExpression, token.filename, token.loc.start.line, false)
}
}

让我们测试一下实现。

// 使用原始字符串
assert.equal(await edge.renderRaw(`@reverse('virk')`), 'kriv')
// 使用变量引用
assert.equal(await edge.renderRaw(`@reverse(username)`, {
username: 'virk'
}), 'kriv')
// 使用函数调用
assert.equal(await edge.renderRaw(`@reverse(getUserName())`, {
getUserName() { return 'virk' }
}), 'kriv')

创建条件标签

让我们提升一下难度,实现一个自定义的、用于显示通知的条件标签。

@notification 标签将接受通知类型,并在存在给定类型的通知时,渲染其子内容(在开始与结束块之间)。例如:

@notification('success')
<div class="alert alert-{{ notification.type }}">
<p> {{ notification.message }} </p>
</div>
@end

在底层,该标签会在模板状态中查找 notifications 对象。

import edge from 'edge.js'
import { TagContract } from 'edge.js/types'
const notification: TagContract = {
/**
* 标签在开始和结束标签之间接受内容
*/
block: true,
/**
* 标签接受参数
*/
seekable: true,
/**
* 标签名
*/
tagName: 'notification',
/**
* 将标签编译为 JavaScript 输出
*/
compile(parser, buffer, token) {
const expression = parser.utils.transformAst(
parser.utils.generateAST(token.properties.js_arg, token.loc, token.filename),
token.filename,
parser
)
const key = parser.utils.stringify(expression)
/**
* 写一个 if 语句
*/
buffer.writeStatement(
`if (state.notifications && state.notifications[${key}]) {`,
token.filename,
token.loc.start.line
)
/**
* 定义一个局部变量
*/
buffer.writeExpression(`let notification = {
type: ${key},
message: state.notifications[${key}],
}`, token.filename, token.loc.start.line)
/**
* 创建一个局部变量作用域,并告诉解析器
* "notification" 变量的存在
*/
parser.stack.defineScope()
parser.stack.defineVariable('notification')
/**
* 使用解析器处理组件子内容
*/
token.children.forEach((child) => {
parser.processToken(child, buffer)
})
/**
* 在关闭 if 语句之前,
* 清除局部变量的作用域
*/
parser.stack.clearScope()
/**
* 关闭 if 语句
*/
buffer.writeStatement(
`}`,
token.filename,
token.loc.start.line
)
}
}
edge.registerTag(notification)

让我们将 notification 标签投入使用。

const notifications = {
success: 'Settings saved successfully'
}
const output = await edge.renderRaw(`
@notification('success')
<div class="alert alert-{{ notification.type }}">
<p> {{ notification.message }} </p>
</div>
@end
`, { notifications })