理解babel的基本原理和使用方法


  babel是一個編譯器,用於將ECMA2015+代碼轉換為向后兼容的javascript語法,其原因在於目前瀏覽器並不能及時的兼容js的新語法,而開發過程中我們往往會選擇es6、jsx、typescript進行開發,而瀏覽器並不能識別並執行這些代碼,因此就必須將這些代碼編譯並轉換成瀏覽器識別的代碼,所以我們才會發現所有的項目構建工具都是使用babel,這就顯示出來babel的重要性。雖然經常使用,但是每次使用都是使用固定的配置代碼,卻沒有了解其執行原理。知其然就需要知其所以然,所以記下自己對於babel的理解。

一、babel的執行過程

  想要了解一個東西就需要先從宏觀上分析它,babel也不例外。我們知道對於一個計算機語言,我們敲出來的代碼都是字符串,想要執行就必須經過編譯。比如react、vue等框架也需要對其類html代碼進行編譯才能生成viertual dom。既然babel需要將高級語法轉換,那么babel也勢必需要進行編譯,而babel的執行過程就是一個編譯轉換的工程。如下圖是babel的執行過程:

   由此我們可以看到babel的核心就是parse、transform和generator三個部分。

  接下來我們采用babel中的插件演示一個最簡單的例子。

var {parse} = require('@babel/parser');
var {default: generate} = require('@babel/generator');
var code = `const name = "jyy";`; // 原始代碼
var ast = parse(code);  // 源代碼生成的ast
var targetCode = generate(ast); // 將ast轉成目標代碼
console.log(targetCode);    //{ code: 'const name = "jyy";', map: null, rawMappings: undefined }

  parse和generate顧名思義就是編譯器和生成器,你可能會發現缺少轉換過程,而且生成的目標代碼和原始代碼都是一樣的,只是多了屬性而已,這相當於什么都沒有做啊。那是因為轉換過程的具體操作需要插件來實現,如果沒有使用插件,最后生成的目標代碼是和原始代碼一樣的。

  1.parse

  在babel中編譯器插件是@babel/parser,其作用就是將源碼轉換為AST,使用前需要npm install @babel/parser,使用方法如下:

const babelParser = require("@babel/parser");
const code = "const name= 'jyy';";
const ast = babelParser.parse(code);
console.log(ast);

  執行后輸出的結果如下:

 1 Node {
 2   type: 'File',
 3   start: 0,
 4   end: 12,
 5   loc:
 6    SourceLocation {
 7      start: Position { line: 1, column: 0 },
 8      end: Position { line: 1, column: 12 } },
 9   errors: [],
10   program:
11    Node {
12      type: 'Program',
13      start: 0,
14      end: 12,
15      loc: SourceLocation { start: [Position], end: [Position] },
16      sourceType: 'script',
17      interpreter: null,
18  body: [ [Node] ],
19      directives: [] },
20   comments: [] }

  這就是babel生成的ast結構,你可能會發現這個ast和平常我們看到的ast好像結構不同,因為沒有發現“name"、“=”等詞法單元。是的,我一開始看到的時候也在懷疑這個輸出到底是不是ast。后來發現babel的parser是根據babel的AST結構生成的,他基於ESTree規范。於是深入輸出發現了code的詞法單元的位置,就在上面代碼的標紅處,可以使用ast.program.body[0],得到如下的輸出:

  從結果中我們看到了declarations(聲明)關鍵字和我們源碼中的const,接着繼續輸出declarations,結果如下:

 

   可以看到這個結果確實將我們的源碼構造成了一個AST。

  2.transform

  這個過程雖然在本文中名字叫transfom,但是事實上babel官網中並沒有這個詞,更沒有稱為轉換器的結構。想要知道為什么沒有,我們需要知道bable是一個工具鏈,所謂工具鏈就是babel是依賴於它的插件的,只有有了插件babel才能發揮出真正的作用,沒有插件的babel只是會將源碼生成AST,然后在通過生成器生成和原來的源碼一摸一樣的代碼,這樣的過程是沒有任何作用的。插件發揮作用的地方基本都是在tranfrom這個過程,當源碼通過parse生成了ast后,我們可以通過轉換插件,對ast進行操作。比如@babel/plugin-transform-react-jsx是將react中的jsx轉換為react的節點對象。這樣這些插件都涉及到對ast的操作,babel提供了一些工具插件,讓我們可以方便的操作ast節點,也就更方便我們開發適合自己項目的插件。比如在babel官網中設計到的插件,點這里。下面介紹兩個比較重要的插件,同時用這兩個實現一個比較簡單的操作ast過程。

  @babel/types

  這個插件的api非常多,見這里,我也沒有實際用過,在這里只是簡單的介紹下了。它的作用是創建、修改、刪除、查找ast節點,因為ast也是一個樹狀結構,我們可以像js操作dom節點一樣,使用types對ast進行操作。

  另外我們知道ast的節點也是分為多種類型,比如ExpressionStatement是表達式、ClassDeclaration是類聲明、VariableDeclaration是變量聲明等等,同樣的這些類型都對應了其創建方法:t.expressionStatement、t.classDeclaration、t.variableDeclaration,也對應了判斷方法:t.isExpressionStatement、t.isClassDeclaration、t.isVariableDeclaration。這個插件往往和traverse遍歷插件一起使用,因為types只能對單一節點進行操作,一般是在對節點的迭代中使用,所以這個插件的例子會放在traverse的實例中。

  @babel/traverse

  這個插件的作用是對ast進行遍歷parse,在迭代的過程中可以定義回調函數,回調函數的參數提供了豐富的增、刪、改、查以及類型斷言的方法,比如replaceWith/remove/find/isMemberExpression。下面以一個例子結合types和traverse進行演示。

  假設我們在開發過程中使用一個函數findEleById來代替document.getElementById,如果我們直接使用findEle而不對其進行處理,js代碼執行過程中是會報錯,因為window下是沒有這個函數的。但是我們可以使用babel修改其ast,將findEleById改為document.getElementById,這樣babel的生成器生成的最新代碼就是document.getElementById,然后js引擎就可以編譯通過了。當然這個過程對於開發者是隱藏的,開發者只需要關注於使用findEleById便捷的開發就可以了,后續的操作交給babel。見如下代碼:

var t = require('@babel/types');
var {parse} = require('@babel/parser');
var {default: traverse} = require('@babel/traverse');
var {default: generate} = require('@babel/generator');

var orginCode = `findEleById("jyy")`;   // 原始代碼
// 生成原始AST
var originAST = parse(orginCode, {
    sourceType: "module"
});
// 對AST進行遍歷並操作
traverse(originAST,{
    Identifier(path){
        var {node} = path;
        // 找到findEleById,將其替換成為目標節點
        if(node && node.name === "findEleById"){
            var newNode = t.memberExpression(t.identifier("document"), t.identifier("getElementById")); // 創建目標節點
            path.replaceWith(newNode);  // 替換原始節點
            path.stop();
        }
    }
});
const targetCode = generate(originAST, { /* options */ }, orginCode);   // 將轉換后的AST生成目標代碼
console.log(targetCode);    // { code: 'document.getElementById("jyy");',map: null,rawMappings: undefined }

  從上面代碼可以看到基本的轉換過程,生成的最終代碼可以直接交付給瀏覽器引擎編譯執行了。

  在babel的工具插件還有一些,因為本文不是為了講解如何開發babel插件,所以這里僅介紹以上兩個插件只為介紹babel在transform階段的基本的工作原理。如果你真的需要開發自己的babel插件,那么需要了解babel提供的插件們,並了解其api的使用。

  3.generator

  這個過程已經在上面的實例中有所展現,使用的插件是@babel/generator,作用就是將轉換好的ast重新生成代碼。這樣的代碼就就可以安全的在瀏覽器運行。

  4.babel-core——整合基本插件

  我們發現基本的babel插件如@babel/parse、@babel/generator都是提供了代碼轉換的基本功能,而另外的一些工具類型的插件比如@babel/types、@babel/traverser起作用是提供操作ast節點的功能。然而在開發插件的過程中如果每個都需要去引入實在太麻煩,所以就有了@babel/core插件,顧名思義就是核心插件,他將底層的插件進行封裝,並另外加入了其他功能,比如讀取、分析配置文件,這個后面會在配置中講到。而這個插件將復雜的過程進行簡化,如下代碼所示:

 

1 var babel = require("@babel/core");
2 var code = "<div class='c'>jyy</div>";  // 代碼
3 babel.transform(code,{plugins: ["@babel/plugin-transform-react-jsx"],},function(err, result){
4     console.log(result.code);   
5     // React.createElement("div", {
6     //   class: "c"
7     // }, "jyy");
8 });

 

  可以發現,我們可以使用transform就可以完成整個步驟。另外我們查看core的依賴可以發現,它依賴於底層的插件並基於次進行進行封裝:

 

 

   core的api很多,可以查看babel官網,另外我們看transform函數的參數第一個為原始代碼,第二個為用於在轉換過程中對ast進行操作的插件,例子中我們使用的是轉換jsx的插件,第三個參數是一個回調函數。

 

