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轉成小程序的代碼也能夠從語法分析層面來進行。

拓展閱讀

實現一個簡單的編譯器

實現一個簡單的打包工具

相關文章
相關標籤/搜索