背景: 项目越来越大,定位组件代码位置越来越难。
随着ts项目越来越大,想修改页面里面某个组件的功能,需要先找到该组件有哪些文字, 然后去vscode里面去搜索,如果项目很大代码庞大,vscode搜索也会搜索很 久,所以就想使用一种方法能通过页面里面的dom元素直接链接到具体的组件源代码。提升开发体验。
下面先从ast开始讲起,最后落地了两个比较有用的插件。
ast语法树
可以点开下面的链接: ast
ast是方便计算机理解源代码,用于表达源代码结构的树状结构, 由一个个节点构成。
可以看到里面有JsxElement,JsxOpeningElement,JsxClosingElement
这些节点。 它其实是一棵树形结构,在数据结构里叫做n叉树。
他们共同来描述了 该段代码的信息。 现在先来看下babel是怎么编译es6的?
babel编译原理
babel编译分为下面几步:
解析 -> 转换 -> 生成
其中
解析:
分为两步,词法分析和语法分析 词法分析把代码 转换为token流。类似于下面这样。
语法分析: 把词法分析得到的token流转换为一个ast结构, 用于后续的操作。
babel解析主要是babelon来完成的。
现在来看看具体ast长什么样?
转换
ast类似于数据结构中的一棵树。
转换,是接收上一步解析生成的ast作为输入,可以像跟操作n×树一样对节点进行新增,删除,替换操作,这一步是最重要的一部分,各种babel插件就在这一步工作。而提供遍历api的。 是由babel-tranverse 这个模块,维护整棵树的状态, 负责遍历节点。 并且babel-tranverse 提供了一种方便访问 ast语法树的节点路径的方法。叫做 visitor模式。
visitor模式是一种用于遍历ast的跨语言的模式。 它是一个对象,定义了用于在一棵ast树结构里面获取具体节点的方法。
当我们修改ast树时,需要修改ast节点,这个是由另一个babel库 babel-types来提供的
它里面包含了构造、验证以及变换 AST 节点的方法.
刚刚提到 访问者模式是一种跨语言的模式,它在解析html的时候也会被用到,比如下面这段html可以使用 simplehtmlparser 来解析。 将这段html标签,传入simplehtmlparser
, 这个库的内部会使用栈来记录遍历到的开始结束标签, 当正则匹配到开始标签的时候,会执行传入的start函数。 匹配到字符的时候。会调用chars函数。
visitor模式
使用二叉树实现visitor模式
为了更加深入的理解visitor模式,用熟悉的二叉树来举个例子。
第一幅图是 一个满二叉树,第二幅图定义了一个前序遍历二叉树的函数。 遵循中左右的遍历方式
接下来传入二叉树根节点和visitor对象。在遍历节点的过程中, 会在节点的进入和退出时候被执行。
最后是输出。 可以观察到最先进入的节点最后退出。
对于 ast树,它其实是一棵n×数。需要结合语言的特性采用同样的 前序遍历的方式来实现一个visitor模式。
babel里面的visitor模式
这是一个babel的插件的模板,可以看到里面的visitor对象, 当进入标识符(Identifier)的时候, 会调用标识符的enter函数,图中是enter函数的简写。
当离开该标识符的时候 ,会调用它的leave函数。 同理,当遇见其他import类型的节点的时候,会调用通过visitor 注册的对应的函数。
ts编译过程
我们来看看 ts编译为js的流程:
-
先根据import 确定有哪些依赖,收集有哪些文件需要进行ts编译。
-
通过scanner 词法解析 生成token,再经过parser 语法解析生成一棵ast树,这个ast树在ts里面叫做 SourceFile树。
-
经过binder,即从 AST 生成 Symbol() ,它可以帮助类型系统推导出类型声明。
-
经过checker,即生成语义检查结果,进行类型检查 并且生成 诊断信息。也就是说平时的ts报错是在这一步抛出的。
-
最后再经过Emitter:把 .ts 和 .d.ts 文件转换成 .js、.d.ts 和 .map 等文件。输出到内存或者磁盘。
当然更加细节的东西可以自己去看 ts的文档。
现在来看看这上面的红色的部分。 用户自定义的ts plugin是在emitter的前后介入工作的。
可以看到上面有个 custom transformer
。 typescript 提供了一个3个钩子函数,可以用来注入用户自定义的 ts transform。
- before:它会在 ts将ts翻译成js之前被调用,因为它可以获得整个ast 树。
所以这个api比较的常用。
-
after,它会在ts把ts翻译为.js之后被执行
-
afterDeclarations: 它是在ts内部翻译 .d.ts文件之后被调用。
其中只有before可以修改 ast节点。 后面会使用其中的before 这个hooks来注入自定义的 ts transfrom。
ts的ast操作方法
前面提到ts提供的注入自定义transfrom的钩子函数。接下来看看提供了哪些api去操作ast节点。
ts ast节点遍历, 提供了ast前序遍历方法ts.visitNode和遍历子节点的ts.visitEachChild方法,
先来说下二叉树,二叉树中的节点操作有,对节点进行遍历, 判断节点类型,新增节点,删除节点。更新节点几类。 对应到ts的ast树里面也有这些api。比如上图里面的那些更新 删除 ,节点的遍历方法。
现在来看一个简单的demo。 这个demo里,通过ts提供的插件api ,可以获得前面提到的 sorceFileNode节点, 使用ts.visitNode 对它进行深度遍历,同时传入一个递归函数。 在这个递归函数里面可以判断当前的节点类型,并且对它进行删除,替换,新增节点。 再调用ts.visitEachChild(node, visitor, context); 对子节点进行递归调用。
当所有的节点都遍历操作完成之后,就可以交给emitter进行输出了。
这是深度优先的遍历,其实是没有实现类似于 babel-tranverse提供的操作ast 的visitor模式的, 对于babel或者其他开发者来说其实是有点不习惯的, 或者如果想把一些新开发的babel插件移植到ts里面,需要修改很多代码。
前面说过 babel 的visitor是一种跨语言的,提供访问具体树状ast具体节点的方法。
不禁陷入思考, 是否能利用ts现有的api实现ts的 visitor模式呢?
网上搜索了一下 确实没有人做这个事情。
现在来看一看怎么实现 .
ts visitor的实现
跟babel-tranverse一样,在前面的demo基础上,增加类型判断。当是某种类型的时候, 调用该种类型对应的enter,leave函数,注意这里,为了知道子节点什么时候返回。
将遍历子节点返回的节点先保存到newNode
, 等最后再返回。这样的话就有机会去调用 该节点的leave函数。
现在来完善左边这段代码。 因为ts语言的丰富,它的ast节点类型众多,所以判断具体是某种节点的is开头的方法众多。 可以点这里去查看最终生成的 ts-tranverse代码
前面实现了 ts的visitor模式,现在使用它来实现一个插件。也是我们的重头戏。
点击页面跳转源代码
可以先下载代码下面试一下。代码在最后。 可以按住mac里面的option键或者windows的alt键,然后点击页面里面的文字,就可以直接跳转到vscode。
现在来看这个插件是怎么实现的?
编译ts时 插件可以获得传入的 每个文件的Sourcefile ast,可以得到当前正在编译的文件名称。
遍历到jsx元素的时候,可以获得它所在的代码行数。
将这文件路径和代码所在行数组合成一个对象存入当前遍历到的jsx节点的属性里面。 最后打包生成的代码里面就会有 该属性。
再通过dom事件代理机制,可以实现点击某个dom,通过 vscode 提供的 url schema 跳转到对应的文件。 而绑定事件的代码可以通过注入import语句的方式来实现。
其他语言 比如 vue babel支持的tsx,也可以使用同样的思路来实现一个插件。
支持其他编辑器,和 不足。
插件的不足和展望
- 支持其他编辑器
- 因为ts前面 parse阶段会把换行给移除掉,所以无法准确获取行数信息。
有兴趣的同学可以一起参与讨论。