TypeScript自定义Transformer实战指南
一、TypeScript Transformer 简介
在深入探讨 TypeScript 自定义 Transformer 实战之前,我们先来了解一下什么是 Transformer。TypeScript 的 Transformer 是一种可以在 TypeScript 代码编译过程中对 AST(抽象语法树)进行操作的机制。AST 是源代码的一种抽象表示,它以树形结构展示代码的语法结构,每个节点代表一个语法元素。
TypeScript 编译器在处理代码时,会先将代码解析成 AST,然后通过一系列的阶段进行处理,Transformer 就可以在这个过程中对 AST 进行修改。这为我们提供了很大的灵活性,可以实现诸如代码转换、优化、添加自定义逻辑等功能。
例如,我们可以利用 Transformer 将特定的 TypeScript 语法结构转换为更通用的 JavaScript 语法,以确保在更多环境中能够运行,或者在编译时自动添加一些日志记录代码等。
二、准备工作
- 环境搭建 首先,确保你已经安装了 TypeScript。你可以通过 npm 全局安装 TypeScript:
npm install -g typescript
接下来,创建一个新的 TypeScript 项目目录,并初始化 package.json
:
mkdir typescript - transformer - demo
cd typescript - transformer - demo
npm init -y
- 项目结构 在项目目录下,创建以下结构:
typescript - transformer - demo
├── src
│ └── main.ts
├── dist
├── tsconfig.json
└── package.json
在 src/main.ts
中编写一些简单的 TypeScript 代码,例如:
function greet(name: string) {
return `Hello, ${name}!`;
}
console.log(greet('World'));
然后,配置 tsconfig.json
,确保编译输出到 dist
目录:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src"
}
}
三、编写第一个自定义 Transformer
- 了解 Transformer 函数签名
一个基本的 TypeScript Transformer 函数接受一个
TransformationContext
参数,并返回另一个函数。这个返回的函数接受一个SourceFile
参数,即输入的 TypeScript 文件的 AST 根节点,返回值是经过修改后的SourceFile
。
import * as ts from 'typescript';
function myTransformer(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
return (sourceFile: ts.SourceFile) => {
// 在这里对 sourceFile 进行修改
return sourceFile;
};
}
- 简单示例:替换字符串字面量
假设我们想要将所有字符串字面量
Hello
替换为Hi
。我们可以在 AST 中遍历节点,找到字符串字面量节点并进行替换。
import * as ts from 'typescript';
function replaceHelloTransformer(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
return (sourceFile: ts.SourceFile) => {
function visit(node: ts.Node): ts.Node {
if (ts.isStringLiteral(node) && node.text === 'Hello') {
return ts.factory.createStringLiteral('Hi');
}
return ts.visitEachChild(node, visit, context);
}
return ts.visitNode(sourceFile, visit);
};
}
- 应用 Transformer
为了应用我们的 Transformer,我们需要创建一个自定义的 TypeScript 编译器主机。以下是完整的示例代码,用于编译
src/main.ts
并应用我们的replaceHelloTransformer
:
import * as ts from 'typescript';
import { replaceHelloTransformer } from './replaceHelloTransformer';
function compileWithTransformer() {
const compilerOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS
};
const program = ts.createProgram(['src/main.ts'], compilerOptions);
const emitResult = program.emit(undefined, undefined, undefined, undefined, {
before: [replaceHelloTransformer(program.getTransformationContext())]
});
const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
allDiagnostics.forEach(diagnostic => {
console.log(ts.formatDiagnostic(diagnostic, {
getCurrentDirectory: () => process.cwd(),
getCanonicalFileName: fileName => fileName,
getNewLine: () => '\n'
}));
});
}
compileWithTransformer();
在上述代码中,我们通过 program.emit
的 transformers
选项应用了我们的 replaceHelloTransformer
。运行这段代码后,你会发现编译输出的 JavaScript 文件中,所有的 Hello
字符串字面量都被替换成了 Hi
。
四、Transformer 高级应用:添加日志记录
- 需求分析 假设我们想要在每个函数调用前添加一条日志记录,记录函数名和传入的参数。这可以帮助我们在运行时更好地跟踪函数的调用情况。
- 实现思路 我们需要在 AST 中找到函数调用表达式节点,然后在其前面插入日志记录代码。首先,我们要创建日志记录表达式,然后将其插入到函数调用表达式之前。
- 代码实现
import * as ts from 'typescript';
function addLoggingTransformer(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
return (sourceFile: ts.SourceFile) => {
function visit(node: ts.Node): ts.Node {
if (ts.isCallExpression(node)) {
const functionName = (node.expression as ts.Identifier).text;
const args = node.arguments.map(arg => ts.visitNode(arg, visit));
const logArgs = args.map(arg => ts.visitNode(arg, visit)).join(', ');
const logStatement = ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('console'),
ts.factory.createIdentifier('log')
),
undefined,
[
ts.factory.createStringLiteral(`Calling ${functionName} with args: [${logArgs}]`)
]
)
);
return ts.factory.createBlock([logStatement, node], true);
}
return ts.visitEachChild(node, visit, context);
}
return ts.visitNode(sourceFile, visit);
};
}
- 应用与测试
将上述
addLoggingTransformer
应用到编译过程中,与之前替换字符串字面量的应用方式类似。修改src/main.ts
中的代码,添加更多函数调用,例如:
function add(a: number, b: number) {
return a + b;
}
function multiply(a: number, b: number) {
return a * b;
}
console.log(add(2, 3));
console.log(multiply(4, 5));
运行编译并执行生成的 JavaScript 文件,你会看到在每个函数调用前都打印了相应的日志信息,例如:
Calling add with args: [2, 3]
5
Calling multiply with args: [4, 5]
20
五、处理复杂 AST 结构:类和方法
- 类和方法的 AST 结构
在 TypeScript 的 AST 中,类定义由
ts.ClassDeclaration
节点表示,类中的方法由ts.MethodDeclaration
节点表示。了解这些节点的结构对于我们在类和方法上应用 Transformer 逻辑至关重要。ts.ClassDeclaration
节点包含members
属性,它是一个数组,包含类的所有成员,包括方法、属性等。ts.MethodDeclaration
节点有name
(方法名)、parameters
(参数列表)和body
(方法体)等属性。 - 示例:在类方法中添加前置逻辑 假设我们有一个类,并且想要在每个类方法执行前添加一段通用的前置逻辑,例如打印一条日志表示方法开始执行。
import * as ts from 'typescript';
function addClassMethodLoggingTransformer(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
return (sourceFile: ts.SourceFile) => {
function visit(node: ts.Node): ts.Node {
if (ts.isClassDeclaration(node)) {
const newMembers: ts.ClassElement[] = [];
node.members.forEach(member => {
if (ts.isMethodDeclaration(member)) {
const methodName = member.name.text;
const logStatement = ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('console'),
ts.factory.createIdentifier('log')
),
undefined,
[
ts.factory.createStringLiteral(`Starting method ${methodName}`)
]
)
);
const newBody = ts.factory.createBlock([logStatement, member.body!], true);
const newMethod = ts.factory.updateMethodDeclaration(
member,
member.decorators,
member.modifiers,
member.name,
member.questionToken,
member.typeParameters,
member.parameters,
member.type,
newBody
);
newMembers.push(newMethod);
} else {
newMembers.push(member);
}
});
return ts.factory.updateClassDeclaration(
node,
node.decorators,
node.modifiers,
node.name,
node.typeParameters,
node.heritageClauses,
newMembers
);
}
return ts.visitEachChild(node, visit, context);
}
return ts.visitNode(sourceFile, visit);
};
}
- 应用与测试
在
src/main.ts
中定义一个类和一些方法:
class MathUtils {
add(a: number, b: number) {
return a + b;
}
subtract(a: number, b: number) {
return a - b;
}
}
const mathUtils = new MathUtils();
console.log(mathUtils.add(2, 3));
console.log(mathUtils.subtract(5, 2));
应用 addClassMethodLoggingTransformer
进行编译并执行生成的 JavaScript 文件,你会看到在每个类方法执行前都打印了相应的日志信息:
Starting method add
5
Starting method subtract
3
六、结合 Babel 使用 TypeScript Transformer
- 为什么结合 Babel 虽然 TypeScript 自身的编译器可以处理很多转换需求,但 Babel 拥有更庞大的插件生态系统,可以处理更广泛的 JavaScript 语法转换和 polyfill 需求。结合 TypeScript Transformer 和 Babel 可以让我们充分利用两者的优势。
- 配置流程 首先,安装必要的依赖:
npm install @babel/core @babel/preset - typescript @babel/preset - env @babel/plugin - transform - runtime typescript - babel - plugin
然后,创建一个 Babel 配置文件 .babelrc
:
{
"presets": [
"@babel/preset - typescript",
"@babel/preset - env"
],
"plugins": [
"@babel/plugin - transform - runtime",
"typescript - babel - plugin"
]
}
接下来,修改我们的 TypeScript 编译脚本,使其先通过 TypeScript Transformer 进行处理,然后再通过 Babel 进行处理。以下是一个示例脚本:
import * as ts from 'typescript';
import * as babel from '@babel/core';
import { addLoggingTransformer } from './addLoggingTransformer';
async function compileWithTransformerAndBabel() {
const compilerOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS
};
const program = ts.createProgram(['src/main.ts'], compilerOptions);
const transformedSourceFile = program.emit(undefined, undefined, undefined, undefined, {
before: [addLoggingTransformer(program.getTransformationContext())]
}).outputFiles[0].text;
const babelResult = await babel.transformAsync(transformedSourceFile, {
configFile: './.babelrc'
});
if (babelResult) {
console.log(babelResult.code);
}
}
compileWithTransformerAndBabel();
在上述代码中,我们先通过 TypeScript Transformer 对代码进行转换,然后将转换后的代码传递给 Babel 进行进一步的处理。这样,我们既可以利用 TypeScript Transformer 的强大功能对 AST 进行自定义操作,又可以借助 Babel 的生态系统进行更广泛的语法转换和 polyfill 支持。
七、注意事项和常见问题
- AST 节点类型判断
在编写 Transformer 时,准确判断 AST 节点类型非常重要。错误的类型判断可能导致程序崩溃或产生不符合预期的结果。例如,在处理函数调用表达式时,确保
node.expression
是ts.Identifier
类型,否则获取函数名时可能会出错。 - 作用域问题 在修改 AST 时,需要注意作用域的影响。例如,在添加新的变量声明或函数定义时,要确保它们在正确的作用域内。否则,可能会出现变量未定义或作用域冲突等问题。
- 性能考虑 复杂的 Transformer 逻辑可能会影响编译性能。尽量避免在 Transformer 中进行过于复杂的计算或大量的节点遍历。如果可能,可以缓存一些计算结果,以减少重复计算。
- 与 TypeScript 版本兼容性 TypeScript 的 AST 结构和 Transformer API 可能会随着版本的更新而发生变化。在使用 Transformer 时,要注意与所使用的 TypeScript 版本的兼容性,及时查阅官方文档以获取最新的 API 信息。
通过深入了解和实践 TypeScript 自定义 Transformer,我们可以在编译阶段对代码进行灵活的转换和优化,满足各种特定的需求。无论是简单的字符串替换,还是复杂的类和方法逻辑添加,Transformer 都为我们提供了强大的工具。同时,结合 Babel 等工具,可以进一步拓展我们的代码转换能力,确保代码在不同环境中的兼容性和高效性。在实际项目中,合理运用 Transformer 可以提升代码的质量和可维护性,为开发过程带来更多的便利和价值。