babel从入门到跑路

4,067 阅读11分钟

babel入门

一个简单的babel配置

babel的配置可以使用多种方式,常用的有.babelrc文件和在package.json里配置babel字段。

.babelrc

{
  "presets": [
    "env",
    "react",
    "stage-2",
  ],
  "plugins": [
    "transform-decorators-legacy",
    "transform-class-properties"
  ]
}
package.json

{
  ...
  "babel": {
    "presets": [
      "env",
      "react",
      "stage-2",
    ],
    "plugins": [
      "transform-decorators-legacy",
      "transform-class-properties"
    ]
  },
  ...
}

还可以使用.babelrc.js,需要用module.exports返回一个描述babel配置的对象,不太常用。

babel运行原理

babel的运行原理和一般的编译器是一样的,分为解析、转换和生成三个步骤,babel提供了一些的工具来进行这个编译过程。

babel核心工具

  • babylon -> babel-parser
  • babel-traverse
  • babel-types
  • babel-core
  • babel-generator

babylon

babylon是babel一开始使用的解析引擎,现在这个项目已经被babel-parser替代,依赖acorn和acorn-jsx。babel用来对代码进行词法分析和语法分析,并生成AST。

babel-traverse

babel-traverse用来对AST进行遍历,生成path,并且负责替换、移除和添加节点。

babel-types

babel-types是一babel的一个工具库,类似于Lodash。它包含了一系列用于节点的工具函数,包括节点构造函数、节点判断函数等。

babel-core

babel-core是babel的核心依赖包,包含了用于AST代码转换的方法。babel的plugins和presets就是在这里执行的。

import { transform, transformFromAst } from 'babel-core'
const {code, map, ast} = transform(code, {
  plugins: [
    pluginName
  ]
})

const {code, map, ast} = transformFromAst(ast, null, {
  plugins: [
    pluginName
  ]
});

transform接收字符串,transformFromAst接收AST。

babel-generator

babel-generator将AST转换为字符串。

babel编译流程

input: string
	↓
babylon parser (babel-parser)  //对string进行词法分析,最终生成AST
	↓
       AST
        ↓
babel-traverse  //根据presets和plugins对AST进行遍历和处理,生成新的AST	
	↓
      newAST
  	↓
babel-generator  //将AST转换成string,并输出
	↓
 output:string

编译程序

词法分析

词法分析(Lexical Analysis)阶段的任务是对构成源程序的字符串从左到右进行扫描和分析,根据语言的词法规则识别出一个个具有单独意义的单词,成为单词符号(Token)。

程序会维护一个符号表,用来记录保留字。词法分析阶段可以做一些词法方面的检查,比如变量是否符合规则,比如变量名中不能含有某些特殊字符。

语法分析

语法分析的任务是在词法分析的基础上,根据语言的语法规则,把Token序列分解成各类语法单位,并进行语法检查。通过语法分析,会生成一棵AST(abstract syntax tree)。

一般来说,将一种结构化语言的代码编译成另一种类似的结构化语言的代码包括以下几个步骤:

compile

  1. parse读取源程序,将代码解析成抽象语法树(AST)
  2. transform对AST进行遍历和替换,生成需要的AST
  3. generator将新的AST转化为目标代码

AST

辅助开发的网站:

function max(a) {
  if(a > 2){
    return a;
  }
}  

上面的代码经过词法分析后,会生一个token的数组,类似于下面的结构

[
  { type: 'reserved', value: 'function'},
  { type: 'string', value: 'max'},
  { type: 'paren',  value: '('},
  { type: 'string', value: 'a'},
  { type: 'paren',  value: ')'},
  { type: 'brace',  value: '{'},
  { type: 'reserved', value: 'if'},
  { type: 'paren',  value: '('},
  { type: 'string', value: 'a'},
  { type: 'reserved', value: '>'},
  { type: 'number', value: '2'},
  { type: 'paren',  value: ')'},
  { type: 'brace',  value: '{'},
  { type: 'reserved',  value: 'return'},
  { type: 'string',  value: 'a'},
  { type: 'brace',  value: '}'},
  { type: 'brace',  value: '}'},
]

将token列表进行语法分析,会输出一个AST,下面的结构会忽略一些属性,是一个简写的树形结构

{
  type: 'File',
    program: {
      type: 'Program',
        body: [
          {
            type: 'FunctionDeclaration',
            id: {
              type: 'Identifier',
              name: 'max'
            },
            params: [
              {
                type: 'Identifier',
                name: 'a',
              }
            ],
            body: {
              type: 'BlockStatement',
              body: [
                {
                  type: 'IfStatement',
                  test: {
                    type: 'BinaryExpression',
                    left: {
                      type: 'Identifier',
                      name: 'a'
                    },
                    operator: '>',
                    right: {
                      type: 'Literal',
                      value: '2',
                    }
                  },
                  consequent: {
                    type: 'BlockStatement',
                    body: [
                      {
                        type: 'ReturnStatement',
                        argument: [
                          {
                            type: 'Identifier',
                            name: 'a'
                          }
                        ]
                      }
                    ]
                  },
                  alternate: null
                }
              ]
            }
          }
        ]
    }
}

AST简化的树状结构如下

ast

编写babel插件

plugin和preset

plugin和preset共同组成了babel的插件系统,写法分别为

  • Babel-plugin-XXX
  • Babel-preset-XXX

preset和plugin在本质上同一种东西,preset是由plugin组成的,和一些plugin的集合。

他们两者的执行顺序有差别,preset是倒序执行的,plugin是顺序执行的,并且plugin的优先级会高于preset。

.babelrc

{
  "presets": [
    ["env", options],
    "react"
  ],
  "plugins": [
    "check-es2015-constants",
    "es2015-arrow-functions",
  ]
}

对于上面的配置项,会先执行plugins里面的插件,先执行check-es2015-constants再执行es2015-arrow-functions;再执行preset的设置,顺序是先执行react,再执行env。

使用visitor遍历AST

babel在遍历AST的时候使用深度优先去遍历整个语法树。对于遍历的每一个节点,都会有enter和exit这两个时机去对节点进行操作。

enter是在节点中包含的子节点还没有被解析的时候触发的,exit是在包含的子节点被解析完成的时候触发的,可以理解为进入节点和离开节点。

 进入  Program
 进入   FunctionDeclaration
 进入    Identifier (函数名max)
 离开    Identifier (函数名max)
 进入    Identifier (参数名a)
 离开    Identifier (参数名a)
 进入    BlockStatement
 进入     IfStatement
 进入      BinaryExpression
 进入       Identifier (变量a)
 离开       Identifier (变量a)
 进入       Literal (变量2)
 离开       Literal (变量2)
 离开      BinaryExpression
 离开     IfStatement
 进入     BlockStatement
 进入      ReturnStatement
 进入       Identifier (变量a)
 离开       Identifier (变量a)
 离开      ReturnStatement
 离开     BlockStatement
 离开    BlockStatement
 离开   FunctionDeclaration
 离开  Program

babel使用visitor去遍历AST,这个visitor是访问者模式,通过visitor去访问对象中的属性。

AST中的每个节点都有一个type字段来保存节点的类型,比如变量节点Identifier,函数节点FunctionDeclaration。

babel的插件需要返回一个visitor对象,用节点的type作为key,一个函数作为置。

const visitor = {
  Identifier: {
    enter(path, state) {

    },
    exit(path, state) {

    }
  }
}


//下面两种写法是等价的
const visitor = {
  Identifier(path, state) {

  }
}

↓ ↓ ↓ ↓ ↓ ↓

const visitor = {
  Identifier: {
    enter(path, state) {

    }
  }
}

babel的插件就是定义一个函数,这个函数会接收babel这个参数,babel中有types属性,用来对节点进行处理。

path

使用visitor来遍历语法树的时候,对特定的节点进行操作的时候,可能会修改节点的信息,所以还需要拿到节点的信息以及和其他节点的关系,visitor的执行函数会传入一个path参数,用来记录节点的信息。

path是表示两个节点之间连接的对象,并不是直接等同于节点,path对象上有很多属性和方法,常用的有以下几种。

属性
node: 当前的节点
parent: 当前节点的父节点
parentPath: 父节点的path对象

方法
get(): 获取子节点的路径
find(): 查找特定的路径,需要传一个callback,参数是nodePath,当callback返回真值时,将这个nodePath返回
findParent(): 查找特定的父路径
getSibling(): 获取兄弟路径
replaceWith(): 用单个AST节点替换单个节点
replaceWithMultiple(): 用多个AST节点替换单个节点
replaceWithSourceString(): 用字符串源码替换节点
insertBefore(): 在节点之前插入
insertAfter(): 在节点之后插入
remove(): 删除节点

一个简单的例子

实现对象解构

const { b, c } = a, { s } = w

↓ ↓ ↓ ↓ ↓ ↓

const b = a.b
const c = a.c
const s = w.s

简化的AST结构

{
  type: 'VariableDeclaration',
    declarations: [
      {
        type: 'VariableDeclarator',
        id: {
          type: 'ObjectPattern',
          Properties: [
            {
              type: 'Property',
              key: {
                type: 'Identifier',
                name: 'b'
              },
              value: {
                type: 'Identifier',
                name: 'b'
              }
            },
            {
              type: 'Property',
              key: {
                type: 'Identifier',
                name: 'c'
              },
              value: {
                type: 'Identifier',
                name: 'c'
              }
            }
          ]
        }
        init: {
          type: 'Identifier',
          name: 'a'
        }

      },

      ...
    ],
    kind: 'const'
}

