给萌新的 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 命令不支持加载自定义插件,但还有很多方法使用自定义转换器:
- 直接调用 TS 编译器的 API 编译代码
- 使用社区提供的 TTypeScript 项目:https://github.com/cevek/ttypescript
- 使用 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,表示转换的上下文。转换的上下文主要用于:
- 提供了一些实用的 API
- 在多个转换器之间共享数据
- 注册生成节点为字符串时的附加事件
自动生成变量
目标:支持 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/原创
更多案例
这里列了一些社区的现成插件,方便研究学习:
ts-nameof
ts-optchain/transform
ts-transform-asset
ts-transform-auto-require
ts-transform-css-modules/dist/transform
ts-transform-graphql-tag/dist/transformer
ts-transform-img/dist/transform
ts-transform-react-intl/dist/transform
ts-transformer-enumerate/transformer
ts-transformer-keys/transformer
ts-transformer-minify-privates
typescript-is/lib/transform-inline/transformer
typescript-plugin-styled-components
typescript-transform-jsx
typescript-transform-macros
typescript-transform-paths
@zerollup/ts-transform-paths
@zoltu/typescript-transformer-append-js-extension
@magic-works/ttypescript-browser-like-import-transformer
如果你想成为大厂前端架构师,如果你还有成长的激情,
欢迎关注“我是前端架构师”微信公众号
给萌新的 TS custom transformer plugin 教程——TypeScript 自定义转换器插件的更多相关文章
- JIRA Plugin Development——Configurable Custom Field Plugin
关于JIRA Plugin开发的中文资料相当少,这可能还是由于JIRA Plugin开发在国内比较小众的原因吧,下面介绍下自己的一个JIRA Plugin开发的详细过程. 业务需求 创建JIRA IS ...
- 从Webpack源码探究打包流程,萌新也能看懂~
简介 上一篇讲述了如何理解tapable这个钩子机制,因为这个是webpack程序的灵魂.虽然钩子机制很灵活,而然却变成了我们读懂webpack道路上的阻碍.每当webpack运行起来的时候,我的心态 ...
- 手把手教你提交Jar包到Maven公共仓库 | 萌新写开源02
在上一篇文章中,我介绍了自己的SpringBoot Starter项目,可以让我们使用注解的方式轻松地获取操作日志,并推送到指定数据源. 之前,我的项目开源在Github上,大家想要用我的项目,还得把 ...
- 萌新笔记——C++里创建 Trie字典树(中文词典)(三)(联想)
萌新做词典第三篇,做得不好,还请指正,谢谢大佬! 今天把词典的联想做好了,也是比较low的,还改了之前的查询.遍历等代码. Orz 一样地先放上运行结果: test1 ID : char : 件 w ...
- 萌新笔记——C++里创建 Trie字典树(中文词典)(二)(插入、查找、导入、导出)
萌新做词典第二篇,做得不好,还请指正,谢谢大佬! 做好了插入与遍历功能之后,我发现最基本的查找功能没有实现,同时还希望能够把内存的数据存入文件保存下来,并可以从文件中导入词典.此外,数据的路径是存在配 ...
- 萌新笔记——C++里创建 Trie字典树(中文词典)(一)(插入、遍历)
萌新做词典第一篇,做得不好,还请指正,谢谢大佬! 写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示 ...
- Ingress 记萌新的第一次连多重(xjbl)
之前为了升七,ArtanisWei学长告诉我可以去紫金园雕塑[这是什么地方啊],顺带靠卖萌骗了一桶key 于是屁颠屁颠的跑去按照群里攻略开始连多重[馒头 by handsomepeach],连了一百年 ...
- java萌新尝试搭建WordPress记录
问题1:安装好PHP环境没找好mysql路径,导致不能调用数据库模块 解决方案:重装一次,参考链接 https://www.cnblogs.com/yangxia-test/p/4174372.htm ...
- 萌新关于C#委托一点见解
开博第一写C#委托(一个简单的委托) 1.关于委托,一直是学习c#的萌新们的噩梦,小生也是.最近在学委托感觉瞬间被虐成狗,但作为C#中极为重要的一个内容,学好了将会及大地减少我们的代码量,而且这也是够 ...
随机推荐
- CSS面试题&知识点汇总
问题&答案 介绍一下标准的CSS的盒子模型?低版本IE的盒子模型有什么不同的? 有两种, IE 盒子模型.W3C 盒子模型: 盒模型: 内容(content).填充(padding).边界(m ...
- Halcon算子函数
Chapter_1_:Classification 1.1 Gaussian-Mixture-Models 1.add_sample_class_gmm 功能:把一個訓練樣本添加到一個高斯混合模型的 ...
- 《内蒙古自治区第十二届大学生程序设计竞赛试题_D: 正品的概率》
问题 D: 正品的概率 内存限制:128 MB时间限制:1 S标准输入输出 题目类型:传统评测方式:文本比较上传者:外部导入 提交:36通过:7 返回比赛提交提交记录 题目描述 袋中有m枚正品硬币,n ...
- [LC] 490. The Maze
There is a ball in a maze with empty spaces and walls. The ball can go through empty spaces by rolli ...
- 在shell下执行命令的方法
在shell下执行命令的方法 1. #!/bin/sh 语法:在shell.sh的开头写入 #!/bin/sh 一般的shell脚本就是这种用法.这种方法调用脚本开头的shell执行命令,子shell ...
- 正负小数js正则表达式
var reg = /^(([1-9]\d+(.[0-9]{1,4})?|\d(.[0-9]{1,4})?)|([-]([1-9]\d+(.[0-9]{1,4})?|\d(.[0-9]{1,4})?) ...
- 吴裕雄--天生自然 PHP开发学习:PhpStorm的配置与安装
下载安装包
- APP倒闭:你充值的钱会蒸发吗?
有一句说到吐,但却又不得不说的话:资本大潮退去,才知道谁在裸泳.随着资本寒冬的来临,互联网上众多看起来狂飙突进的项目却呈现迅速萎靡态势.尤其是众多具有互联网元素的油卡.洗衣.保洁等成为重灾区,其中不少 ...
- 最近做的一个Spring Boot小项目,欢迎大家访问 http://39.97.115.152/
最近做的一个Spring Boot小项目,欢迎大家访问 http://39.97.115.152/,帮忙找找bug,网站里有源码地址 网站说明 甲壳虫社区(Beetle Community) 一个开源 ...
- Python 爬虫 selenium 笔记
1. selenium 安装, 与文档 pip install selenium Selenium with Python中文翻译文档 selenium官网英文文档 2. selenium 的第一个示 ...