使用TypeScript Compiler APIs

一个进行中或完成的React项目如果要进行国际化,那么第一步需要从源码中提取中文词条,这往往是一个体力活,并且有无法找到所有的中文词条的风险。我们可以开发工具来代替人工提取,简单点可以使用基于字符串和正则表达式查找就可以完成,这种方式有一个很大问题:中文词条的提取效率取决于你正则表达式有多强大,并且如果后续有词条替换的需求,实现起来相对复杂。下面要介绍另一种方式,从String -> AST -> String的方式,这里我使用了TS Compiler API

认识Compiler APIs

TS早在2.x版本就提供了一系列的API来更好的操作TypeScript AST,利用这些API,可以很方便的编写插件来影响TS编译过程,当然这些API也可以单独使用。关于AST这里并不做过多介绍,推荐一篇文章:深入Babel,这一篇就够了,以Babel举例,详细描述了Babel编译(转译)的过程,以及如何编写Babel插件,TS的工作流程和Babel某种程度上是相似的。

下面介绍几个常用的API

createSourceFile

1
function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes?: boolean, scriptKind: ScriptKind): SourceFile;

该方法接受源代码(文件名/字符串)并返回SourceFile,那SourceFile是否就是AST呢?再看SourceFile的定义:

1
2
3
4
5
6
7
8
interface SourceFile extends Declaration {
kind: SyntaxKind.SourceFile;
statements: NodeArray<Statement>;
endOfFileToken: Token<SyntaxKind.EndOfFileToken>;
fileName: string;
text: string;
...
}

通过查看SourceFile的定义,我们可以把SourceFile当做是TypeScriptAST,其中statements属性是源代码语句的数组,Statement也就是AST中的节点(Node)。

好了,如果我们有一份使用TSReact源代码,对于每个.tsx文件而言,使用下面的方式就可以得到AST了。

1
const ast = ts.createSourceFile('', codeString, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TSX)

Printer

使用createSourceFile可以将源代码转成SourceFile,那么Printer就是把SourceFile/Node转成字符串的APIs,其常用的几个API如下:

1
2
3
4
5
function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer;
interface Printer {
printFile(sourceFile: SourceFile): string;
printNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string;
}

  • createPrinter返回一个Printer实例,该实例可使用既定的printerOptionsNodeSourceFile进行打印(生成字符串形式)
  • printFile依据SourceFile打印字符串源码,不进行任何转换
  • printNode打印节点
1
2
3
4
5
6
7
8
9
const sourceFile: ts.SourceFile = 
ts.createSourceFile('test.ts', '', ts.ScriptTarget.ES2015, true, ts.ScriptKind.TS);
// printFile
const printer = ts.createPrinter()
console.log(printer.printFile(sourceFile)) // test.ts源码
// printNode
const node = ts.createAdd(ts.createLiteral(1), ts.createLiteral(2))
console.log(printer.printNode(ts.EmitHint.Expression, node, sourceFile)); // 1 + 2
// note: `printNode`第三个参数其实和打印节点无直接关系

transform

1
function transform<T extends Node>(source: T | T[], transformers: TransformerFactory<T>[], compilerOptions?: CompilerOptions): TransformationResult<T>;

生成AST后就要开始处理了,ts也提供了一系列的API用于遍历ASTforEachChildvisitEachChild都可以遍历AST,初次之外visitEachChild还可以修改节点,并返回修改后节点。

1
2
function visitEachChild<T extends Node>(node: T, visitor: Visitor, context: TransformationContext): T;
function forEachChild<T>(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined;

transform方法就和其字面意思一样,使用该方法可以转换AST。它接收多个transformer,最简单的transformer可以是下面这样:

1
2
3
4
5
6
7
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
function visit(node: T) {
console.log(node.kind)
return ts.visitEachChild(node, visit, context)
}
return ts.visitNode(rootNode, visit)
}

上面的transformer访问了每个节点,并且打印出当前访问节点的kind

下面再写一个比较实际的transformer,找到所有的中文字符串节点,并使用变量来替换该节点。

1
2
3
4
5
6
7
8
9
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
function visit(node: T) {
if (node.kind === ts.SyntaxKind.StringLiteral && node.text.match(/[^\x00-\xff]/g)) {
return ts.createIdentifier('placeholder')
}
return ts.visitEachChild(node, visit, context)
}
return ts.visitNode(rootNode, visit)
}

使用上面的transformervar name = '张三'将会被转换成var name = placeholder,文件中所有的StringLiteral节点中只要包含中文都会被转成指定的Identifier

TS还提供了create*update*多个API用于创建和更新节点,当对compiler API更加了解后,我们就可以做更多的事,例如var total = 1 + 2 变成var t = 3类似的代码压缩,自定义lint规则等等。

Compiler APIs的应用

基于Compiler APIs实现了一款React国际化工具 ext-intl,实现了React项目词条提取、词条key生成、代码原处替换等功能。该工具目前是可用的,一定程度上可以提升React项目国际化效率。

使用方式:

1
$ yarn add --dev ext-intl

完。

分享到 评论