给萌新的 TS custom transformer plugin 教程——TypeScript 自定义转换器插件

xuld/原创

Custom transformer (自定义转换器)是干什么的

简单说,TypeScript 可以将 TS 源码编译成 JS 代码,自定义转换器插件则可以让你定制生成的代码。比如删掉代码里的注释、改变变量的名字、将类转换为函数等等。

TypeScript 将 TS 代码编译到 JS 的功能,其实也是通过内置的转换器实现的,从 TS 2.3 开始,TS 将此功能开放,允许开发者编写自定义的转换器。

预备知识

语法树

语法树是用于表示语法的数据结构。具体请参考我的另一个篇文章:https://www.cnblogs.com/xuld/p/12238167.html 。

转换器原理

TS 源码会先被解析为语法树,然后通过弱干个转换器生成新的语法树,最后通过代码打印器将语法树转回源码。

转换器本质就是一个函数,这个函数接收一个语法树,并返回转换后的新语法树。

自定义转换器分 before 和 after,其中,before 是位于内置转换器之前(转换 TS 代码),after 是位于内置转换器之后(转换已处理的 JS 代码)。

如何使用转换器

官方的 tsc 命令不支持加载自定义插件,但还有很多方法使用自定义转换器:

  1. 直接调用 TS 编译器的 API 编译代码
  2. 使用社区提供的 TTypeScript 项目:https://github.com/cevek/ttypescript
  3. 使用 Webpack+TS-loader 编译项目,并且在 TS-loader 配置自定义转换插件:
{
    test: /\.ts$/,
    loader: ‘ts-loader‘,
    options: {
        getCustomTransformers(program) {
            return {
                before: [myTransformer],
                after: []
            }
        }
    }
}

其中,myTransformer 就是一个转换器。这里接收一个数组,可以传递多个转换器函数。

Hello world

按惯例先来一个简单的例子,教你如何写一个转换器。

目标:将下面源码中字符串的内容改成 “Hello world”

console.log("Hello xuld")

1. 新建一个 hello.js,内容如下:

const ts = require("typescript")

// 这是一个自定义转换器
function createTransformer() {
    return context => {
        return node => ts.visitNode(node, visit)

        function visit(node) {
            // 如果发现字符串,替换为自己的内容
            if (ts.isStringLiteral(node)) {
                return ts.createStringLiteral("Hello world")
            }
            // 其它节点保持不变
            return ts.visitEachChild(node, visit, context)
        }
    }
}

2. 测试自定义转换器

为学习方便,这里采用直接调用 TS API 的方案使用转换器

const ts = require("typescript")

// 要编译的源码
const source = `console.log("Hello xuld")`

// 编译源码
const result = ts.transpileModule(source, {
    transformers: { before: [createTransformer()] }
})

// 打印结果
console.log(result.outputText)

使用 node 执行以上代码可以看到最终的结果。

实现转换器

转换器的职责是接收一个语法树节点,然后返回生成的新节点,如果这个节点无需变化(多数情况),可以返回节点本身。

需要特别注意的是:转换器只会生成新的节点,而不会修改原有节点

这是因为一个节点会在多个地方被使用,而且很多地方针对节点作了缓存,为了确保系统稳定性,禁止修改节点可以避免很多意外的错误。

语法树是一种有层级的树结构,只要任何一个节点变化,这个节点的所有父节点都需要重新生成。为了避免每次重新创建大量节点浪费性能,TS 提供了 ts.visitNode,这个 API 接收一个节点和一个回调函数,然后将节点传递给回调函数,回调函数负责返回新节点,如果新节点和原节点相同,则重用旧节点,否则自动创建新的父节点。对用户而言,我们只需要使用 ts.visitNode 找出需要处理的节点并返回新节点,其它情况使用默认的 ts.visitEachChild 即可。

简而言之,无论你要做什么功能的转换器,不用在意原理,只要按这个模板填代码即可:

function createTransformer() {
    return context => {
        return node => ts.visitNode(node, visit)

        function visit(node) {
            // 其它代码不变,只需修改下面的部分
            // =======================================
            if (判断节点的类型(node)) {
                return 创建转换的节点(node)
            }
            if (判断节点的类型(node)) {
                return 创建转换的节点(node)
            }
            // =======================================

            return ts.visitEachChild(node, visit, context)
        }
    }
}

判断节点的类型

要判断节点的类型,可以通过 node.kind === SyntaxKind.xxx 比较,也可以通过 ts.isXXX(node):

如果你不清楚你要处理的这个语法对于的类型叫什么,可以使用 AstExplorer 。

创建转换的节点

