林一二2022年04月24日 23:26
AST转换的铁三角
- Builder:直接将一种 AST 节点的大部分属性转换为另一种节点的属性(退出条件);里面会调用 BuildNodes 处理子节点
- BuildNodes:用于转换子节点数组(递归步骤),因为子节点可以是任意类型,所以需要调用 Switcher
- Switcher:传入任意类型的 AST 节点,判断一下其类型,调用相应的 Builder
这必然导致三者循环引用,需要处于同一文件里,但 Builder 很多,不可能全放同一文件里,所以我们参考 mdast-util-to-hast 的做法重新抽象:
将转换函数作为依赖注入
- Handler:对各类型节点的处理,写在各自的文件里。然后在
handler/index.js里集中引入集中导出,变成一个 handlers 字典;里面会调用 All 处理子节点 - All:和 BuildNodes 一样,对
children字段做 while 循环处理,循环里对节点调用 One - One:和 Switcher 一样,只不过从一个 handlers 字典里去取相应的 handler;这个字典通过参数传入(依赖注入),而不是直接从
handler文件夹 import,避免循环引用 - Factory:集中引入 Handler、All 和 One,将 Handler 作为参数传给 One 或者 All
可以看出结构基本一致,只不过通过 import 成字典的形式来分文件,然后通过依赖注入的方式将三角△循环引用变成了Y集中引用。
如果尝试先创建字典,然后再用一个 register 函数注册各 Builder,则会遇到逆变协变问题,各 Builder 所要求的节点类型是子类,而 register 函数的参数必然是 AST 基类,无法逆变传入参数。通过 import 来直接创建字典就可以避免逆变问题。
文件结构
简单的 AST 转换器模式基本类似,可以用 yoman 这样的代码生成器生成样板代码:
One和All放在traverse.ts里,这里不导入其它代码,只导入 IHandler 等等类型index.ts导入All和Handlers,将库调用者传入的 Root AST 节点的 children 传给All, handlers 也作为参数传给All- handlers 文件夹,内有各种 handler 函数,集中导出为 handlers 对象
handlers 里每一个函数都只需要考虑对自己负责的类型做转换,实现关注点分离。而 traverse 里的递归逻辑则保证我们新加的 handler 即使是在树的中间被调用,它产出的结果也能被正确地安放在结果的中间。