探究Babel生態

作者:百度外賣FE 張從鳳

摘要

本文從babel的基礎知識,使用方法,如何配置,解析原理和如何開發babel插件五個方面讓你了解bablel,以便用babel提升開發效率。

使用方法和如何配置章節將告訴你在項目中如何使用babel,解析原理和如何開發babel插件章節幫助你開發自己的babel插件。

基本介紹

相信用es6/7寫代碼的同學對babel應該不會陌生,Babel是用於編寫下一代JavaScript的編譯器。JavaScript作為一門語言,不斷發展更新,新的特性層出不窮,想要提前使用這些特性,必須藉助像babel這樣的編譯器。目前主流瀏覽器最新版本都基本實現了對es5/6語法的完全支持,具體請參考兼容性表格。

當你需要使用es7+語法特性或者兼容舊版本瀏覽器的需求時,你就需要一款編譯器支持將ES6/7編譯成瀏覽器支持的ES5/ES3代碼。Babel是使用最多的一個。除了編譯器,babel還支持語法擴展(如react的JSX語法,React現在已經依賴babel編譯它的JSX語法且棄用了它原有的自定義工具)和靜態語法分析(語法檢查,代碼高亮,壓縮,統一代碼風格等)。

使用方法

本文提到的所有配置的都是babel6,babel6廢棄了babel包,取而代之的是各種模塊包,babel6把所有轉換器都分離出去以插件的形式存在,需要單獨安裝插件包。意味著默認情況下babel不會對源碼做任何轉換,需要自己配置。

  1. 集成到工具中

    Webpack

    ① 安裝:

    npm install –save-dev babel-loader babel-coren

    ② webpack配置:

    module: {n rules: [n { test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }n ]n}n

    ③ 配置.babelrc文件

    安裝插件:

    npm install babel-preset-es2015 babel-plugin-transform-runtimen

    .babelrc

    {n "presets": [es2015],n "plugins": [transform-runtime]n }n

    fis3:

    ① 安裝:

    npm install –save-dev babel-core fis-parser-babel-6.xn

    ② 配置fis-conf文件:

    fis.match(src/**.js, {n isMod: true,n preprocessor: fis.plugin(js-require-css),n parser: fis.plugin(babel-6.x, {n sourceMaps: true,n }),n rExt: .jsn});n

    ③ 創建.babelrc文件同上

    其他工具請參見安裝組合方式。

  2. 命令行下使用babel命令編譯

    安裝

    npm install -g babel-cli or npm install --save-dev babel-clin

    若全局安裝,在命令行直接使用babel編譯,若項目目錄下本地安裝,將babel命令寫進npm scripts使用

    babel example.js -o compiled.js //編譯單獨文件nbabel src -d lib//編譯整個目錄n

  3. babel-register,babel-node

    使用babel-register需要創建register.js文件,在文件中引入babel-register和需要編譯的所有文件,

    //register.js nrequire("babel-register");nrequire("./index.js");n// clinnode register.js // 啟動編譯n

    or

    npm install babel-node babel-cli //使用babel-nodenbabel-node index.js //啟動編譯 n

    這種方式不適合正式產品環境引用,因為直接部署本地編譯完的代碼肯定比在線上編譯要好。但這種方式用在構建腳本或是其他本地運行的腳本中很合適。

  4. 以編程的方式使用babel,安裝babel-core

    babel.transform("code();", options);n

    這種方式適合用在開發babel插件的測試腳本中,後面插件開發的時候會提到。

Babel配置

預設(presets)

你不需要在配置文件中指定和維護大量的轉換器信息,你可以在babel6中預設插件,可以將一組類似的插件或所有你需要的插件打包組合使用:

  • babel-preset-es2015:ES6語法編譯成ES5

  • babel-preset-react:剝離所有流類型的注釋和聲明,並將JSX語法轉換為createElement調用

  • babel-preset-stage-x:支持尚未被發布為JavaScript標準的特性

    • stage-0:展示階段,最初的想法,離成為標準必須經歷以下3個階段
    • stage-1:提議階段,徵求意見
    • stage-2:草案階段,完成初步的篩選
    • stage-3:候選,有完整的規範和初始瀏覽器的實現,將會發布在下一版的標準里

可以創建自己的預設,根據項目需求定製插件組合。參考preset-es2015的實現,注意把所有插件的option暴露出來。

點擊插件查看具體包含哪些語法特性。

插件(plugins)

插件推薦:

  • babel-plugin-transform-runtime: 對於JavaScript新增的API和一些全局對象上的方法,在運行時動態插入兼容補丁

  • babel-plugin-transform-regenerator: 編譯Generator

  • babel-plugin-react-transform:react開發輔助插件

需要任何插件請瀏覽所有官方插件及豐富的社區插件。

.babelrc示例:

{n "presets": ["es2015", "react", "stage-0"],n // 區分生產環境還是開發環境:BABEL_ENV || NODE_ENV || "development"n "env": {n "production": {n "plugins": ["add-module-exports", "transform-decorators-legacy", "transform-runtime"]n },n "development": {n "plugins": [n ["react-transform", {n "transforms": [{n "transform": "react-transform-hmr",n "imports": ["react"],n "locals": ["module"]n }]n }]n ]n }n }n}n

點擊options瀏覽babel的所有配置項。

Babel編譯過程

首先必須了解幾個基礎概念。

抽象語法樹

abstract syntax tree ,縮寫AST,是源代碼的抽象語法結構的樹狀表現形式。 樹上的每個節點都表示源代碼中的一種結構。

AST Explorer 可以幫你直觀的認識AST的結構,為了跟babel保持一致,請確保選擇的解析器是babylon6。

看看下面的代碼解析成AST後的樣子

let i= 0;ni+1;n

AST

program{n type:"Program"n -body:[n -VariableDeclaration {n type: "VariableDeclaration"n -declarations: [n -VariableDeclarator{n type: "VariableDeclarator"n -id:Identifier{n type: "Identifier"n name: in }n -init:NumericLiteral{n type:"NumericLiteral",n value:0n }n }n ]n kind: "let"n }n-ExpressionStatement {n type:"ExpressionStatement"n -expression: BinaryExpression {n type:"BinaryExpression"n -left:Identifier{n type: "Identifier"n name: in }n operator:"+"n -right:NumericLiteral{n type: "NumericLiteral"n name: 1n }n }n }n ]n}n

為了更加直觀,根據AST的結構得到類似下圖的結構圖

可以看到AST就是由一個個節點構成,每個節點都是源代碼語法的一個標籤,都有類似的結構。

babylon

babylon是babel的解析器,負責將字元串形式的代碼轉換成AST。

babylon.parse(code);//接收字元串形式的code做參數,返回一個ASTn

babel-traverse

遍歷AST,負責添加,更新,移除節點。

traverse(ast,visitor);//ast和visitor函數(遍歷到相應節點時觸發)作為參數,返回更新後的ASTn

babel-types

是一個用於AST節點的工具庫,包含構造、驗證、變換 AST 節點的方法。

t.binaryExpression("*", t.identifier("a"), t.identifier("b"));//構造節點 構造一個二進位表達式 a * bn// 生成的ast結構n{n type: "BinaryExpression",n operator: "*",n left: {n type: "Identifier",n name: "a"n },n right: {n type: "Identifier",n name: "b"n }n}n

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });//驗證節點n

babel-generator

轉換AST,生成源碼和代碼映射。

generate(ast, {}, code);n

帶著這些基礎看看babel的編譯過程,Babel對源代碼的處理經歷3個步驟:

  1. 解析(parse):把字元串形式的代碼轉換成AST
  2. 轉換(transform):插件就是在這一階段介入,對AST的節點進行添加/更新/刪除操作,返回新的AST

    細節:babel-traverse對AST進行遞歸的樹形遍歷(如下圖),當你訪問每一個節點的時候,都會調用事先創建的visitor方法,對AST節點的所有操作都寫在visitor方法里,在進入/退出這個節點時可添加對節點的修改。

    const visitor1 = {n Identifier(path) {// 默認是enter,訪問Identifier節點時都會調用這個方法n console.log("訪問 Identifier");n }n};nnconst visitor2 = {n BinaryExpression: {n enter() {// 進入節點n console.log("Entered!");n },n exit() {//退出節點n console.log("Exited!");n }n }n};n

    參數 path表示該節點的路徑的對象,包含了這個節點的路徑信息,也是跟其他節點連接的橋樑,path對象:

    {n "parent": {...},n "node": {...},n "hub": {...},n "contexts": [],n "data": {},n "shouldSkip": false,n "shouldStop": false,n "removed": false,n "state": null,n "opts": null,n "skipKeys": null,n "parentPath": null,n "context": null,n "container": null,n "listKey": null,n "inList": false,n "parentKey": null,n "key": null,n "scope": null,n "type": null,n "typeAnnotation": nulln}n

  3. 生成(generate):深度優先遍歷整個AST,構建轉換後的字元串形式的代碼,並創建源碼映射

開發Babel插件

插件的工作在轉換階段,只需要操作AST,得到新的AST。通過一個插件示例看看babel插件具體的開發過程。

背景:通常組件庫會是在一個文件中把所有組件都export出來。import { Show } from fivesix; 這種引入組件的方式會載入fivesix的所有組件,想要按需載入,必須修改組件的引入路徑:

import Show from fivesix/lib/basic/Show; 由此設計一個自動修改引入路徑的插件。

要操作AST節點,首先看看這兩段代碼經過babel解析之後的AST對比:

// import { Show } from fivesix;nImportDeclaration {n type:"ImportDeclaration"n -specifiers:[n -ImportSpecifier {n type:"ImportSpecifier"n -imported:Identifier {n type:"Identifier"n name:"Show"n }n -local:Identifier {n type:"Identifier"n name:"Show"n }n }n ]n -source:StringLiteral {n type:"StringLiteral"n value:"fivesix"n }n}n

// import Show from fivesix/lib/basic/Show;nImportDeclaration {n type:"ImportDeclaration"n -specifiers:[n -ImportDefaultSpecifier {n type:"ImportDefaultSpecifier"n -local:Identifier {n type:"Identifier"n name:"Show"n }n }n ]n -source:StringLiteral {n type:"StringLiteral"n value:"fivesix/lib/basic/Show"n }n}n

直觀的對比圖

可以清晰的看到兩個語法樹之間要修改的部分,需要替換ImportSpecifier 節點為ImportDefaultSpecifier 節點,並修改source屬性的value值。給visitor添加ImportDeclaration方法:

// plugin.jsnmodule.exports = function({types: t}) {n return {n visitor: {n ImportDeclaration: function(path) {n if (path.node.source.value !== "fivesix") return;n const name = path.node.specifiers[0].local.name;n path.node.source.value = "fivesix/lib/basic/" + name;n path.node.specifiers = [t.ImportDefaultSpecifier(t.identifier(name))];n }n }n };n};n

Babel遍歷AST時,會遍歷所有節點,如果插件有相應的visitor方法,會調用該方法,並傳入path(上下文),state(狀態)。寫一個測試腳本驗證插件是否能夠完成預期的工作,是否將import 方式引入fivesix組件的地方都改了:

// test.jsnvar fs = require(fs);nvar babel = require(babel-core);nvar plugin = require(./plugin);nnvar fileName = process.argv[2];// 命令行里讀取文件名nfs.readFile(fileName, function(err, data) {n if(err) throw err;n var src = data.toString();//將源代碼轉成字元串n // 利用寫好的babel插件更新astn var out = babel.transform(src, {n plugins: [plugin]n });n console.log(out.code);n});n

寫好測試用例

// case.msnimport { Show2 } from fivesix2;nimport { Show } from fivesix;n

結果:

當引入多個組件的時候,AST的結構會變得稍微複雜:

/*多個組件同時引入*/nProgram {n body: [n ImportDeclaration {n type:"ImportDeclaration"n -specifiers:[n +ImportSpecifiern +ImportSpecifiern ]n -source:StringLiteral {n type:"StringLiteral"n value:"fivesix"n }n }n ]n}n// 轉換後astnProgram {n body: [n +ImportDeclarationn +ImportDeclarationn ]n }n

需要給body增加多個ImportDeclaration節點,替換原來的ImportDeclaration節點

// plugin.jsnmodule.exports = function({types: t}) {n return {n visitor: {n ImportDeclaration: function(path) {n if (path.node.source.value !== "fivesix") return;n var components = [];n path.node.specifiers.forEach((val)=>{n components.push(val.local.name);n })n path.node.specifiers = [t.ImportDefaultSpecifier(t.identifier(components[0]))];n path.node.source.value = "fivesix/lib/basic/" + components[0];n components.forEach((val,inx)=>{n if (inx > 0) {n path.parent.body.push(t.ImportDeclaration(n [t.ImportDefaultSpecifier(t.identifier(val))],n t.StringLiteral("fivesix/lib/basic/" + val)n ));n }n });n }n }n };n};n

修改測試用例

// case.msnimport { Show2 } from fivesix2;nimport { Show } from fivesix;nimport { Show3,show4,show5 } from fivesix;n

測試結果:

參考文獻

babel官網:Babel · The compiler for writing next generation JavaScript

babel手冊:thejameskyle/babel-handbook

babel插件:Plugins · Babel


推薦閱讀:

重新設計 React 組件庫
10min手寫(b三):b窮逼前端趕製的聖誕節禮物
React Native 開源一周年回顧

TAG:Babel | 前端工程师 | 前端开发 |