阅读 571

从0到1完成一个Babel插件

前言

社区里面有很多关于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],
});
复制代码

进阶例子

这里提供一个简化版的vuereact的示例,有兴趣的可以学习下,地址

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

关注下面的标签,发现更多相似文章
评论