最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 一种编辑 TypeScript 代码的方式

    正文概述 掘金(kingwl)   2021-03-22   776

    前言

    众所周知,TypeScript 在 #13940 中开放了 transformer 的API,在此之后, 这些 API 已经成为在 TypeScript 生态中编写代码生成器(codegen),代码转换(transformer)的普遍方式。同时,目前这些 API 也有它的一些缺点。 本文将分享与分析作者在开发一个代码转换工具过程中遇到的问题,思考以及解决方案。

    背景

    传统方式:transformer API

    TypeScript 目前提供了将代码解析为 AST, 并且进行转换的 API。 让我们通过一个简单的例子了解它。

    目标:将 foo 标识符 转换为 bar 标识符

    首先,我们要将代码解析为 AST,以 let foo = 42 为例:

    import * as ts from 'typescript'
    
    const code = "let foo = 1;" // 要转换的代码
    const ast = ts.createSourceFile(
        /* filename */'dummy.ts',
        /* sourceText */ code,
        ts.ScriptTarget.Latest
    ) // 解析后的 AST
    

    可以看到,调用 TypeScript 编译器解析的过程非常简单。接下来,我们创建一个非常简单的 transformer, 这个transformer 会找到所有为 foo 的标识符,并将其修改为 bar

    const transformerFactory: ts.TransformerFactory<ts.Node> = (context: ts.TransformationContext) => {
        return visitor
    
        function visitor(node: ts.Node): ts.Node {
            if (ts.isIdentifier(node) && node.text === "foo") { // 判断是否是 foo 标识符
                return ts.createIdentifier("bar") // 如果是则替换为 bar 标识符
            }
            return ts.visitEachChild(node, visitor, context) // 否则继续遍历其他节点
        }
    };
    
    

    在开发 transformer 的过程中,可能涉及许多 TypeScript 的数据结构,我们可以使用 ts-ast-viewer 查看 AST 的结构并且获取生成对应代码的工厂函数。 然后我们尝试转换并生成代码:

    const result = ts.transform(ast, [transformerFactory]) // 转换 AST
    const node = result.transformed[0]
    
    // 由 AST 生成代码
    const printer = ts.createPrinter()
    const codeAfterTransform = printer.printNode(ts.EmitHint.Unspecified, node, ast);
    
    console.log(codeAfterTransform) // let bar = 1;
    

    可以看到,最后生成的代码已经如我们所预期的,将标识符 foo 改为了 bar。为了验证,我们再尝试一下:

    const code = "console.log(foo)"
    // ...
    console.log(codeAfterTransform) // console.log(bar);
    

    以上就是一个简单的 transformer。

    扩展

    显而易见,我们可以由 transformer 实现基于 AST 节点的宏,即判断某些节点,并替换为其他内容。但是由于 transformer 在解析后才被执行,所以不可以像其他语言的宏一样修改/扩展语法,这也是它的限制之一。

    问题

    transformer API 非常简洁好用,但是它同样也有一些缺憾:

    • 对 JSDoc 以及注释支持欠佳。在某些特殊情况下会丢失注释,并且在 TypeScript 4.0 之前的版本,JSDoc 相关的 API 并没有公开,并且处理 JSDoc/注释也是非常繁琐的工作。
    • 对代码风格有很大的破坏。可以认为transformer API 解析代码后,重新根据 AST 生成了新的代码。由于在这一过程中,丢失了很多信息,例如:空行,缩进等,因此通过 transformer API 转换后的代码相比原有的代码会有很多的改动。如果我们的目的是修改已有的 codebase,并且尽可能保留原有风格,那么这是不可接受的。
    • 对跨节点的操作支持不够友好。如果需要修改同一级的两个节点,往往需要在访问公共的父节点时进行操作,这会让 transformer 的编写非常痛苦。

    针对这些问题,作者经过一些尝试,找到了另外一种“新”方法。

    新的选择 —— TextChanges API

    什么是 TextChanges API

    TypeScript 在编译器之外,还提供了 language service 的功能,它为众多编辑器提供了例如“快速修复”,“重构” 等功能。这些功能会增删改代码,并且几乎不会修改原本的格式,而这些操作则是由被称为 textChanges 的一系列 API 所提供。所以,textChangs 并不 “新”,只是很少暴露于视野中。

    我们可以看到,TypeScript 在解析代码时,记录了每一个节点的位置:

    export interface TextRange {
        pos: number;
        end: number;
    }
    
    export interface Node extends TextRange {
        // ...
    }
    

    在获取到这些信息后,可以直接通过字符串编辑操作更新代码。而 textChanges 就是基于这些信息进行代码编辑的一系列 API。

    为什么选择 TextChanges API

    由于 textChanges 基于直接的字符串操作进行,不会涉及其他代码,因此不会有上文中代码格式变动方面的问题。由于同样的原因,它并不要求在访问公共父节点时操作,提升了编辑时的灵活性。

    并且我们在编辑代码时,并不只是简单的文本替换,还需要考虑到例如代码格式方面(例如逗号,括号等)的问题。自行处理的话需要花费非常大的精力,而 textChanges 作为 TypeScript 用来提供代码编辑的接口,本身处理了大量的 edge case。本着不重复造轮子的原则,我们选择其作为编辑代码的另一种选择。

    实践

    下面作者简单分享一些在使用 textChanges API 时的实践与经验。

    使用 TypeScript 内部 API

    不幸的是 textChanges 目前仅被 TypeScript 内部使用,并没有开放 API。但是由于我们最终运行的是 JavaScript,而这些 API 仅仅是没有提供类型定义,所以我们可以手动为这些内部 API 添加类型定义。例如使用作者的另一个工具 open-typescript 或自行手动添加类型定义。

    TextChanges 的依赖

    使用 textChanges 需要构造 LanguageServiceHost,其中有一些涉及 TypeScript Compiler Host 与 VFS 的细节在此不再赘述,可以查看 hosts.ts 与 upgrade.ts了解更多。

    实现

    下面我们重新使用 textChanges API 改写上面的例子:

    // ...
    declare const context: ts.textChanges.TextChangesContext; // 构造 context 的过程省略
    
    const changes = ts.textChanges.ChangeTracker.with(context, tracker => {
        function changesVisitor (node: ts.Node) {
            if (ts.isIdentifier(node) && node.text === "foo") {
                tracker.replaceNode(ast, node, ts.createIdentifier("bar")) // 替换节点
            }
            ts.forEachChild(node, changesVisitor)
        }
    
        ts.forEachChild(ast, changesVisitor)
    })
    
    const result = ts.textChanges.applyChanges(ast.text, changes.map(change => change.textChanges).flat()) // 获取所有的 changes
    console.log(result) // console.log(bar)
    

    可以看到,在遍历 AST 时,不再需要返回更改后的节点,而是由 changeTracker 记录。在遍历结束后统一应用。这样在处理同级的节点时,只需通过各种手段(例如 symbol 等)获取到相应的节点即可。

    结语

    transformertextChanges 都是进行代码编辑/转换的接口,在实际开发中,应根据自己不同场景和实际的需求,选择最适合自己的工具。

    感谢阅读。


    起源地下载网 » 一种编辑 TypeScript 代码的方式

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元