创建转换后的新节点有两种方式:一种是最简单的,使用 ts.createXXX 创建;还有一种 ts.updateXXX 是基于已有的节点,如果节点发生变化则创建新节点,否则重用节点(主要为了节约内存损耗)。

比如要创建一个表示 a + 1 的节点:

ts.createBinary(ts.createIdentifier("a"), ts.SyntaxKind.PlusToken, ts.createNumericLiteral(1))

替换变量名

按以上的思路,替换变量名就需要:先找出变量名对应的节点,然后返回替换后的新变量名:

// 将代码中变量 foo 变成 goo
if (ts.isIdentifier(node) && node.text === "goo") {
    return ts.createIdentifier("goo")
}

但这里有个问题,就是变量名、函数名、类名也都是 Identifier 类型的节点,上面代码会全部换掉,有时,我们只希望处理某些条件下的节点,这时可以添加更多的判断,比如只替换作为函数名调用的 foo() 中的 foo,但不替换其它场景:

if (ts.isIdentifier(node) && node.text === "goo" &&    ts.isCallExpression(node.parent) && node.parent.expression === node) {
    return ts.createIdentifier("goo")
}

转换上下文

所有转换器都接收一个参数 context,表示转换的上下文。转换的上下文主要用于:

  1. 提供了一些实用的 API
  2. 在多个转换器之间共享数据
  3. 注册生成节点为字符串时的附加事件

自动生成变量

目标:支持 case 语句中使用 it 关键字:

源代码:

switch (1 + 1) {
    case it == 2:
}

转换后:

var _t_1;_t_1 = 1 + 1
switch (true) {
    case _t_1 == 2:
}

代码如下:

function createTransformer() {
    return context => {
        return node => ts.visitNode(node, visit)

        function visit(node) {
            if (ts.isSwitchStatement(node)) {
                // 创建临时变量
                const name = ts.createUniqueName("_t")
                // 插入变量
                context.hoistVariableDeclaration(name)
                // 生成两行代码
                return [
                    // 赋值变量
                    ts.createExpressionStatement(ts.createAssignment(name, node.expression)),
                    // 将 switch 的条件改为 true
                    ts.createSwitch(ts.createTrue(), ts.visitEachChild(node.caseBlock, child => visitSwitch(child, name), context))
                ]
            }
            // 其它节点保持不变
            return ts.visitEachChild(node, visit, context)
        }

        function visitSwitch(node, name) {
            // 将 it 变为新的变量名
            if (ts.isIdentifier(node) && node.text === "it") {
                return name
            }
            // 其它节点保持不变
            return ts.visitEachChild(node, child => visitSwitch(child, name), context)
        }
    }
}

思路:先创建一个临时变量,存放 switch 条件内容,然后将原始条件改成 true,并将内部 it 替换掉。

报错

在转换时,如果需要报错,可以使用 context.addDiagnostic(diag)

使用类型信息

在实际场景中,可能需要用到代码的类型信息(比如变量有没有定义,变量在哪些地方被使用,变量的类型)

转换器本身并没有直接提供这些信息,但可以通过 program.getTypeChecker() 获取到 TypeChecker,然后通过 TypeChecker 提供的丰富 API 获取到这些信息。

如果是采用了 ts-loader, program 对象通过 getCustomTransformer() 的参数得到。

[[[TODO: 更多的案例待阅读量超过1000后添加]]]

xuld/原创

更多案例

这里列了一些社区的现成插件,方便研究学习:

原文地址:https://www.cnblogs.com/xuld/p/12516828.html

时间: 2024-08-05 08:35:39

给萌新的 TS custom transformer plugin 教程——TypeScript 自定义转换器插件的相关文章

JIRA Plugin Development——Configurable Custom Field Plugin

关于JIRA Plugin开发的中文资料相当少,这可能还是由于JIRA Plugin开发在国内比较小众的原因吧,下面介绍下自己的一个JIRA Plugin开发的详细过程. 业务需求 创建JIRA ISSUE时能提供一个字段,字段内容是类似于订单号或手机号这种样式的数据,并且显示出来是一个链接,点击后可跳转到订单详情页或手机号所对应的客户的整个订单页,供用户查看和此任务工单关联的订单数据: 例如: 订单号为123456: 订单详情URL为:http://192.168.11.211?order=1

萌新的旅行-

Description zstu的萌新们准备去自助旅行,他们租了一辆吉普车,然后选择了n个城市作为游览地点.然后他们惊喜的发现他们选择的城市刚好绕城一个环. 也就是说如果给所有城市按照0,1,2,--,n-1编号,0号城市和n-1号城市是相邻的,并且只能从i号城市去(i+1)%n号城市. 已知每个城市可以充油gas(i),从 i 到 (i+1)%n 城市耗油 cost(i). 假设这辆吉普车没有的油箱一开始是空的,并且没有上限. 没有油的话自然就不能继续旅行了,这个问题让萌新们非常困扰.作为优秀

