第七章 - 使用编译器API输出生成的TypeScript和JavaScript代码
通过编写AST实现自动生成TypeScript代码的入门指南
这是本书最令人兴奋的章节之一!我们将学习如何自动生成TypeScript代码!
在构建时生成代码有很多应用场景 - 比如生成HTTP客户端、SDK、文档、HTML页面等等。掌握代码生成技术将为你的开发工具箱增添新利器。
本章示例中,我们将基于配置文件生成React代码。不了解React也没关系,它的语法类似HTML但更现代化。关于React的入门知识,可以参考Mozilla的React入门指南。
目标
我们的目标是根据JSON配置文件生成React TSX代码。假设配置文件如下:
{
"name": "IntroPage",
"title": "我的页面标题",
"subtitle": "我的页面副标题",
"path": "/pages/intro",
"parent": "cloud",
"content": "你好,世界!",
"tags": [
{ "id": "tag1", "name": "标签1" },
{ "id": "tag2", "name": "标签2" }
]
}
我们要将其转换为如下代码:
import React from "react"
import { PageTitle, PageSubtitle, PageBody, PageTags, PageTag } from "my-ui-lib"
export default function IntroPage() {
const context = useContext()
return (
<>
<PageTitle>我的页面标题</PageTitle>
<PageSubtitle>我的页面副标题</PageSubtitle>
<PageBody>你好,世界!</PageBody>
<PageTags>
<PageTag>标签1</PageTag>
<PageTag>标签2</PageTag>
</PageTags>
</>
)
}
你可能会问:既然React本身就支持模板功能,为什么还要生成代码?这是个好问题。React确实可以直接这样写:
export default function Page() {
const context = useContext()
return (
<>
<PageTitle>{pageTitle}</PageTitle>
<PageSubtitle>{subtitle}</PageSubtitle>
<PageBody>{content}</PageBody>
<PageTags>
{tags.map((tag) => (
<PageTag key={tag.id}>{tag.name}</PageTag>
))}
</PageTags>
</>
)
}
为什么不直接使用模板?
两种方式各有优劣。代码生成可以减少运行时计算,因为大部分内容是静态的(比如不需要循环处理标签)。但会增加客户端包体积,因为每个页面都有重复代码而不是共用布局模板。
虽然这个例子中模板可能更合适,但我们仍会演示代码生成,因为:
- 有些场景需要在构建时注入生成代码
- 掌握这项技术对未来处理复杂情况很有帮助
通过代码生成,我们可以:
- 结合Webpack等工具自动添加代码
- 减少需要手写的代码量
- 支持微前端等架构模式
这项技术不仅适用于前端React,任何代码场景都能受益。
工厂方法
我们将使用TypeScript编译器API的factory命名空间来创建AST节点。例如创建const a = 17:
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier("a"),
undefined,
undefined,
ts.factory.createNumericLiteral("17")
)
],
ts.NodeFlags.Const
)
)
这段代码会生成对应的AST,之后可以转换为源代码。建议浏览ts.factory了解可用的方法。
开始实践
我们继续使用index.mjs作为开发环境,并配合TS AST Viewer工具。
首先在文件顶部添加打印生成代码的辅助函数:
/**
* @param {ts.Node[]} nodes
*/
function printCode(nodes) {
const resultFile = ts.createSourceFile("source.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX)
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
const sourceFile = ts.factory.createSourceFile(
nodes,
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None
)
const result = printer.printNode(ts.EmitHint.Unspecified, sourceFile, resultFile)
console.log(result)
}
注意创建源文件时指定了TSX脚本类型。为方便起见,我们直接将配置存入变量:
const config = {
pages: [
{
name: "IntroPage",
title: "我的页面标题",
subtitle: "我的页面副标题",
path: "/pages/intro",
parent: "cloud",
content: "你好,世界!",
tags: [
{ id: "tag1", name: "标签1" },
{ id: "tag2", name: "标签2" }
]
}
]
}
构建页面
将生成逻辑分为两部分:
- 创建import语句
- 创建export语句
对应两个函数:
function createImportStatements() {}
function createExportStatement(page) {}
连接这两个部分:
const page = [...createImportStatements(), createExportStatement(config.pages[0])]
printCode(page)
注意:使用扩展运算符展开import语句的返回数组
生成import语句
使用TS AST Viewer工具分析以下代码:
import React from "react"
import { PageTitle, PageSubtitle, PageBody, PageTags, PageTag } from "my-ui-lib"
工具会生成对应的factory代码。我们可以优化为:
function createImportStatements() {
const componentImports = ["PageTitle", "PageSubtitle", "PageBody", "PageTags", "PageTag"]
return [
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(false, ts.factory.createIdentifier("React"), undefined),
ts.factory.createStringLiteral("react"),
undefined
),
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(
componentImports.map((id) =>
ts.factory.createImportSpecifier(false, undefined, factory.createIdentifier(id)))
)
),
ts.factory.createStringLiteral("my-ui-lib"),
undefined
)
]
}
这样更易于维护,运行后会输出正确的import语句。
生成export语句
将示例函数粘贴到TS AST Viewer,会得到大量factory代码。我们需要做以下修改:
- 使用配置中的页面标题
- 使用配置中的副标题和内容
- 设置导出函数名为配置中的名称
- 移除不必要的JSX文本节点
- 动态生成标签组件
关键修改点:
// 动态生成标签
const tagElements = page.tags.map((tag) =>
ts.factory.createJsxElement(
ts.factory.createJsxOpeningElement(
ts.factory.createIdentifier("PageTag"),
undefined,
ts.factory.createJsxAttributes([])
),
[ts.factory.createJsxText(tag.name, false)],
ts.factory.createJsxClosingElement(ts.factory.createIdentifier("PageTag"))
)
)
// 在返回语句中使用
ts.factory.createJsxElement(
ts.factory.createJsxOpeningElement(
ts.factory.createIdentifier("PageTags"),
undefined,
ts.factory.createJsxAttributes([])
),
tagElements,
ts.factory.createJsxClosingElement(ts.factory.createIdentifier("PageTags"))
)
完整脚本运行后会输出预期的React组件代码。
代码优化
发现重复模式可以提取公共函数:
function createComponent(identifier, text) {
return ts.factory.createJsxElement(
ts.factory.createJsxOpeningElement(
ts.factory.createIdentifier(identifier),
undefined,
ts.factory.createJsxAttributes([])
),
[ts.factory.createJsxText(text, false)],
ts.factory.createJsxClosingElement(ts.factory.createIdentifier(identifier))
)
}
然后可以这样使用:
createComponent("PageTitle", page.title)
这类似于JSX的底层实现原理,非常有趣!
总结
本章我们:
- 学习了使用TypeScript编译器API生成代码
- 完成了React TSX代码生成示例
- 理解了代码生成的应用场景
下一章我们将学习如何读取和重写TypeScript AST来实现代码转换!