为什么需要上下文(Context)
上一节通过 nodeTransforms 数组实现了插件化转换,但转换函数只接收 node 参数,无法获取节点的位置信息和父子关系。要实现节点替换和节点删除,就需要更多的上下文数据。
上下文(Context)可以理解为局部的全局变量——在程序的某个范围内,通过一个对象方便地访问所有相关的属性和方法。这个概念在很多框架中都存在:
| 框架/库 | 上下文机制 |
|---|---|
| React | createContext + useContext |
| Vue | provide / inject 依赖注入 |
| Koa | ctx 对象 |
| Express | req / res 对象 |
扩展后的 Context 结构
interface TransformContext {
// 节点信息
currentNode: ASTNode // 当前正在转换的节点
childIndex: number // 当前节点在父节点 children 中的索引
parent: ASTNode | null // 父节点引用
// 转换插件
nodeTransforms: TransformFunction[]
// 操作方法
replaceNode(node: ASTNode): void // 替换当前节点
removeNode(): void // 删除当前节点
}
typescript
traverseNode 更新
上下文信息在遍历过程中动态更新:
function traverseNode(ast: ASTNode, context: TransformContext) {
// 通过 context 记录当前节点
context.currentNode = ast
const children = ast.children
// 执行所有转换插件
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
// 转换函数可能删除节点,需要检查 currentNode 是否还存在
transforms[i](context.currentNode, context)
}
// 如果节点被删除,不再遍历子节点
if (!context.currentNode) return
// 递归遍历子节点,更新上下文信息
if (children && children.length) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}
}
typescript
replaceNode:替换节点
// 在 context 中定义 replaceNode
const context = {
// ... 其他属性
replaceNode(node: ASTNode) {
// 找到父节点,用新节点替换当前节点
context.parent!.children[context.childIndex] = node
// 更新 currentNode 引用
context.currentNode = node
},
}
typescript
使用示例——将文本节点替换为元素节点:
function transformText(node: ASTNode, context: TransformContext) {
if (node.type === 'Text') {
// 将文本节点替换为 span 元素
context.replaceNode({
type: 'Element',
tag: 'span',
children: [],
})
}
}
typescript
removeNode:删除节点
const context = {
// ... 其他属性
removeNode() {
if (context.parent) {
// 从父节点的 children 数组中删除
context.parent.children.splice(context.childIndex, 1)
// 将 currentNode 置空
context.currentNode = null
}
},
}
typescript
删除节点后,currentNode 被置为 null,traverseNode 会在执行转换函数后检查这个值:
for (let i = 0; i < transforms.length; i++) {
transforms[i](context.currentNode, context)
// 转换函数可能调用了 removeNode
// 每次执行完都需要检查 currentNode 是否还存在
}
// 如果节点被删除,跳过子节点遍历
if (!context.currentNode) return
typescript
完整执行流程
transform(ast)
└── 创建 context(包含 currentNode, parent, childIndex, replaceNode, removeNode)
└── traverseNode(root, context)
├── 更新 context.currentNode = 当前节点
├── 执行 nodeTransforms 中的转换函数
│ ├── transformElement(node, context)
│ │ → 可能调用 context.replaceNode() 或 context.removeNode()
│ ├── transformText(node, context)
│ └── ...
├── 检查 context.currentNode 是否存在
│ ├── 存在 → 继续遍历子节点
│ └── 为 null → return(节点已被删除)
└── 遍历子节点时更新 context.parent 和 context.childIndex
text
设计要点
为什么用 context 而非全局变量
| 对比 | 全局变量 | Context 对象 |
|---|---|---|
| 作用域 | 全局,可能冲突 | 局部,可多实例并行 |
| 扩展性 | 需修改全局命名空间 | 通过属性方法扩展 |
| 安全性 | 任何代码都能访问 | 只在 traverseNode 内传递 |
| 测试性 | 难以隔离测试 | 可以创建独立 context 测试 |
每次循环后检查 currentNode
转换函数可能删除当前节点,因此每次调用转换函数后都必须检查 context.currentNode 是否为 null。这是一个容易被忽略但至关重要的细节。
本节要点
- Context 对象将节点信息和操作方法封装在一起,便于转换函数访问和修改 AST
- replaceNode 通过修改父节点的 children 数组实现节点替换
- removeNode 通过 splice 删除节点并将 currentNode 置空
- traverseNode 在每次转换后检查 currentNode 是否存在,防止操作已删除的节点
- Context 的设计思路在 React、Vue、Koa 等框架中普遍存在
↑