罪在人民 發表於 2020-3-18 14:31:00

给萌新的 TS custom transformer plugin 教程——TypeScript 自定义转换器插件

<p>xuld/原创</p>
<h2>Custom transformer (自定义转换器)是干什么的</h2>
<p>简单说,TypeScript 可以将 TS 源码编译成 JS 代码,自定义转换器插件则可以让你定制生成的代码。比如删掉代码里的注释、改变变量的名字、将类转换为函数等等。</p>
<p>TypeScript 将 TS 代码编译到 JS 的功能,其实也是通过内置的转换器实现的,从 TS 2.3 开始,TS 将此功能开放,允许开发者编写自定义的转换器。</p>
<p>&nbsp;</p>
<h2>预备知识</h2>
<h3>语法树</h3>
<p>语法树是用于表示语法的数据结构。具体请参考我的另一个篇文章:https://www.cnblogs.com/xuld/p/12238167.html&nbsp;。</p>
<p>&nbsp;</p>
<h3>转换器原理</h3>
<p>TS 源码会先被解析为语法树,然后通过弱干个转换器生成新的语法树,最后通过代码打印器将语法树转回源码。</p>
<p><img src="https://img2020.cnblogs.com/i-beta/158732/202003/158732-20200318120421396-1731577028.png" alt="" width="964" height="217"></p>
<p>转换器本质就是一个函数,这个函数接收一个语法树,并返回转换后的新语法树。</p>
<p>自定义转换器分 before 和 after,其中,before 是位于内置转换器之前(转换 TS 代码),after 是位于内置转换器之后(转换已处理的 JS 代码)。</p>
<p>&nbsp;</p>
<h3>如何使用转换器</h3>
<p>官方的&nbsp;tsc 命令不支持加载自定义插件,但还有很多方法使用自定义转换器:</p>
<ol>
<li>直接调用 TS 编译器的 API 编译代码</li>
<li>使用社区提供的 TTypeScript 项目:https://github.com/cevek/ttypescript</li>
<li>使用 Webpack+TS-loader 编译项目,并且在 TS-loader 配置自定义转换插件:</li>
</ol>
<div class="cnblogs_code">
<pre><span>{
    test: /\.ts$/<span>,
    loader: 'ts-loader'<span>,
    options: {
      getCustomTransformers(program) {
            return<span> {
                before: ,
                after: []
            }
      }
    }
}</span></span></span></span></pre>
</div>
<p>其中,myTransformer 就是一个转换器。这里接收一个数组,可以传递多个转换器函数。</p>
<p>&nbsp;</p>
<h2>Hello world</h2>
<p>按惯例先来一个简单的例子,教你如何写一个转换器。</p>
<p>&nbsp;</p>
<p><strong>目标</strong>:将下面源码中字符串的内容改成 “Hello world”</p>
<div class="cnblogs_code">
<pre>console.log(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Hello xuld</span><span style="color: rgba(128, 0, 0, 1)">"</span>)</pre>
</div>
<p>&nbsp;</p>
<p>1. 新建一个 hello.js,内容如下:</p>
<div class="cnblogs_code">
<pre>const ts = require("typescript"<span style="color: rgba(0, 0, 0, 1)">)

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这是一个自定义转换器</span>
<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> createTransformer() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> context =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> node =&gt;<span style="color: rgba(0, 0, 0, 1)"> ts.visitNode(node, visit)

      </span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> visit(node) {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果发现字符串,替换为自己的内容</span>
            <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (ts.isStringLiteral(node)) {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span> ts.createStringLiteral("Hello world"<span style="color: rgba(0, 0, 0, 1)">)
            }
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 其它节点保持不变</span>
            <span style="color: rgba(0, 0, 255, 1)">return</span> ts.visitEachChild(node, <span style="color: rgba(0, 0, 0, 1)">visit, context)
      }
    }
}</span></pre>
</div>
<p>&nbsp;</p>
<p>2. 测试自定义转换器</p>
<p>为学习方便,这里采用直接调用 TS API 的方案使用转换器</p>
<div class="cnblogs_code">
<pre>const ts = require("typescript"<span style="color: rgba(0, 0, 0, 1)">)

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 要编译的源码</span>
const source = `console.log("Hello xuld"<span style="color: rgba(0, 0, 0, 1)">)`

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 编译源码</span>
const result =<span style="color: rgba(0, 0, 0, 1)"> ts.transpileModule(source, {
    transformers: { before: }
})

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 打印结果</span>
console.log(result.outputText)</pre>
</div>
<p>使用 node 执行以上代码可以看到最终的结果。</p>
<p>&nbsp;</p>
<h2>实现转换器</h2>
<p>转换器的职责是接收一个语法树节点,然后返回生成的新节点,如果这个节点无需变化(多数情况),可以返回节点本身。</p>
<p>需要特别注意的是:<strong>转换器只会生成新的节点,而不会修改原有节点</strong>。</p>
<p>这是因为一个节点会在多个地方被使用,而且很多地方针对节点作了缓存,为了确保系统稳定性,禁止修改节点可以避免很多意外的错误。</p>
<p>语法树是一种有层级的树结构,只要任何一个节点变化,这个节点的所有父节点都需要重新生成。为了避免每次重新创建大量节点浪费性能,TS 提供了 ts.visitNode,这个 API 接收一个节点和一个回调函数,然后将节点传递给回调函数,回调函数负责返回新节点,如果新节点和原节点相同,则重用旧节点,否则自动创建新的父节点。对用户而言,我们只需要使用&nbsp;ts.visitNode 找出需要处理的节点并返回新节点,其它情况使用默认的 ts.visitEachChild 即可。</p>
<p>&nbsp;</p>
<p>简而言之,无论你要做什么功能的转换器,不用在意原理,只要按这个模板填代码即可:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> createTransformer() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> context =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> node =&gt;<span style="color: rgba(0, 0, 0, 1)"> ts.visitNode(node, visit)

      </span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> visit(node) {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 其它代码不变,只需修改下面的部分</span>
            <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> =======================================</span>
            <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (判断节点的类型(node)) {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> 创建转换的节点(node)
            }
            </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (判断节点的类型(node)) {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> 创建转换的节点(node)
            }
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> =======================================</span>

            <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ts.visitEachChild(node, visit, context)
      }
    }
}</span></pre>
</div>
<p>&nbsp;</p>
<h3><span>判断节点的类型</span></h3>
<p>要判断节点的类型,可以通过 node.kind === SyntaxKind.xxx 比较,也可以通过 ts.isXXX(node):</p>
<p><img src="https://img2020.cnblogs.com/i-beta/158732/202003/158732-20200318125114718-1053841298.png" alt="" width="854" height="480"></p>
<p>&nbsp;</p>
<p>如果你不清楚你要处理的这个语法对于的类型叫什么,可以使用 AstExplorer&nbsp;。</p>
<p>&nbsp;</p>
<h3>创建转换的节点</h3>
<p>创建转换后的新节点有两种方式:一种是最简单的,使用 ts.createXXX 创建;还有一种 ts.updateXXX 是基于已有的节点,如果节点发生变化则创建新节点,否则重用节点(主要为了节约内存损耗)。</p>
<p><img src="https://img2020.cnblogs.com/i-beta/158732/202003/158732-20200318125518462-180018948.png" alt="" width="856" height="484"></p>
<p>&nbsp;</p>
<p>比如要创建一个表示 a + 1 的节点:</p>
<div class="cnblogs_code">
<pre>ts.createBinary(ts.createIdentifier("a"), ts.SyntaxKind.PlusToken, ts.createNumericLiteral(1))</pre>
</div>
<p>&nbsp;</p>
<h3>替换变量名</h3>
<p>按以上的思路,替换变量名就需要:先找出变量名对应的节点,然后返回替换后的新变量名:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将代码中变量 foo 变成 goo</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (ts.isIdentifier(node) &amp;&amp; node.text === "goo"<span style="color: rgba(0, 0, 0, 1)">) {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> ts.createIdentifier("goo"<span style="color: rgba(0, 0, 0, 1)">)
}</span></pre>
</div>
<p>但这里有个问题,就是变量名、函数名、类名也都是 Identifier 类型的节点,上面代码会全部换掉,有时,我们只希望处理某些条件下的节点,这时可以添加更多的判断,比如只替换作为函数名调用的 foo() 中的 foo,但不替换其它场景:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">if</span> (ts.isIdentifier(node) &amp;&amp; node.text === "goo" &amp;&amp;<br>    ts.isCallExpression(node.parent) &amp;&amp; node.parent.expression ===<span style="color: rgba(0, 0, 0, 1)"> node) {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> ts.createIdentifier("goo"<span style="color: rgba(0, 0, 0, 1)">)
}</span></pre>
</div>
<p>&nbsp;</p>
<h2>转换上下文</h2>
<p>所有转换器都接收一个参数 context,表示转换的上下文。转换的上下文主要用于:</p>
<ol>
<li>提供了一些实用的 API</li>
<li>在多个转换器之间共享数据</li>
<li>注册生成节点为字符串时的附加事件</li>
</ol>
<h3>自动生成变量&nbsp;</h3>
<p><strong>目标</strong>:支持 case 语句中使用 it 关键字:</p>
<p>&nbsp;源代码:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">switch</span> (1 + 1<span style="color: rgba(0, 0, 0, 1)">) {
    </span><span style="color: rgba(0, 0, 255, 1)">case</span> it == 2<span style="color: rgba(0, 0, 0, 1)">:
}</span></pre>
</div>
<p>转换后:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> _t_1;<br>_t_1 = 1 + 1
<span style="color: rgba(0, 0, 255, 1)">switch</span> (<span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
    </span><span style="color: rgba(0, 0, 255, 1)">case</span> _t_1 == 2<span style="color: rgba(0, 0, 0, 1)">:
}</span></pre>
</div>
<p>代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> createTransformer() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> context =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> node =&gt;<span style="color: rgba(0, 0, 0, 1)"> ts.visitNode(node, visit)

      </span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> visit(node) {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (ts.isSwitchStatement(node)) {
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 创建临时变量</span>
                const name = ts.createUniqueName("_t"<span style="color: rgba(0, 0, 0, 1)">)
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 插入变量</span>
<span style="color: rgba(0, 0, 0, 1)">                context.hoistVariableDeclaration(name)
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 生成两行代码</span>
                <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> [
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 赋值变量</span>
<span style="color: rgba(0, 0, 0, 1)">                  ts.createExpressionStatement(ts.createAssignment(name, node.expression)),
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将 switch 的条件改为 true</span>
                  ts.createSwitch(ts.createTrue(), ts.visitEachChild(node.caseBlock, child =&gt;<span style="color: rgba(0, 0, 0, 1)"> visitSwitch(child, name), context))
                ]
            }
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 其它节点保持不变</span>
            <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ts.visitEachChild(node, visit, context)
      }

      </span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> visitSwitch(node, name) {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将 it 变为新的变量名</span>
            <span style="color: rgba(0, 0, 255, 1)">if</span> (ts.isIdentifier(node) &amp;&amp; node.text === "it"<span style="color: rgba(0, 0, 0, 1)">) {
                </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> name
            }
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 其它节点保持不变</span>
            <span style="color: rgba(0, 0, 255, 1)">return</span> ts.visitEachChild(node, child =&gt;<span style="color: rgba(0, 0, 0, 1)"> visitSwitch(child, name), context)
      }
    }
}</span></pre>
</div>
<p>思路:先创建一个临时变量,存放 switch 条件内容,然后将原始条件改成 true,并将内部 it 替换掉。</p>
<p>&nbsp;</p>
<h3>报错</h3>
<p>在转换时,如果需要报错,可以使用 context.addDiagnostic(diag)</p>
<p>&nbsp;</p>
<h3>使用类型信息</h3>
<p>在实际场景中,可能需要用到代码的类型信息(比如变量有没有定义,变量在哪些地方被使用,变量的类型)</p>
<p>转换器本身并没有直接提供这些信息,但可以通过 program.getTypeChecker() 获取到 TypeChecker,然后通过 TypeChecker 提供的丰富 API 获取到这些信息。</p>
<p>如果是采用了 ts-loader, program 对象通过 getCustomTransformer() 的参数得到。</p>
<p>&nbsp;</p>
<p><em>[[]]</em></p>
<p>&nbsp;</p>
<p>xuld/原创</p>
<h2>更多案例</h2>
<p>这里列了一些社区的现成插件,方便研究学习:</p>
<ul>
<li><code>ts-nameof</code></li>
<li><code>ts-optchain/transform</code></li>
<li><code>ts-transform-asset</code></li>
<li><code>ts-transform-auto-require</code></li>
<li><code>ts-transform-css-modules/dist/transform</code></li>
<li><code>ts-transform-graphql-tag/dist/transformer</code></li>
<li><code>ts-transform-img/dist/transform</code></li>
<li><code>ts-transform-react-intl/dist/transform</code></li>
<li><code>ts-transformer-enumerate/transformer</code></li>
<li><code>ts-transformer-keys/transformer</code></li>
<li><code>ts-transformer-minify-privates</code></li>
<li><code>typescript-is/lib/transform-inline/transformer</code></li>
<li><code>typescript-plugin-styled-components</code></li>
<li><code>typescript-transform-jsx</code></li>
<li><code>typescript-transform-macros</code></li>
<li><code>typescript-transform-paths</code></li>
<li><code>@zerollup/ts-transform-paths</code></li>
<li><code>@zoltu/typescript-transformer-append-js-extension</code></li>
<li><code>@magic-works/ttypescript-browser-like-import-transformer</code></li>
</ul>
<p>如果你想成为大厂前端架构师,如果你还有成长的激情,&nbsp;</p>
<p>欢迎关注“我是前端架构师”微信公众号</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><img src="https://img2020.cnblogs.com/blog/158732/202004/158732-20200406104341355-1579438269.png" alt="" width="635" height="218"></p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/xuld/p/12516828.html
頁: [1]
查看完整版本: 给萌新的 TS custom transformer plugin 教程——TypeScript 自定义转换器插件