拓冰建站拓冰建站
首页 / 资讯中心 / 正文

使用 @lit/ts-transformers 将 Lit 装饰器编译为原生 JavaScript:TypeScript Transformer 完整实战指南

使用 lit/ts-transformers 将 Lit 装饰器编译为原生 JavaScriptTypeScript Transformer 完整实战指南【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/litlit/ts-transformers 是 Lit 官方仓库中专门用于编译期处理的一组 TypeScript 自定义 Transformer其核心价值在于把customElement、property、query等官方 Lit 装饰器在编译阶段直接展开为等价的普通 JavaScript从而让使用装饰器的组件代码依然可以输出「无装饰器依赖」的产物。读完本文你将掌握这三个 TransformeridiomaticDecoratorsTransformer、preserveBlankLinesTransformer、constructorCleanupTransformer各自的行为、适用场景与运行时机并能通过 TypeScript Compiler API、ttypescript/ts-patch 以及 rollup/plugin-typescript 三种方式把它们接入自己的构建流程。为什么需要一组「Lit 装饰器」TransformerLit 的声明式 APIcustomElement、property、state、query等极大提升了组件开发体验但这些装饰器在运行时依赖lit/decorators.js模块。如果你希望产出的 JavaScript 完全不依赖装饰器运行时逻辑或者你的下游消费者需要的是最朴素的原生类 customElements.define形态就可以借助 TypeScript 编译器提供的自定义 TransformercustomTransformers机制在代码发射emit阶段把装饰器就地翻译成等价的普通 JavaScript。这正是 packages/ts-transformers/README.md 中定义的三件套要做的事。三者各有分工Transformer运行时机职责idiomaticDecoratorsTransformerbefore将所有官方 Lit 类装饰器与成员装饰器替换为地道的原生 JavaScriptpreserveBlankLinesTransformerbefore用特殊占位注释保留源码中的空行弥补 TypeScript 发射时不保留空行的缺陷constructorCleanupTransformerafter恢复构造器的源码位置并简化super(...)调用安装npm i lit/ts-transformers从仓库中的 package.json 可以看到该包以typescript ^5.5.0作为 peerDependency并以ts-clone-node作为运行时依赖包本身采用 CommonJS 格式发布发布产物包含根目录的*.js、*.d.ts以及internal/子目录。Transformer 一idiomaticDecoratorsTransformerimport {idiomaticDecoratorsTransformer} from lit/ts-transformers;它会把所有官方 Lit 类装饰器和属性装饰器替换为地道的 vanilla JavaScript。必须作为beforetransformer 运行——因为它的职责是在 TypeScript 进一步处理之前先把源码中的装饰器语法消解掉。输入与输出示例输入import {LitElement, html} from lit; import {customElement, property} from lit/decorators.js; customElement(simple-greeting) class SimpleGreeting extends LitElement { property() name World; render() { return htmlpHello ${this.name}!/p; } }经过转换后输出import {LitElement, html} from lit; class SimpleGreeting extends LitElement { static properties { name: {}, }; constructor() { super(); this.name World; } render() { return htmlpHello ${this.name}!/p; } } customElements.define(simple-greeting, SimpleGreeting);可以看到三类关键变换customElement(simple-greeting)被删除并在类定义之后追加了一条customElements.define(simple-greeting, SimpleGreeting)语句property()字段被删除字段元数据被聚合进static properties字段初始化表达式name World被移动进构造器this.name World从lit/decorators.js的导入被整体移除如果没有其他仍需要的绑定。需要说明的是html模板字符串本身不属于该 Transformer 的处理范围模板内容原样透传源码中html的导入仅被记录、不会触发删除参见 lit-transformer.ts 中的注释。上述示例中输出里的name: {}表示空选项对象字段名始终取自成员名本身。支持的装饰器与转换行为README 中给出了完整的对照表这是接入前必读的行为契约DecoratorTransformer behaviorcustomElementAdds acustomElements.definecallpropertyAdds an entry tostatic properties, and moves initializers to theconstructorstateSame aspropertywith{state: true}queryDefines a getter that callsquerySelectorquerySelectorAllDefines a getter that callsquerySelectorAllqueryAsyncDefines anasyncgetter that awaitsupdateCompleteand then callsquerySelectorqueryAssignedElementsDefines a getter that callsquerySelector(slot[namefoo]).assignedElementsqueryAssignedNodesDefines a getter that callsquerySelector(slot[namefoo]).assignedNodeslocalizedAdds anupdateWhenLocaleChangescall to the constructor源码层面的实现原理从 idiomatic-decorators.ts 可以看到该 Transformer 的核心是一个LitTransformer它注册了十个 visitor分别对应上表中的装饰器外加一个eventOptionsvisitorCustomElementVisitor读取装饰器参数中的元素名生成customElements.define(name, ClassName)调用并放入类之后的相邻语句custom-element.tsPropertyVisitor把property字段的选项并入reactiveProperties把字段初始化器改写为this.xxx ...追加进构造器然后删除原字段property.tsStateVisitor继承PropertyVisitor在选项对象上强制注入{state: true}并过滤掉用户显式传入的state键以避免重复state.tsQueryVisitor把query(#foo, true)字段改写为 getter缓存模式下返回this.__foo ?? this.renderRoot?.querySelector(#foo) ?? nullquery.tsLocalizedVisitor把localized()转换为构造器中的updateWhenLocaleChanges(this)调用同时把导入绑定从localized替换为updateWhenLocaleChangeslocalized.ts。LitTransformer的调度逻辑lit-transformer.ts还包含两个值得注意的工程细节按需遍历只有文件中存在来自官方 Lit 包lit、lit-element、lit/*及其子路径的导入且注册了对应 visitor 时才会遍历该文件其余文件原样返回因此对无关代码零开销导入风格校验装饰器导入必须是lit/decorators.js这种带.js后缀的写法或模块默认导入形式否则会抛出一个带位置的诊断错误提示Did you mean ...js?对应isLitImport与hasJsExtensionOrIsDefaultModule的实现见 lit-transformer.ts。这也是 README 示例中统一使用lit/decorators.js的原因。Transformer 二preserveBlankLinesTransformerimport { preserveBlankLinesTransformer, BLANK_LINE_PLACEHOLDER_COMMENT, BLANK_LINE_PLACEHOLDER_COMMENT_REGEXP, } from lit/ts-transformers;这是一个可读性readabilityTransformerTypeScript 在发射代码时不会保留源码中的空行参见 TypeScript#843因此它把原始源码中的空行替换为一个特殊注释待变换结束后再通过简单的查找替换把注释还原成换行从而保住代码的分段观感。生成的注释内容永远是下面这一串常量定义见 preserve-blank-lines.ts//__BLANK_LINE_PLACEHOLDER_G1JVXUEBNCL6YN5NFE13MD1PT3H9OIHB__该字符串内嵌了一段固定随机串其唯一作用是使碰撞概率低到可以忽略。必须作为beforetransformer 运行并且通常应放在所有其他 transformer 之前——这样后续变换在处理节点时才能看到这些占位注释。注意该常量是包对外导出的公开 API更改它属于破坏性变更README 与源码注释均强调这一点下游工具可能会依赖它。实现要点与配套清理方式从 preserve-blank-lines.ts 的实现看它对每个源文件创建一个PreserveBlankLinesTransformer实例通过addComments扫描节点的前置 trivia统计注释区间之间的空行数量按空行数插入等量的占位注释由于同一段 trivia 可能被多个 AST 节点共享例如let x0的 VariableStatement 与 VariableDeclarationList实现中用handledTriviaRanges集合去重避免重复发射注释。配套清理方式有以下几种使用导出的BLANK_LINE_PLACEHOLDER_COMMENT_REGEXP正则匹配包括前置缩进与//在内的完整注释使用 sed 命令README 原文给出的写法sed -i $s/\s*\/\/__BLANK_LINE_PLACEHOLDER_G1JVXUEBNCL6YN5NFE13MD1PT3H9OIHB__/\\\n/g lib/*.js lib/**/*.js或在 Rollup 流程中用rollup/plugin-replace将注释替换为空字符串见下文完整示例。Transformer 三constructorCleanupTransformerimport {constructorCleanupTransformer} from lit/ts-transformers;这是一个可读性 Transformer做两件事恢复构造器位置把构造器移回其在源码中的原始位置如果构造器完全是合成的源码中并不存在则移到类中最后一个static字段的正下方。背景是默认情况下只要 TypeScript 修改过构造器它就会被移动到类的顶部——这通常不符合人的阅读习惯。简化super(...)调用在类的所有祖先类都没有带参数的构造器时以类型检查器的判定为准把super(...arguments)简化为super()。必须作为aftertransformer 运行。原因在源码注释中写得很明确constructor-cleanup.ts合成构造器是由 TypeScript 在 after 阶段生成的如果把它放在before阶段它将看不到合成构造器从而完全无效。源码层面的判定逻辑实现细节constructor-cleanup.ts值得展开通过ctor.flags ts.NodeFlags.Synthesized判断构造器是否被 TypeScript 创建/修改过若没有该标志说明构造器仍在源码原位置直接跳过对于被移动过的构造器如果ctor.pos仍指向源码位置ctor.pos ! class_.pos ctor.pos ! -1就把它插回成员列表中它原来所处的次序如果完全合成位置为 -1则放到最后一个 static 成员之后并在上方补一条空行占位注释完全合成的构造器一定带super(...arguments)此时用findSuperSpreadArgument定位展开参数再通过anyAncestorConstructorHasParameters结合program.getTypeChecker()遍历继承链判断是否可以删除...arguments删除采用登记待删除节点 遍历时跳过的方式而不是直接修改 AST。三种方式接入构建流程README 提供了三条接入路径覆盖了从底层 Compiler API 到日常工程化的全部场景。方式一TypeScript Compiler API如果你直接使用 TypeScript 编译器 API把 transformer 传给emit的customTransformers参数即可import ts from typescript; import { idiomaticDecoratorsTransformer, preserveBlankLinesTransformer, constructorCleanupTransformer, } from lit/ts-transformers; // 注意这不是一个完整示例。更多信息参见 TypeScript Compiler API 文档。 const program ts.createProgram(...); const result program.emit(undefined, undefined, undefined, undefined, { before: [ // 可选保留空行以获得更好的可读性。 preserveBlankLinesTransformer(), // 将 Lit 装饰器转换为地道的 vanilla JavaScript。 idiomaticLitDecoratorTransformer(program), ], after: [ // 可选对构造器做可读性优化。 constructorCleanupTransformer(program), ], });注意两点idiomaticDecoratorsTransformer与constructorCleanupTransformer都是工厂函数需要传入program而preserveBlankLinesTransformer无需参数。方式二ttypescript / ts-patchttypescript 与 ts-patch 是两种类似的工具它们给 TypeScript 编译器打补丁允许直接在tsconfig.json中声明变换{ compilerOptions: { plugins: [ { transform: lit/ts-transformers, import: preserveBlankLinesTransformer }, { transform: lit/ts-transformers, import: idiomaticDecoratorsTransformer }, { transform: lit/ts-transformers, import: constructorCleanupTransformer, after: true } ] } }关键点preserveBlankLinesTransformer与idiomaticDecoratorsTransformer默认作为 before 运行插件顺序即执行顺序constructorCleanupTransformer必须显式声明after: true。如果使用了preserveBlankLinesTransformer一种移除空行占位注释的方式是 sedsed -i $s/\s*\/\/__BLANK_LINE_PLACEHOLDER_G1JVXUEBNCL6YN5NFE13MD1PT3H9OIHB__/\\\n/g lib/*.js lib/**/*.js方式三rollup/plugin-typescriptrollup/plugin-typescript 是 Rollup 生态中支持 transformer 的 TypeScript 编译插件。完整示例import typescript from rollup/plugin-typescript; import resolve from rollup/plugin-node-resolve; import replace from rollup/plugin-replace; import { idiomaticDecoratorsTransformer, preserveBlankLinesTransformer, constructorCleanupTransformer, BLANK_LINE_PLACEHOLDER_COMMENT, } from lit/ts-transformers; export default { input: ./src/my-element.ts, plugins: [ typescript({ transformers: { before: [ // 可选保留空行以获得更好的可读性。 {factory: preserveBlankLinesTransformer}, // 将 Lit 装饰器转换为地道的 vanilla JavaScript。 {type: program, factory: idiomaticDecoratorsTransformer}, ], after: [ // 可选对构造器做可读性优化。 {type: program, factory: constructorCleanupTransformer}, ], }, }), // 仅在使用了 preserveBlankLinesTransformer 时需要。 replace({ values: { [//${BLANK_LINE_PLACEHOLDER_COMMENT}]: , }, delimiters: [, ], }), resolve(), ], output: { file: dist/my-element.js, format: esm, }, };注意这里的两个细节依赖program的 transformer 需要标记{type: program}由于BLANK_LINE_PLACEHOLDER_COMMENT常量本身不带//前缀所以 replace 的查找串要手动补上//。测试与验证仓库内为这三个 Transformer 配备了完整的测试套件可以作为接入前的行为参照idiomatic-decorators-test.ts 覆盖了customElement、property含已有/无构造器两种情形、query、state、localized等装饰器的输入输出断言。测试通过compileTsFragment把源码片段送入真实编译器并将输出用 Prettier 格式化后与期望结果比对同时会把占位注释还原为换行后再比较见该文件 第 29-79 行 的checkTransform辅助函数constructor-cleanup-test.ts 与 preserve-blank-lines-test.ts 分别验证构造器位置/super()简化和空行占位行为测试框架使用uvu运行命令为uvu tests/ -test\.js$见 package.json且test任务依赖完整的build包括 lit 与 localize 包的构建说明该包与 Lit 主包、lit/localize 之间存在真实的联动验证关系。使用注意事项小结运行时机不可搞反preserveBlankLinesTransformer与idiomaticDecoratorsTransformer必须放在before且空行占位 Transformer 通常要排在最前constructorCleanupTransformer必须放在after否则不生效。导入写法官方 Lit 装饰器必须使用带.js后缀的导入路径如lit/decorators.js、lit/reactive-element/decorators.js否则 Transformer 会直接抛出诊断错误。空行占位注释是公开契约BLANK_LINE_PLACEHOLDER_COMMENT与BLANK_LINE_PLACEHOLDER_COMMENT_REGEXP均对外导出修改该常量属于破坏性变更无论用 sed、正则还是 replace 插件清理都要保证与注释内容严格一致。适用的代码形态该工具面向的是使用官方 Lit 装饰器含localized的 TypeScript 组件它按需遍历文件对不相关的源码零影响可以放心地放进共享编译管线。【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门