用到的types

  • VariableDeclaration
  • variableDeclarator
  • objectPattern
  • memberExpression
VariableDeclaration:  //声明变量
t.variableDeclaration(kind, declarations)  //构造函数
kind: "var" | "let" | "const" (必填)
declarations: Array<VariableDeclarator> (必填)
t.isVariableDeclaration(node, opts)  //判断节点是否是VariableDeclaration

variableDeclarator:  //变量赋值语句
t.variableDeclarator(id, init)
id: LVal(必填)  //赋值语句左边的变量
init: Expression (默认为:null)   //赋值语句右边的表达式
t.isVariableDeclarator(node, opts)  //判断节点是否是variableDeclarator

objectPattern:  //对象
t.objectPattern(properties, typeAnnotation)
properties: Array<RestProperty | Property> (必填)
typeAnnotation (必填)
decorators: Array<Decorator> (默认为:null)
t.isObjectPattern(node, opts)  //判断节点是否是objectPattern

memberExpression: //成员表达式
t.memberExpression(object, property, computed)
object: Expression (必填)  //对象
property: if computed then Expression else Identifier (必填)  //属性
computed: boolean (默认为:false)
t.isMemberExpression(node, opts)  //判断节点是否是memberExpression

插件代码

module.exports = function({ types : t}) {

  function validateNodeHasObjectPattern(node) {  //判断变量声明中是否有对象
    return node.declarations.some(declaration => 				          														t.isObjectPattern(declaration.id));
  }

  function buildVariableDeclaration(property, init, kind) {  //生成一个变量声明语句
    return t.variableDeclaration(kind, [
      t.variableDeclarator(
        property.value,
        t.memberExpression(init, property.key)
      ),
    ]);

  }

  return {
    visitor: {
      VariableDeclaration(path) {
        const { node } = path; 
        const { kind } = node;
        if (!validateNodeHasObjectPattern(node)) {
          return ;
        }

        var outputNodes = [];

        node.declarations.forEach(declaration => {
          const { id, init } = declaration;

          if (t.isObjectPattern(id)) {

            id.properties.forEach(property => {
              outputNodes.push(
                buildVariableDeclaration(property, init, kind)
              );
            });

          }

        });

        path.replaceWithMultiple(outputNodes);

      },
    }
  };
}

简单实现模块的按需加载

import { clone, copy } from 'lodash';

↓ ↓ ↓ ↓ ↓ ↓

import clone from 'lodash/clone';
import 'lodash/clone/style';
import copy from 'lodash/copy';
import 'lodash/copy/style';


.babelrc:
{
  "plugins": [
    ["first", {
      "libraryName": "lodash",
      "style": "true"
    }]
  ]
}


plugin:
module.exports = function({ types : t}) {
  function buildImportDeclaration(specifier, source, specifierType) {
    const specifierList = [];

    if (specifier) {
      if (specifierType === 'default') {
        specifierList.push(
          t.importDefaultSpecifier(specifier.imported)
        );
      } else {
        specifierList.push(
          t.importSpecifier(specifier.imported)
        );
      }
    }

    return t.importDeclaration(
      specifierList,
      t.stringLiteral(source)
    );

  }

  return {
    visitor: {
      ImportDeclaration(path, { opts }) {  //opts为babelrc中传过来的参数
        const { libraryName = '', style = ''} = opts;
        if (!libraryName) {
          return ;
        }
        const { node } = path;
        const { source, specifiers } = node;

        if (source.value !== libraryName) {
          return ;
        }


        if (t.isImportDefaultSpecifier(specifiers[0])) {
          return ;
        }

        var outputNodes = [];

        specifiers.forEach(specifier => {
          outputNodes.push(
            buildImportDeclaration(specifier, libraryName + '/' + 															      specifier.imported.name, 'default')
          );

          if (style) {
            outputNodes.push(
              buildImportDeclaration(null, libraryName + '/' + 																		      specifier.imported.name + '/style')
            );
          }

        });

        path.replaceWithMultiple(outputNodes);

      }

    }
  };
}

插件选项

如果想对插件进行一些定制化的设置,可以通过plugin将选项传入,visitor会用state的opts属性来接收这些选项。

.babelrc
{
  plugins: [
    ['import', {
      "libraryName": "antd",
      "style": true,
    }]
  ]
}


visitor
visitor: {
  ImportDeclaration(path, state) {
    console.log(state.opts);
    // { libraryName: 'antd', style: true }
  }
}

插件的准备和收尾工作

插件可以具有在插件之前或之后运行的函数。它们可以用于设置或清理/分析目的。

