babel從入門到跑路

supot發表於2018-08-09

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轉成小程式的程式碼也可以從語法分析層面來進行。

擴充閱讀

實現一個簡單的編譯器

實現一個簡單的打包工具

相關文章