萌新笔记——C++里创建 Trie字典树(中文词典)(二)(插入、查找、导入、导出)

萌新做词典第二篇,做得不好,还请指正,谢谢大佬! 做好了插入与遍历功能之后,我发现最基本的查找功能没有实现,同时还希望能够把内存的数据存入文件保存下来,并可以从文件中导入词典.此外,数据的路径是存在配置文件中的.甚至,还想尝试类似自动补全的功能.当然了,是做一个比较low的补全,比如传入"编程",能够得到"软件"."学习"."学习网站"."入门"四个字符串.但是传入"编"不会得到&quo

Ingress 记萌新的第一次连多重(xjbl)

之前为了升七,ArtanisWei学长告诉我可以去紫金园雕塑[这是什么地方啊],顺带靠卖萌骗了一桶key 于是屁颠屁颠的跑去按照群里攻略开始连多重[馒头 by handsomepeach],连了一百年...这攻略怎么让人xjb跑啊QwQ 于是连出来这个 于是桃大就让绿军来炸 然后就没有然后了[躺],手机噼里啪啦一阵乱响爹地的江山就没了[哭唧唧] 晚上不服气又来了一次,矮油我滴妈晚上一个人真口怕,passerg就陪我摸了一遍就跑路了我勒个去 然后今天下定决心决定自己来连一次多重 于是萌新的第一次多

一个萌新的自白

浑浑噩噩的毕业已经过了半年,在这半年时光自己很没有方向感,都在尝试改变自己,但时光已不再,心却未能平静.一次偶然的机会想进IT行业来尝试自己是否能在这里让自己的心海不在一片涟漪.毕竟朦胧的青葱岁月早已逝去,是时候在这个青春真当时,让自己留下值得的回忆. IT行业是一个靠自己实力才能立足的行业,对于我来说是一个比较大的挑战,我是真正完全的电脑萌新,当时抱着很复杂的心态来学习,一边想着自己应该迈开这一步,或许会收到自己想要的那一份充实,另一方面有考虑自己是否能够适应,即使在学习第一节课时,我都在问自

萌新笔记——C++里创建 Trie字典树(中文词典)(插入、查找、遍历、导入、导出)(上)

写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示一串可能要搜索的东西. 首先放上最终的结果: input: 1 编程入门 2 编程软件 3 编程学习 4 编程学习网站 output: 1 char : 件 2 word : 编程软件 3 char : 习 4 word : 编程学习 5 char : 网 6 word : 编程学习网 7 char : 门 8 word : 编程入门 其实这里不应

萌新--关于vue.js入门及环境搭建

十几天闭关修炼,恶补了html跟css以及JavaScript相应的基础知识,恰巧有个群友准备做开源项目,愿意带着我做,但是要求我必须懂vue.js,所以开始恶补vue.js相关的东西. 在淘宝上买了相关视频,前两章简介听得懵懵懂懂的,能够勉强理解.到第三章,开始有案例之后,整个人就懵了,为啥老师的一个程序文件夹那么多东西,我就一个可怜兮兮的html??等等,老师说这node.js是什么? 不是另外一个框架吗?webpack又是啥?... 一连串懵逼之后,终于发现,视频不适合我.开始找其他的途径

RabbitMQ code=200, text="Goodbye"比较萌新的问题大佬绕道,乌拉~!

比较萌新的问题大佬绕道,乌拉~! 生产段代码: 消费端代码: 以上都是本萌新根据博客上面+官网的列子去搬运的,生产段的代码是能够执行并将消息保存到队列中的在消费端进行消费时抛出了一个错误,错误代码如下 Already closed: The AMQP operation was interrupted: AMQP close-reason, initiated by Application, code=200, text="Goodbye", classId=0, methodId=0

萌新web前端从零开始(1)——计算机入门

前言:这是一个萌新从零开始的学习之路,与大家分享自己的看法与见解,还请指出错误与遗漏点方便改正. 1.认识计算机. 计算机语言常见的有C,PHP,Ruby,Java,C#,Basic,JS,C++等,这里我用到的是C#语言,用到的软件是VisualStudio 2013版本.当用VS打开发现之前写的内容代码没有显示时可以点击右侧Program.cs进行显示. 默认情况下,编译文件位置在同名文件下bin文件下的Debug文件中的exe文件.如果要更迁移,复制粘贴新建项目的文件夹即可,在迁移后的文件