• Babel插件开发入门指南


    文章概览

    主要包括:Babel如何进行转码、插件编写的入门基础、实例讲解如何编写插件。

    阅读本文前,需要读者对Babel插件如何使用、配置有一定了解,可以参考笔者之前的文章

    本文所有例子可以在 笔者的github 找到,欢迎访问笔者博客获取更多相关文章。

    Babel运行阶段

    首先来了解Babel转码的过程分三个阶段:分析(parse)、转换(transform)、生成(generate)。

    其中,分析、生成阶段由Babel核心完成,而转换阶段,则由Babel插件完成,这也是本文的重点。

    分析

    Babel读入源代码,经过词法分析、语法分析后,生成抽象语法树(AST)

    parse(sourceCode) => AST
    

    转换

    经过前一阶段的代码分析,Babel得到了AST。在原始AST的基础上,Babel通过插件,对其进行修改,比如新增、删除、修改后,得到新的AST。

    transform(AST, BabelPlugins) => newAST
    

    生成

    通过前一阶段的转换,Babel得到了新的AST,然后就可以逆向操作,生成新的代码。

    generate(newAST) => newSourceCode
    

    插件基础入门

    典型的Babel插件结构,如下代码所示。

    export default function({ types: babelTypes }) {
      return {
        visitor: {
          Identifier(path, state) {},
          ASTNodeTypeHere(path, state) {}
        }
      };
    };
    

    需要关注的内容如下:

    • babelType:类似lodash那样的工具集,主要用来操作AST节点,比如创建、校验、转变等。举例:判断某个节点是不是标识符(identifier)。
    • path:AST中有很多节点,每个节点可能有不同的属性,并且节点之间可能存在关联。path是个对象,它代表了两个节点之间的关联。你可以在path上访问到节点的属性,也可以通过path来访问到关联的节点(比如父节点、兄弟节点等)
    • state:代表了插件的状态,你可以通过state来访问插件的配置项。
    • visitor:Babel采取递归的方式访问AST的每个节点,之所以叫做visitor,只是因为有个类似的设计模式叫做访问者模式,不用在意背后的细节。
    • Identifier、ASTNodeTypeHere:AST的每个节点,都有对应的节点类型,比如标识符(Identifier)、函数声明(FunctionDeclaration)等,可以在visitor上声明同名的属性,当Babel遍历到相应类型的节点,属性对应的方法就会被调用,传入的参数就是path、state。

    极简插件实例

    在本例子中,我们实现一个毫无意义的插件:将所有名称为bad的标识符,转成good。完整代码在这里

    首先,安装项目依赖。

    npm init -f
    npm install --save-dev babel-cli
    

    接着,创建插件。判断标识符的名称是否是bad,如果是则替换成good。

    // plugin.js
    module.exports = function({ types: babelTypes }) {
      return {
        name: "deadly-simple-plugin-example",
        visitor: {
          Identifier(path, state) {
            if (path.node.name === 'bad') {
              path.node.name = 'good';
            }
          }
        }
      };
    };
    

    源码前的源代码:

    // index.js
    let bad = true;
    

    运行转码命令:

    npx babel --plugins ./plugin.js index.js
    

    输出转码结果:

    // index.js
    let good = true;
    

    插件配置

    插件可以有自己的配置项。我们修改前面的例子,看下在Babel插件中如何获取配置项。完整代码在这里

    首先,我们新建 .babelrc,传入配置项。

    {
      "plugins": [ ["./plugin", {
        "bad": "good",
        "dead": "alive"
      }] ]
    }
    

    然后,修改插件代码。我们从 state.opts 中获取到配置参数。

    // plugin.js
    module.exports = function({ types: babelTypes }) {
      return {
        name: "deadly-simple-plugin-example",
        visitor: {
          Identifier(path, state) {
            let name = path.node.name;
            if (state.opts[name]) {
              path.node.name = state.opts[name];
            }
          }
        }
      };
    };
    

    修改需要转换的代码:

    // index.js
    let bad = true;
    let dead = true;
    

    运行转码命令 npx babel index.js,转码结果如下:

    // index.js
    let good = true;
    let alive = true;
    

    复杂插件例子:替换process.env.NODE_ENV

    下面,来看一个稍微复杂一点但比较实用的例子:替换 process.env.NODE_ENV。示例完整代码可以在 这里找到,参考了这个插件

    在很多开源项目中,我们经常会看到类似下面的代码,对这些代码,需要在构建阶段进行处理,比如进行替换。

    // index.js
    if ( process.env.NODE_ENV === 'development' ) {
      console.log('我是程序猿小卡');
    }
    

    下面,我们创建一个叫做 node-env-replacer 的插件,代码如下,下面会对插件代码进行讲解。

    // plugin.js
    module.exports = function({ types: babelTypes }) {
      return {
        name: "node-env-replacer",
        visitor: {
    	  // 成员表达式
          MemberExpression(path, state) {
    	    // 如果 object 对应的节点匹配了模式 "process.env"
            if (path.get("object").matchesPattern("process.env")) {
    		  // 这里返回结果为字符串字面量类型的节点
              const key = path.toComputedKey();
              if ( babelTypes.isStringLiteral(key) ) {
    		    // path.replaceWith( newNode ) 用来替换当前节点
    			// babelTypes.valueToNode( value ) 用来创建节点,如果value是字符串,则返回字符串字面量类型的节点
                path.replaceWith(babelTypes.valueToNode(process.env[key.value]));
              }
            }
          }
        }
      };
    };
    

    插件代码讲解

    这次我们处理的是成员表达方式(MemberExpression)。对于MemberExpression,BabelType的定义如下:

    MemberExpression 主要是由 object、property、computed、optional 组成的。对于本例子来说,object 是 process.env 对应的节点,property 为 NODE_ENV 对应的节点。

    defineType("MemberExpression", {
      builder: ["object", "property", "computed", "optional"],
      visitor: ["object", "property"],
      // ...
    });
    

    前面提到,path对应了节点的属性,以及节点的关联关系。path.get("object") 获取到的就是 object(process.env)对应的 path实例。

    matchesPattern(pattern) 检查某个节点是否符合某种模式(pattern)。本例子中,path.get("object").matchesPattern("process.env") 检查 object 是否符合 "process.env" 这种模式。比如 成员表达式 process.env.NODE_ENV 为true,而成员表达式 process.hello.NODE_ENV 返回false。

     if (path.get("object").matchesPattern("process.env")) { }
    

    接着,通过 path.toComputedKey() 获取成员表达式的键(key),对于对于MemberExpression,返回的是类型为字符串字面量(stringLiteral)的节点。

    const key = path.toComputedKey();
    

    if ( babelTypes.isStringLiteral(key) ) 判断 key 是否为字符串字面量,如果是,则返回true。

    path.replaceWith( node ) 方法用来替换节点。babelTypes.valueToNode( value ) 用来创建节点,如果value是字符串,则返回字符串字面量类型的节点。

    path.replaceWith(babelTypes.valueToNode(process.env[key.value]));
    

    运行插件

    命令如下:

    npx babel --plugins ./plugin.js index.js
    

    转换结果:

    // index.js
    if ('development' === 'development') {
      console.log('我是程序猿小卡');
    }
    

    小结

    Babel的插件入门比较简单,照葫芦画瓢即可。在编写插件过程中,可能会遇到的主要障碍,包括对ECMA规范不了解、对Babel的API不了解。

    1. 对ECMA规范不了解:MemberExpression、FunctionDeclaration、Identifier等都是规范里的术语,如果对规范没有一定的了解,转换代码的时候就不知道如何入手。建议读者稍微了解下ECMA规范。
    2. 对Babel的API不了解:Babel相关API的文档比较少,这会对插件编写造成不小的困难,目前比较好的解决办法,就是参考现有的插件进行修改。

    总而言之,就是多看多写多查。

    这里再留个小问题,前面插件替换了 process.env.NODE_ENV,如果是下面代码该怎么替换?

    process.env['NODE_' + 'ENV'];
    

    相关链接

    babel-handbook

    ECMA-262/5.1

    Babel核心package的文档

    热心开发者贡献的文档

  • 相关阅读:
    RequireJS 模块化加载框架使用
    MySQL数据类型(最大值 和 最小值)
    utf-8的中文是一个字符占几个字节
    spring的事务是什么?与数据库的事务是否一样
    事务和锁机制是什么关系? 开启事务就自动加锁了吗?
    关于数据库主键和外键
    浅谈Mysql共享锁、排他锁、悲观锁、乐观锁及其使用场景
    TCP/IP协议
    分库分表分区需要考虑的问题及方案
    Redis(十一):Redis的事务功能详解
  • 原文地址:https://www.cnblogs.com/chyingp/p/how-to-write-a-babel-plugin.html
Copyright © 2020-2023  润新知