探究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不會對源碼做任何轉換,需要自己配置。
集成到工具中
Webpack
① 安裝:
npm install –save-dev babel-loader babel-coren
② webpack配置:
③ 配置.babelrc文件module: {n rules: [n { test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }n ]n}n
安裝插件:
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文件:
③ 創建.babelrc文件同上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
其他工具請參見安裝組合方式。
命令行下使用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
babel-register,babel-node
使用babel-register需要創建register.js文件,在文件中引入babel-register和需要編譯的所有文件,
or//register.js nrequire("babel-register");nrequire("./index.js");n// clinnode register.js // 啟動編譯n
這種方式不適合正式產品環境引用,因為直接部署本地編譯完的代碼肯定比在線上編譯要好。但這種方式用在構建腳本或是其他本地運行的腳本中很合適。npm install babel-node babel-cli //使用babel-nodenbabel-node index.js //啟動編譯 n
以編程的方式使用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
Babel編譯過程
首先必須了解幾個基礎概念。
抽象語法樹
abstract syntax tree ,縮寫AST,是源代碼的抽象語法結構的樹狀表現形式。 樹上的每個節點都表示源代碼中的一種結構。
AST Explorer 可以幫你直觀的認識AST的結構,為了跟babel保持一致,請確保選擇的解析器是babylon6。
看看下面的代碼解析成AST後的樣子
let i= 0;ni+1;n
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
- 解析(parse):把字元串形式的代碼轉換成AST
- 轉換(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
- 生成(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
// 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 開源一周年回顧