最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 使用 babel 来生成 javascript 代码

    正文概述 掘金(kerwinleeyi)   2020-11-26   779

    前言

    今天在开发cli工具的时候遇到了一个场景,通过命令向项目添加完sentry后,需要自动向shared.module.ts文件添加两行ts代码用于引入依赖。如下:

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { RouterModule } from '@angular/router';
    import { TranslateModule } from '@ngx-translate/core';
    import { ReactiveFormsModule, FormsModule } from '@angular/forms';
    import { COMPONENTS } from './components';
    import { DIRECTIVES } from './directives';
    import { ZorroModule } from '@modules/zorro/zorro.module';
    import { PIPES } from './pipes';
    import { SENTRY_PROVIDERS } from '@core/sentry'; // 需要添加的代码
    
    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        RouterModule,
        TranslateModule,
        ReactiveFormsModule,
        ZorroModule,
      ],
      declarations: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
      exports: [
        CommonModule,
        FormsModule,
        RouterModule,
        TranslateModule,
        ReactiveFormsModule,
        ZorroModule,
        ...COMPONENTS,
        ...DIRECTIVES,
        ...PIPES,
      ],
      providers: [SENTRY_PROVIDERS], // 需要添加的代码
    })
    class SharedModule {}
    
    export { SharedModule };
    

    刚开始咱是通过正则的方式来处理,但在 review 代码过程中,大佬表示这种方式风险性太高,建议用babel来处理这种情况。由于之前只用过babel来做些简单的兼容处理,从未用其来生成代码,于是便利用闲暇时间进行了一番摸索。本文主要概括了babel几个核心插件的使用方法和开发中的部分技巧,希望能给大家一些帮助。

    Babel 介绍

    一想到babel,大家第一反应应该是它在兼容方面的处理。它可以将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。但在这里,我们主要是介绍如果使用babel来生成咱们期望的javascript代码。

    先介绍下咱们将用到的babel插件

    • @babel/parser:将javascript代码编译为抽象语法树(Abstract Syntax Tree,AST)(后续简称AST
    • @babel/traverse:遍历AST,通过这个插件,我们可以对AST上的任意节点进行增删查改
    • @babel/types:AST 节点类型,通过该库咱们可以生成想要的AST节点
    • @babel/generator:编译AST来生成javascript代码

    创建项目 && 安装依赖

    mkdir ast-demo && cd ast-demo && npm init #  创建项目并初始化package.json
    mkdir code && cd code && touch demo.ts # 新建code/demo.ts用于放置待解析的ts代码
    touch run.js # 用于放置核心逻辑
    npm install @babel/parser @babel/traverse @babel/types @babel/generator @babel/core --save-dev
    

    最后将待解析的这段代码粘贴至ast-demo/code/demo.ts文件中,如下:

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { RouterModule } from '@angular/router';
    import { TranslateModule } from '@ngx-translate/core';
    import { ReactiveFormsModule, FormsModule } from '@angular/forms';
    import { COMPONENTS } from './components';
    import { DIRECTIVES } from './directives';
    import { ZorroModule } from '@modules/zorro/zorro.module';
    import { PIPES } from './pipes';
    
    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        RouterModule,
        TranslateModule,
        ReactiveFormsModule,
        ZorroModule,
      ],
      declarations: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
      exports: [
        CommonModule,
        FormsModule,
        RouterModule,
        TranslateModule,
        ReactiveFormsModule,
        ZorroModule,
        ...COMPONENTS,
        ...DIRECTIVES,
        ...PIPES,
      ],
      providers: [], // 需要添加的代码
    })
    class SharedModule {}
    
    export { SharedModule };
    

    转换 javascript 代码

    使用 babel 来生成 javascript 代码

    使用@babel/parser 解析 javascript 代码生成 AST

    第一步,我们要将对应的javascript代码解析为AST。这里因为涉及到文件的读写,后续我们都用node来处理:

    需要注意的是,在使用@babel/parser时,由于待解析代码中有装饰器,所以必须添加decorators-legacy这个插件才能识别,否则会报错:

    SyntaxError: This experimental syntax requires enabling one of the following parser plugin(s): 'decorators-legacy, decorators' (11:0)

    const { parse } = require('@babel/parser');
    const path = require('path');
    const fs = require('fs');
    const codePath = './code/demo.ts'; // 待解析代码路径,
    const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
    const ast = parse(file, {
      sourceType: 'module',
      plugins: ['decorators-legacy'], // 如果待代码中有装饰器,需要添加该plugin,才能识别。
    });
    

    使用@babel/traverse 遍历 AST 节点,并对特殊节点进行处理

    在获取到对应的AST后,我们便可以对其节点进行修改

    这里我们拿import xx from xx这个语法举例,在run.js中添加该代码

    const { parse } = require('@babel/parser');
    const traverse = require('@babel/traverse').default; // 遍历ast,对各个节点进行处理
    const path = require('path');
    const fs = require('fs');
    const codePath = './code/demo.ts'; // 待解析代码路径,
    const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
    const ast = parse(file, {
      sourceType: 'module',
      plugins: ['decorators-legacy'], // 如果待代码中有装饰器,需要添加该plugin,才能识别。
    });
    let num = 0;
    traverse(ast, {
      ImportDeclaration(path) {
        num++;
        console.log(num); //  输出1,2,3,4,5,6,7,8,9
      },
    });
    

    执行命令node run.js结果如下 使用 babel 来生成 javascript 代码

    接下来,回到正题,我们期望的结果是:

    • class前面添加一行import { SENTRY_PROVIDERS } from '@core/sentry';(有的同学可能会想,看代码结构,为什么不是在@NgModule这个装饰器前面添加呢?而是在class前面 ?。大家可以思考下,后续我们来填坑 )
    • @NgModule装饰器里面添加一个键值对,providers: [SENTRY_PROVIDERS]

    那么如何知道我们class SharedModule对应的AST节点类型呢?

    这里由于AST 节点类型实在太多了,去官方文档查看的话,会花费很多时间。这里推荐使用AST Explorer。

    使用 babel 来生成 javascript 代码

    如果希望获取某个具体的节点,在左侧源码中选择对应的代码即可,右侧黄色部分即节点类型 使用 babel 来生成 javascript 代码

    知道了如何获取AST节点类型后,接下来我们便可以通过同样的方式来获取class对应的节点类型 使用 babel 来生成 javascript 代码

    这里大家可以发现和咱们代码中表现的不同,ClassDeclaration内部包含了节点Decorator,而不是咱们代码中直观看到的装饰器与类是同级的。这也填了咱们前文中的坑。如果直接在@NgModule的前一个节点添加ImportDeclaration,那么它会添加在ClassDeclaration的内部,不是我们期望的结果。熟悉装饰器的同学也应该知道,装饰器可以装饰类、属性、方法等,而不会独立存在的。所以如果你理解装饰器,这里应该第一想到的是应该去ClassDeclaration前面添加需要的节点,当然,通过AST Explorer也可以直观的得出结果。

    接下来修改run.js并运行,通过path.node属性可以获取对应的AST节点

    const { parse } = require('@babel/parser');
    const traverse = require('@babel/traverse').default; // 遍历ast,对各个节点进行处理
    const path = require('path');
    const fs = require('fs');
    const codePath = './code/demo.ts'; // 待解析代码路径,
    const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
    const ast = parse(file, {
      sourceType: 'module',
      plugins: ['decorators-legacy'], // 如果待代码中有装饰器,需要添加该plugin,才能识别。
    });
    
    traverse(ast, {
      ClassDeclaration(path) {
        console.log(path.node); // add it
      },
    });
    

    使用 babel 来生成 javascript 代码

    使用@babel/types 创建新的 AS 节点

    通过@babel/traverseAST Explorer,我们找到了class sharedModule对应的AST节点类型。接下来,我们来生成新代码import { SENTRY_PROVIDERS } from '@core/sentry'

    这时候便是@babel/types登场了,它可以帮助我们创建AST节点。详情可以参考@babel/types api 文档。

    在文档中,我们可以看到许多的 api,可以帮助你创建任意已知的AST节点。那么问题来了,我怎么知道如何组合这些 api 来生成我的代码呢?

    我们拿import { SENTRY_PROVIDERS } from '@core/sentry'这行代码举例。同样需要AST Explorer,观察其对应的AST

    使用 babel 来生成 javascript 代码

    显而易见,它的AST节点类型ImportDeclaration

    接着,我们便查看@babel/types api 文档是如何生成一个ImportDeclaration节点的。

    使用 babel 来生成 javascript 代码

    通过文档,我们了解到,要生成import xx from xx这种格式的代码,需要两个参数specifierssource。那么我们可以先添加如下代码

    const t = require('@babel/types');
    
    t.importDeclaration(specifiers, source); // specifiers, source为定义
    

    specifiers的类型是Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>数组对象。如果你现在不确定其节点类型是ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier的哪一个话,那么便可以回到AST Explorer去查看。

    点击SENTRY_PROVIDERS可以获取当前的节点类型Identifier,其可以理解为咱们的变量/标识,其父级便是ImportSpecifier类型。 使用 babel 来生成 javascript 代码

    确定类型后,返回@babel/types api 文档,查看生成ImportSpecifier节点,需要local importedimportKind参数,而localimported必填,是Identifier类型,也就是变量。

    修改代码如下

    const t = require('@babel/types');
    const local = t.Identifier('SENTRY_PROVIDERS');
    const imported = t.Identifier('SENTRY_PROVIDERS');
    const specifiers = [t.ImportSpecifier(local, imported)];
    const importDeclaration = t.importDeclaration(specifiers, source); // source未定义
    

    完成了ImportSpecifier节点的生成,接下来我们查看ImportDeclaration所需要的第二个参数,即source对应的节点类型是StringLiteral,采用同样的方式去查找生成StringLiteral节点所需的参数。

    使用 babel 来生成 javascript 代码

    使用 babel 来生成 javascript 代码

    使用 babel 来生成 javascript 代码

    修改代码如下,便获得了最终import xx from 'xx'这个语法对应的AST

    const { parse } = require('@babel/parser');
    const traverse = require('@babel/traverse').default; // 遍历ast,对各个节点进行处理
    const t = require('@babel/types');
    const path = require('path');
    const fs = require('fs');
    const codePath = './code/demo.ts'; // 待解析代码路径,
    const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
    const ast = parse(file, {
      sourceType: 'module',
      plugins: ['decorators-legacy'], // 如果待代码中有装饰器,需要添加该plugin,才能识别。
    });
    
    traverse(ast, {
      ClassDeclaration(path) {
        const local = t.Identifier('SENTRY_PROVIDERS');
        const imported = t.Identifier('SENTRY_PROVIDERS');
        const specifiers = [t.ImportSpecifier(local, imported)];
        const source = t.stringLiteral('@core/sentry');
        const importDeclaration = t.importDeclaration(specifiers, source);
    
        console.log(importDeclaration);
      },
    });
    

    对当前的 ast 节点进行操作

    在获得了ImportDeclarationAST后,我们需要对原来的AST进行修改,从而生成新的AST

    这里便需要用到@babel/traverse中的path参数了。 相关的参数可以查看babel 操作手册-转换操作。文档中对已知的 api 都进行了说明。

    我们需要在ClassDeclaration前面添加ImportDeclaration节点,修改代码如下:

    const { parse } = require('@babel/parser');
    const traverse = require('@babel/traverse').default; // 遍历ast,对各个节点进行处理
    const t = require('@babel/types');
    const path = require('path');
    const fs = require('fs');
    const codePath = './code/demo.ts'; // 待解析代码路径,
    const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
    const ast = parse(file, {
      sourceType: 'module',
      plugins: ['decorators-legacy'], // 如果待代码中有装饰器,需要添加该plugin,才能识别。
    });
    
    traverse(ast, {
      ClassDeclaration(path) {
        const local = t.Identifier('SENTRY_PROVIDERS');
        const imported = t.Identifier('SENTRY_PROVIDERS');
        const specifiers = [t.ImportSpecifier(local, imported)];
        const source = t.stringLiteral('@core/sentry');
        const importDeclaration = t.importDeclaration(specifiers, source);
    
        path.insertBefore(importDeclaration); // update it
      },
    });
    

    这里还有一步操作是在@NgModule装饰器里面添加providers: [SENTRY_PROVIDERS]键值对,使用的是上述同样方法。直接上代码:

    const { parse } = require('@babel/parser');
    const traverse = require('@babel/traverse').default; // 遍历ast,对各个节点进行处理
    const path = require('path');
    const fs = require('fs');
    const codePath = './code/demo.ts'; // 待解析代码路径,
    const file = fs.readFileSync(path.resolve(__dirname, codePath)).toString();
    const ast = parse(file, {
      sourceType: 'module',
      plugins: ['decorators-legacy'], // 如果待代码中有装饰器,需要添加该plugin,才能识别。
    });
    let code;
    let hasProviders = false;
    
    traverse(ast, {
      ClassDeclaration(path) {
        const local = t.Identifier('SENTRY_PROVIDERS');
        const imported = t.Identifier('SENTRY_PROVIDERS');
        const specifiers = [t.ImportSpecifier(local, imported)];
        const source = t.stringLiteral('@core/sentry');
        const importDeclaration = t.importDeclaration(specifiers, source);
    
        path.insertBefore(importDeclaration); // 在当前ClassDeclaration节点前插入importDeclaration节点
      },
      ObjectProperty(path) {
        // ObjectProperty 对应js语法中的键值对, xx: xx
        if (path.node.key.name === 'providers') {
          // 这里判断,如果代码中已经存在 key值 providers,直接进行添加
          hasProviders = true;
          path.node.value.elements.push(t.identifier('SENTRY_PROVIDERS')); // path.node.value.elements可以通过AST Explorer来查看对应层级
        }
        if (!hasProviders && isEnd(path.getAllNextSiblings())) {
          // 判断如果遍历到最后一个ObjectProperty,仍没有providers属性,则添加键值对
          hasProviders = false;
          // 在当前节点后面添加一个键值对
          path.insertAfter(
            t.objectProperty(t.identifier('providers'), t.arrayExpression())
          );
        }
      },
    });
    
    function isEnd(nodes) {
      return !nodes.some((item) => item.node.type === 'ObjectProperty');
    }
    

    使用@babel/generator 生成代码

    最后使用@babel/generator将其AST编译为代码。可以在@babel/generator api来获取更多信息。接着使用fs模块将代码写入到目标文件中

    添加代码如下:

    ...
    
    fs.writeFileSync(codePath, generate(ast, {}, code).code);
    console.log('Success to generate it');
    
    

    完整代码: github 源码

    参考链接

    使用 babel 修改 js 代码


    起源地下载网 » 使用 babel 来生成 javascript 代码

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元