从0到1完成一个Babel插件

  • Post author:
  • Post category:其他


前言

社区里面有很多关于

Babel

的文章,有些写的很好,我自己也受这些文章启发很大。但我发现一个问题就是,这类文章一进来就讲了很多babel底层的概念,说实话对基础不深的一些童鞋来说,看完之后理解起来还是有一定难度的,最重要的是看完了之后,自己并不知道如何去写一个

Babel

插件,因而这促使了

如何从0到1完成一个babel插件

这篇文章的编写,学习完本篇文章,期望是大家能对

Babel

有一个整体的认识,知道

Babel

是什么?

Babel

是如何运作的?并且自己能实现一个简单的

Babel

插件。

什么是Babel


Babel

是一个JavaScript编译器,意思就是说你为

Babel

提供一些代码,

Babel

做一些转换,给你返回一些新的代码。比如,我们常见的将ES5+的代码转换成ES5+之前的一些代码。

Babel的处理步骤

如图,Babel经过3个处理步骤,分别为

解析(parse)



转换(transform)



生成(generate)

解析

解析又经过

词法分析



语法分析

两个步骤,将输入的代码生成

抽象语法数(AST)

,AST可以理解为就是描述一段代码的节点树,看如下这个例子:

我们输入

const a = 1
复制代码

经过

解析(parse)

,生成如下结构的节点树(为了方便观看,去掉了一些表明节点位置信息的属性),详细的可以通过这个

工具

查看

{
	"type": "VariableDeclaration",
	"declarations": [
		{
			"type": "VariableDeclarator",
			"id": {
				"type": "Identifier",
				"name": "a"
			},
			"init": {
				"type": "Literal",
				"value": 1,
				"rawValue": 1,
				"raw": "1"
			}
		}
	],
	"kind": "const"
}

复制代码

每一个{“type”:””}包裹的内容都可以视为一个节点(Node)

转换

得到了AST抽象语法树,本质就是一个用来描述代码的节点树(Node),我们就可以通过

树形遍历

来遍历它,从而进行代码转换(对节点添加、更新及移除等操作),也就是

Babel插件

真正处理的地方

生成

经过转换之后的AST还是AST,所以我们还需要将AST生成字符串形式的代码

实战

Babel的基础知识还有很多,我觉得一开始了解这么多就够了,我们现在开始开发一个简单的Babel转换。

如前面所说Babel的3个步骤,

解析



转换



生成

,Babel都提供了对应的方法,分别如下:

  • @babel/parser 提供解析

    parse
  • @babel/traverse 提供转换

    traverse
  • @babel/generator 提供生成

    generate

我们要实现一个插件,将整个引入组件的代码

import { Select as MySelect, Pagination } from 'UI';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
复制代码

处理为如下按需处理的形式

import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
复制代码

第一:搭建一个开发环境

这里我使用了

codesandbox

在线编写的方式,

访问这里

,将需要的依赖包引进来。

const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
复制代码

其中,

@babel/types

是用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。

第二:解析代码

const code = `import { Select as MySelect, Pagination } from '';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
`;
const ast = parse(code);
复制代码

第三:转换代码

这步很关键,我们的转换处理都在这一步

traverse(ast, {
  ImportDeclaration(path) {
    // 获取原本组件名
    const source = path.node.source.value;
    // 获取Select as MySelect , Pagination两个节点
    const specifiers = path.node.specifiers;
    // import specifiers有3种形式,ImportSpecifier ,ImportNamespaceSpecifier,ImportDefaultSpecifier
    // 获取specifiers类型是否是 命名空间类型,类似 import * as UI from 'xxx-ui' 这种
    const isImportNamespaceSpecifier = t.isImportNamespaceSpecifier(
      specifiers[0]
    );
    // 获取specifiers类型是否是 默认导出类型,类似 import UI2 from 'xxx-ui' 这种
    const isImportDefaultSpecifier = t.isImportDefaultSpecifier(specifiers[0]);
    if (!isImportNamespaceSpecifier && !isImportDefaultSpecifier) {
      const declarations = specifiers.map(specifier => {
        // 缓存单个组件名
        let localName = specifier.local.name;
        // 拼接引入路径
        let newSource = `${source}/${localName}/${localName}.js`;
        // 构造新的ImportDeclaration节点
        return t.importDeclaration(
          [t.importDefaultSpecifier(specifier.local)],
          t.stringLiteral(newSource)
        );
      });
      // 将构造好的新AST替换原来的AST
      path.replaceWithMultiple(declarations);
    }
  }
});
复制代码


traverse

方法第二个参数传入的就是我们对具体节点遍历的处理方法,这里有个概念需要明确的是,当我们以访问者身份遍历节点的时候,我们其实访问的是路径path,而非具体某个节点,所以示例中我们我们有2个

ImportDeclaration

节点,但我们只写了一个处理方法,因为这里这个方法会被执行2次。

第四:生成

最后,我们需要将转换后的AST重新生成代码

let newCode = generate(ast).code;
console.log(newCode);
复制代码

第五:运行

终端输入

node ./src/index.js
复制代码

可以看到最终我们生成的代码

import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';

import * as UI from 'xxx-ui';
复制代码

实际项目引用

我们完成了一个babel插件,那在项目中如何引入呢?其实,上述所述的步骤代码只是从内部剖析了下Babel插件的处理原理,真正我们在项目中只需要对外暴露一个方法,里面返回一个包含

visitor

属性的对象。

visitor访问者是一个对象,定义了一系列访问树形结构中节点的方法

// myPlugin.js
const babel = require(@babel/core');
const t = require('@babel/types');
export default function() {
  return {
    visitor: {
      ImportDeclaration(path, state) {
        //转换逻辑
      },
    }
  };
};
复制代码

然后在

babel-loader

的plugin引入

options:{
    plugins:[
        ["myPlugin"]
    ]
}
复制代码

原理是啥呢?是因为通过

babel-loader

引入,Babel里面core模块提供了

transform

方法,具体APi可以查看

这里

,只需要传入

visitor

对象,该方法据此默认会去做

解析



转换



生成

工作,内部处理逻辑如下:

const visitor = require('visitor.js');
const babel = require('@babel/core');
const result = babel.transform(code, {
	plugins: [visitor],
});
复制代码

进阶例子

这里提供一个简化版的

vue



react

的示例,有兴趣的可以学习下,

地址

最后,如果你对多端开发有兴趣,我们微店有个小组,从事多端统一开发研究,有兴趣的童鞋也可以加入进来看看,里面有很多Babel相关实例和文章,

访问地址