二、插件

  我們一般不會自己開發babel的轉換插件,實際項目中往往都是直接使用現成的插件。而配置插件卻時常讓人很煩悶,因為有各種插件,什么stage-1、env、es2015等等,各種插件的各種配合設置給人摸不到頭腦的感覺,會想問為什么不能出一個統一的插件,里面包含所有的轉換功能,這樣在配置的時候只需要在plugins里面放一個插件名就好了呢?這個問題主要是有以下幾點原因:

  一是因為js發展太快了,我們知道這幾年js新的語法和函數不斷地出現,如果把所有對最新語法的轉換放在一個插件中,那么每次出現新的語法就需要不斷的修改代碼

  二是babel不僅僅包含對ecma2015+的轉換,還包括ts、flow和上面我們提到的jsx的轉換,全都揉在一起的話實在是太大太亂了,不易於維護

  三是這樣做可以讓用戶更自由地去選擇,就像菜市場去買菜,我只會去買我想要買的菜。這無疑減少了項目打包時的負擔,進而影響到所占用的網絡的帶寬。事實上這種方式廣泛的存在於產品中。比如echarts,我們可以選擇其中的某些圖表,然后下載對應的代碼,而這樣做的前提就是降低模塊之間的耦合。

  以上的分析都是我在瞎扯淡,僅供參考,誰知道babel的開發者是怎么想的。但至少是本人的一個見解。

  一般情況下,項目不需要我們去開發babel插件,因為這些插件都寫好了,看官網。里面的插件非常多,而且有些插件貌似很小,僅僅具體到一個語法,比如arrow-function,這是ES2015中的箭頭函數語法,下面使用這個插件進行演示:

var babel = require("@babel/core");
var code = `num => {
   return num ** 2;
}`;  // 代碼
babel.transform(code,{plugins: ["@babel/plugin-transform-arrow-functions"]},function(err, result){
    console.log(result.code);
    // (function (num) {
    //     return num ** 2;
    // });
});

  結果如我們預期,使用arrow-function插件后,確實將箭頭函數轉換為了es5的語法。但是我們還會發現代碼中的**,冪等運算符是ES2016的新特性。這樣的代碼還是不能被es5識別,於是我們加入轉換冪等運算符的插件:

var babel = require("@babel/core");
var code = `num => {
   return num ** 2;
}`;  // 代碼
babel.transform(code,{plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-exponentiation-operator"]},function(err, result){
    console.log(result.code);
    // (function (num) {
    //     return Math.pow(num, 2);
    // });
});

  發現結果已經將冪等運算符進行了轉換。對於插件的配置需要記住以下幾點:

  1.plugin的段名稱

  配置plugin的時候,可以設置插件的短名稱,可以將省略babel-plugin,例如:

  ["@babel/babel-plugin-name"]和["@babel/name"]是等價的

  2.排列順序

  多個插件的執行順序是按照從前到后的順序執行,例如["@babel/name1","@babel/name2"]兩個插件的執行順序是先執行name1,然后執行name2。

 

三、預設(preset)——babel的插件套裝

  那么問題來了新語法新特性那么多,難道我們要挨個去加嗎?當然不是,babel已經預設了幾套插件,將最新的語法進行轉換,可以使用在不同的環境中,如下:

  @babel/preset-env
  @babel/preset-flow
  @babel/preset-react
  @babel/preset-typescript

  從名字上就能看出他們使用的環境了,需要注意的是env,他的作用是將最新js轉換為es6代碼。預設是babel插件的組合,我們可以看下package.json(截取一部分):

 

 

   由此看到他組合了很多的插件,是一個官方提供的,這樣我們只需要使用一個插件就可以了。那么有了這個插件,我們使用上一個例子,來測試一下:

