Babel简介

Babel是Javascript编译器,是种代码到代码的编译器,通常也叫做『转换编译器』。

Babel的工作过程

Babel的处理主要过程:解析(parse)、转换(transform)、生成(generate)。

  • 代码解析
    词法分析和语法分析构造AST。
  • 代码转换
    处理AST,处理工具、插件等就是在这个阶段进行代码转换,返回新的AST。
  • 代码生成
    遍历AST,输出代码字符串。

    所以我们需要对AST有一定了解才能进行Babel插件开发。

AST

在这整个过程中,都是围绕着抽象语法树(AST)来进行的。在Javascritp中,AST,简单来说,就是一个记录着代码语法结构的Object。感兴趣的同学可到https://astexplorer.net/ 去深入体验
比如下面的代码:

import {Button} from 'antd';import Card from 'antd/button/lib/index.js';

转换成AST后如下,

{  "type": "Program",  "start": 0,  "end": 253,  "body": [    {      "type": "ImportDeclaration",      "start": 179,      "end": 207,      "specifiers": [        {          "type": "ImportSpecifier",          "start": 187,          "end": 193,          "imported": {            "type": "Identifier",            "start": 187,            "end": 193,            "name": "Button"          },          "local": {            "type": "Identifier",            "start": 187,            "end": 193,            "name": "Button"          }        }      ],      "source": {        "type": "Literal",        "start": 200,        "end": 206,        "value": "antd",        "raw": "'antd'"      }    },    {      "type": "ImportDeclaration",      "start": 209,      "end": 253,      "specifiers": [        {          "type": "ImportDefaultSpecifier",          "start": 216,          "end": 220,          "local": {            "type": "Identifier",            "start": 216,            "end": 220,            "name": "Card"          }        }      ],      "source": {        "type": "Literal",        "start": 226,        "end": 252,        "value": "antd/button/lib/index.js",        "raw": "'antd/button/lib/index.js'"      }    }  ],  "sourceType": "module"}

插件开发思路

  • 确定我们需要处理的节点类型
  • 处理节点
  • 返回新的节点

简单插件结构

插件必须是一个函数,根据官方文档要求,形式如下:

module.exports = function ({ types: t }) {    return {        visitor: {            ImportDeclaration(path, source){                //todo            },            FunctionDeclaration(path, source){                //todo            },        }        }}

types来自@babel/types工具类,主要用途是在创建AST的过程中判断各种语法的类型和节点构造。

实现示例

很多同学用过 babel-plugin-import ,它帮助我们在使用一些JS类库是达到按需加载。其实,该插件帮助我们做了如下代码转换:

//fromimport {Button } from 'antd';//toimport Button from 'antd/es/button';import 'antd/es/button/style.css'; 

我们先看看两者的AST有何差别,以帮助我们对转换有个清晰的认识:

转换前:

 [{      "type": "ImportDeclaration",      "start": 6,      "end": 45,      "specifiers": [        {          "type": "ImportSpecifier",          "start": 14,          "end": 20,          "imported": {            "type": "Identifier",            "start": 14,            "end": 20,            "name": "Button"          },          "local": {            "type": "Identifier",            "start": 14,            "end": 20,            "name": "Button"          }        }      ],      "source": {        "type": "Literal",        "start": 28,        "end": 44,        "value": "antd/es/button",        "raw": "'antd/es/button'"      }    }]    

转换后:

 [{  "type": "ImportDeclaration",  "start": 5,  "end": 41,  "specifiers": [    {      "type": "ImportDefaultSpecifier",      "start": 12,      "end": 18,      "local": {        "type": "Identifier",        "start": 12,        "end": 18,        "name": "Button"      }    }  ],  "source": {    "type": "Literal",    "start": 24,    "end": 40,    "value": "antd/es/button",    "raw": "'antd/es/button'"  }},{  "type": "ImportDeclaration",  "start": 46,  "end": 76,  "specifiers": [],  "source": {    "type": "Literal",    "start": 53,    "end": 75,    "value": "antd/es/button/style",    "raw": "'antd/es/button/style'"  }}]

对比两棵树,我们应该有个大致的思路。在转换过程中,我们还需要一些参数,这些参数在配置文件(package.json或者.babelrc)中,提供了一些自定义配置,比如antd的按需加载:

["import",{libraryName:"antd",libraryDireactory:"es","style":"css"}]

现在我们开始尝试实现这个插件吧:

module.exports = function ({ types: t }) {return {    visitor: {        ImportDeclaration(path, source) {                        //取出参数            const { opts: { libraryName, libraryDirectory='lib', style="css" } } = source;            //拿到老的AST节点            let node = path.node            if(node.source.value !== libraryName){                return;            }            //创建一个数组存入新生成AST            let newImports = [];            //构造新节点            path.node.specifiers.forEach(item => {                newImports.push(t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${libraryName}/${libraryDirectory}/${item.local.name}`)));                newImports.push(t.importDeclaration([], t.stringLiteral(`${libraryName}/${libraryDirectory}/style.${style}`)))            });            //替换原节点            path.replaceWithMultiple(newImports);                        }    }}

}

现在,简单版本的@babel-plugin-import的babel插件我们已经完成了。
若感兴趣了解更多内容,babel插件中文开发文档提供了很多详细资料。