typescript插件开发:点击页面跳转源代码

背景: 项目越来越大,定位组件代码位置越来越难。

随着ts项目越来越大,想修改页面里面某个组件的功能,需要先找到该组件有哪些文字, 然后去vscode里面去搜索,如果项目很大代码庞大,vscode搜索也会搜索很 久,所以就想使用一种方法能通过页面里面的dom元素直接链接到具体的组件源代码。提升开发体验。

下面先从ast开始讲起,最后落地了两个比较有用的插件。

ast语法树

image

可以点开下面的链接: ast

ast是方便计算机理解源代码,用于表达源代码结构的树状结构, 由一个个节点构成。

可以看到里面有JsxElement,JsxOpeningElement,JsxClosingElement这些节点。 它其实是一棵树形结构,在数据结构里叫做n叉树。

他们共同来描述了 该段代码的信息。 现在先来看下babel是怎么编译es6的?

babel编译原理

babel编译分为下面几步:

解析 -> 转换 -> 生成

其中

解析:

分为两步,词法分析和语法分析 词法分析把代码 转换为token流。类似于下面这样。

image 语法分析: 把词法分析得到的token流转换为一个ast结构, 用于后续的操作。

babel解析主要是babelon来完成的。

现在来看看具体ast长什么样? image

转换

ast类似于数据结构中的一棵树。

转换,是接收上一步解析生成的ast作为输入,可以像跟操作n×树一样对节点进行新增,删除,替换操作,这一步是最重要的一部分,各种babel插件就在这一步工作。而提供遍历api的。 是由babel-tranverse 这个模块,维护整棵树的状态, 负责遍历节点。 并且babel-tranverse 提供了一种方便访问 ast语法树的节点路径的方法。叫做 visitor模式。

visitor模式是一种用于遍历ast的跨语言的模式。 它是一个对象,定义了用于在一棵ast树结构里面获取具体节点的方法。

当我们修改ast树时,需要修改ast节点,这个是由另一个babel库 babel-types来提供的

它里面包含了构造、验证以及变换 AST 节点的方法.

刚刚提到 访问者模式是一种跨语言的模式,它在解析html的时候也会被用到,比如下面这段html可以使用 simplehtmlparser 来解析。 image image 将这段html标签,传入simplehtmlparser, 这个库的内部会使用栈来记录遍历到的开始结束标签, 当正则匹配到开始标签的时候,会执行传入的start函数。 匹配到字符的时候。会调用chars函数。

visitor模式

使用二叉树实现visitor模式

为了更加深入的理解visitor模式,用熟悉的二叉树来举个例子。 image

image image
第一幅图是 一个满二叉树,第二幅图定义了一个前序遍历二叉树的函数。 遵循中左右的遍历方式

接下来传入二叉树根节点和visitor对象。在遍历节点的过程中, 会在节点的进入和退出时候被执行。

最后是输出。 可以观察到最先进入的节点最后退出。

对于 ast树,它其实是一棵n×数。需要结合语言的特性采用同样的 前序遍历的方式来实现一个visitor模式。

babel里面的visitor模式

image 这是一个babel的插件的模板,可以看到里面的visitor对象, 当进入标识符(Identifier)的时候, 会调用标识符的enter函数,图中是enter函数的简写。

当离开该标识符的时候 ,会调用它的leave函数。 同理,当遇见其他import类型的节点的时候,会调用通过visitor 注册的对应的函数。

ts编译过程

image 我们来看看 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操作方法

image
前面提到ts提供的注入自定义transfrom的钩子函数。接下来看看提供了哪些api去操作ast节点。

ts ast节点遍历, 提供了ast前序遍历方法ts.visitNode和遍历子节点的ts.visitEachChild方法,

先来说下二叉树,二叉树中的节点操作有,对节点进行遍历, 判断节点类型,新增节点,删除节点。更新节点几类。 对应到ts的ast树里面也有这些api。比如上图里面的那些更新 删除 ,节点的遍历方法。

现在来看一个简单的demo。 image 这个demo里,通过ts提供的插件api ,可以获得前面提到的 sorceFileNode节点, 使用ts.visitNode 对它进行深度遍历,同时传入一个递归函数。 在这个递归函数里面可以判断当前的节点类型,并且对它进行删除,替换,新增节点。 再调用ts.visitEachChild(node, visitor, context); 对子节点进行递归调用。

当所有的节点都遍历操作完成之后,就可以交给emitter进行输出了。

image 这是深度优先的遍历,其实是没有实现类似于 babel-tranverse提供的操作ast 的visitor模式的, 对于babel或者其他开发者来说其实是有点不习惯的, 或者如果想把一些新开发的babel插件移植到ts里面,需要修改很多代码。

前面说过 babel 的visitor是一种跨语言的,提供访问具体树状ast具体节点的方法。

不禁陷入思考, 是否能利用ts现有的api实现ts的 visitor模式呢?

网上搜索了一下 确实没有人做这个事情。

现在来看一看怎么实现 .

ts visitor的实现

image 跟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阶段会把换行给移除掉,所以无法准确获取行数信息。

有兴趣的同学可以一起参与讨论。

仓库地址

猜你喜欢

转载自juejin.im/post/7031088820510539807