var babel = require("@babel/core");
var code = `num => {
   const offset = 23;
   return num ** 2 + offset;
}`;  // 代碼
babel.transform(code,{presets: ["@babel/preset-env"]},function(err, result){
    console.log(result.code);
    // "use strict";
    // (function (num) {
    //     var offset = 23;
    //     return Math.pow(num, 2) + offset;
    // });
});

  可以看到,代碼中額外將const轉為var,還加上了use strict

  需要注意的是因為@babel/preset-env是預設的包含多個插件,所以不同於單一的插件,需要使用presets參數,如代碼紅色標記所示。

  對於env插件,我們還需要知道他是以前es2015、es2016和es2017的集合,另外他默認不支持stage-x插件

  stage-x(babel7已廢棄)

  那么什么是stage-x呢?state-x里面包含了當年最新規范的草案,每年更新。因為有可能項目所使用的是最新的語法,那么官方的預設插件還沒有將其納入,這時候就需要使用state-x。如下是state-x的階段:

  • Stage 0 - 稻草人: 只是一個想法,經過 TC39 成員提出即可。
  • Stage 1 - 提案: 初步嘗試。
  • Stage 2 - 初稿: 完成初步規范。
  • Stage 3 - 候選: 完成規范和瀏覽器初步實現。
  • Stage 4 - 完成: 將被添加到下一年度發布。

  所以我們經常會在代碼中看到這樣的preset配置:[es2015, react, stage-0]。好在在babel7,stage-x以被廢棄,詳情點這里

  額外注意的

  1.preset可以設置短名稱 

  和插件一樣preset也可以設置段名稱。可以省略preset,例如:

{
    presets: ["@babel/preset-env", "@babel/preset-react"] 
}

  可以省略為:

{
    presets: ["@babel/env", "@babel/react"] 
}

  2.排列順序

  預設的執行順序也同樣重要,preset在plugin之前執行,而且和plugin不同的是,preset是從后往前執行,比如我們使用react,那么應該這么寫:

{
    presets: ["@babel/env", "@babel/react"]
}

  因為我們需要將react中的jsx轉為js,然后將js在轉換為es5,所以需要將react的插件放在后面,讓他先執行。

四、配置

   實際項目中我們不會親自動手去調用babel的api去轉換代碼,而且如果我們整個項目很可能都是用es6編寫,不可能手動調用babel的api去一個一個轉換,我們希望使用命令行,通過傳遞文件夾的名稱去交給babel轉換。這個時候babel-cli就出現了,比如,我們想要轉換某個文件夾下的文件,那么我們可以在控制台輸入這樣的命令:

babel src --out-dir lib --presets=@babel/preset-env,@babel/react

  這段命令的意思是,將src文件夾下的所有文件使用env和react預設進行轉換,並且將轉換后的文件存放在lib文件夾下。這樣就節省了我們很多的時間。需要注意的是:babel-cli只是一套命令,想要執行babel的轉換工作,仍然需要引入babel-core。

  此時仍然有個問題預設也是有參數的,另外還有plugins等等,當然可以將這些參數加在命令中,但是這樣還是會很復雜。解決這個問題的通用方法就是在項目中創建一個配置文件,在里面配置相應的插件和預設。

  1. .babelrc

  只是項目中經常用到的方式,在項目根目錄創建名為.babelrc文件,內部包括兩個方面:plugins和p'resets

{
  "presets": [...],
  "plugins": [...]
}

  2. babel.config.js

  這種方式是使用js代碼編寫,並導出一個和上面方式相同的對象

1 module.exports = function () {
2   const presets = [ ... ];
3   const plugins = [ ... ];
4   return {
5     presets,
6     plugins
7   };
8 }

  3. package.json

  這種該方式是在項目配置文件package.json中進行配置,如下:

1 {
2   "name": "my-package-babel",
3   "version": "1.0.0",
4   "babel": {
5     "presets": [ ... ],
6     "plugins": [ ... ],
7   }
8 }

  這三種都是等效的,當配置完成后,可以在babel-cli的命令行中配置,比如:

babel --config-file /path/to/my/babel.config.json --out-dir dist ./src

  事實上,真正收取並處理分析配置文件的還是@babel/core,在core源碼里面對於支持的配置文件名如下:

 

五、具體使用實例

  下面以真實項目的搭建為例,簡單介紹babel的具體使用方式。

  項目的框架使用react,構建工具是webpack。webpack有babel-loader的插件,加入這個loader之后表示所有打包的文件都會由babel-loader來處理,同時使用到的babel插件有@babel/core、@babel/preset-env、@babel/preset-react。此時webpack中的配置文件關於babel的配置如下:

  webpack.config.js:

1 {
2     test: "\.js$",
3     loader: "babel-loader",
4     exclude: "/node_modules"
5 }

  .babelrc

{
    "presets": ["@babel/env", "@babel/react"],
    "plugins": ["transform-runtime"]
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM