babel从入门到跑路

babel入门

一个简单的babel配置

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

.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配置的对象,不太经常使用。html

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。vue

babel-traverse

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

babel-types

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

babel-core

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

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。git

babel-generator

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

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)。json

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

语法分析

语法分析的任务是在词法分析的基础上,根据语言的语法规则,把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,它提供了 ES五、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之后,新的语法和特性都会每一年进行一次升级,好比ES201六、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转成小程序的代码也能够从语法分析层面来进行。

拓展阅读

实现一个简单的编译器

实现一个简单的打包工具

相关文章
相关标签/搜索