export default function({ types: t }) {
  return {
    pre(state) {
      this.cache = new Map();
    },
    visitor: {
      StringLiteral(path) {
        this.cache.set(path.node.value, 1);
      }
    },
    post(state) {
      console.log(this.cache);
    }
  };
}

babel-polyfill和babel-runtime

babel的插件系统只能转义语法层面的代码,对于一些API,比如Promise、Set、Object.assign、Array.from等就无法转义了。babel-polyfill和babel-runtime就是为了解决API的兼容性而诞生的。

core-js 标准库

core-js标准库是zloirock/core-js,它提供了 ES5、ES6 的 polyfills,包括promises、setImmediate、iterators等,babel-runtime和babel-polyfill都会引入这个标准库

###regenerator-runtime

这是Facebook开源的一个库regenerator,用来实现 ES6/ES7 中 generators、yield、async 及 await 等相关的 polyfills。

babel-runtime

babel-runtime是babel提供的一个polyfill,它本身就是由core-js和regenerator-runtime组成的。

在使用时,需要手动的去加载需要的模块。比如想要使用promise,那么就需要在每一个使用promise的模块中去手动去引入对应的polyfill

const Promise = require('babel-runtime/core-js/promise');

babel-plugin-transform-runtime

从上面可以看出来,使用babel-runtime的时候,会有繁琐的手动引用模块,所以开发了这个插件。

在babel配置文件中加入这个plugin后,Babel 发现代码中使用到 Symbol、Promise、Map 等新类型时,自动且按需进行 polyfill。因为是按需引入,所以最后的polyfill的文件会变小。

babel-plugin-transform-runtime的沙盒机制

使用babel-plugin-transform-runtime不会污染全局变量,是因为插件有一个沙盒机制,虽然代码中的promise、Symbol等像是使用了全局的对象,但是在沙盒模式下,代码会被转义。

const sym = Symbol();
const promise = new Promise();
console.log(arr[Symbol.iterator]());

			↓ ↓ ↓ ↓ ↓ ↓

"use strict";
var _getIterator2 = require("babel-runtime/core-js/get-iterator");
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _promise = require("babel-runtime/core-js/promise");
var _promise2 = _interopRequireDefault(_promise);
var _symbol = require("babel-runtime/core-js/symbol");
var _symbol2 = _interopRequireDefault(_symbol);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var sym = (0, _symbol2.default)();
var promise = new _promise2.default();
console.log((0, _getIterator3.default)(arr));

从转义出的代码中可以看出,promise被替换成_promise2,并且没有被挂载到全局下面,避免了污染全局变量。

babel-polyfill

babel-polyfill也包含了core-js和regenerator-runtime,它的目的是模拟一整套ES6的运行环境,所以它会以全局变量的方式去polyfill promise、Map这些类型,也会以Array.prototype.includes()这种方式去污染原型对象。

babel-polyfill是一次性引入到代码中,所以开发的时候不会感知它的存在。如果浏览器原生支持promise,那么就会使用原生的模块。

babel-polyfill是一次性引入所有的模块,并且会污染全局变量,无法进行按需加载;babel-plugin-transform-runtime可以进行按需加载,并且不会污染全局的代码,也不会修改内建类的原型,这也造成babel-runtime无法polyfill原型上的扩展,比如Array.prototype.includes() 不会被 polyfill,Array.from() 则会被 polyfill。

所以官方推荐babel-polyfill在独立的业务开发中使用,即使全局和原型被污染也没有太大的影响;而babel-runtime适合用于第三方库的开发,不会污染全局。

未来,是否还需要babel

随着浏览器对新特性的支持,是否还需要babel对代码进行转义?

ECMAScript从ES5升级到ES6,用了6年的时间。从ES2015以后,新的语法和特性都会每年进行一次升级,比如ES2016、ES2017,不会再进行大版本的发布,所以想要使用一些新的实验性的语法还是需要babel进行转义。

不仅如此,babel已经成为新规范落地的一种工具了。ES规范的推进分为五个阶段

  • Stage 0 - Strawman(展示阶段)
  • Stage 1 - Proposal(征求意见阶段)
  • Stage 2 - Draft(草案阶段)
  • Stage 3 - Candidate(候选人阶段)
  • Stage 4 - Finished(定案阶段)

在Stage-2这个阶段,对于草案有两个要求,其中一个就是要求新的特性能够被babel等编译器转义。只要能被babel等编译器模拟,就可以满足Stage-2的要求,才能进入下一个阶段。

更关键的一点,babel把语法分析引入了前端领域,并且提供了一系列的配套工具,使得前端开发能够在更底层的阶段对代码进行控制。

打包工具parcel就是使用babylon来进行语法分析的;Facebook的重构工具jscodeshift也是基于babel来实现的;vue或者react转成小程序的代码也可以从语法分析层面来进行。

拓展阅读

实现一个简单的编译器

实现一个简单的打包工具