深刻了解babel(二)

接着上一篇文章《深刻了解babel(一)》前端

Babel 的處理步驟

Babel 的三個主要處理步驟分別是: 解析(parse),轉換(transform),生成(generate)。對應着babel-core源碼中分別用到的babylon、babel-traverse、babel-generator。node

(1)Babylon

Babylon 是 Babel 的解析器。最初是 從Acorn項目fork出來的。Acorn很是快,易於使用。webpack

import * as babylon from "babylon";

const code = `function square(n) {
  return n * n;
}`;

babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }

(2)babel-traverse

Babel Traverse(遍歷)模塊維護了整棵樹的狀態,而且負責替換、移除和添加節點。咱們能夠和 Babylon 一塊兒使用來遍歷和更新節點。git

import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `function square(n) {
  return n * n;
}`;

const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});

(3)babel-generator

Babel Generator模塊是 Babel 的代碼生成器,它讀取AST並將其轉換爲代碼和源碼映射github

import * as babylon from "babylon";
import generate from "babel-generator";

const code = `function square(n) {
  return n * n;
}`;

const ast = babylon.parse(code);

generate(ast, {}, code);
// {
//   code: "...",
//   map: "..."
// }

抽象語法樹(AST)

ast抽象語法樹在以上三個神器中都出現過,因此ast對於編譯器來講相當重要。如下列舉了一些ast的應用:web

  • 瀏覽器會把js源碼經過解析器轉爲抽象語法樹,再進一步轉化爲字節碼或直接生成機器碼
  • JSLint、JSHint對代碼錯誤或風格的檢查,發現一些潛在的錯誤
  • IDE的錯誤提示、格式化、高亮、自動補全等等
  • UglifyJS
  • 代碼打包工具webpack、rollup
  • CoffeeScript、TypeScript、JSX等轉化爲原生Javascript

本身動手寫插件

presets預設就是關於一系列插件的集合,presets的存在減小了babelrc配置文件的體積,不用看到一大堆的插件數組,而且保證了每一個用戶配置的插件清單如出一轍,因此插件對於babel來講相當重要,前端開發者如何開發一個自定義插件決定了從此對代碼編譯的掌控程度,babel插件就像一把手術刀對js源碼進行精準、可靠的改裝。
本人在寫練習寫插件的過程當中主要用到了如下兩個方法:express

引用babel-core模塊進行編碼方式以下:json

const {transform,generate}=require('babel-core');
const myPlugin=require('./myPlugin');

const code = `d = a + b + c`;

var es5Code = transform(code, {
  plugins: [myPlugin]
})
console.log(es5Code.code);

ast explorer

本人比較青睞的babel插件在線編寫方式,能夠實時看到編譯後的結果以及對應的AST部分,結合babel-types能夠很快的寫出手術刀式的插件,下面這張圖是ast explorer解析出來的json:
clipboard.pngsegmentfault

插件編寫第一站 -- 認識path

export default function (babel) {
  const {types:t}=babel;
  return {
    name: "無關緊要的插件名字",
    visitor: {
      VariableDeclaration(path,state){
          console.log(path);
      }
    },
  };
  
}

每個插件都要返回帶有visitor字段的對象,而visitor對象中存放你的遍歷方法,本人總結爲等價於上面ast explorer截圖中的type屬性(例如:VariableDeclaration),遍歷方法是指插件根據遍歷方法讓ast中的節點走進你寫的遍歷方法函數中。遍歷方法就像js中的addeventlistener,能夠重複寫多個監聽函數,因此當多個插件疊合在一塊兒就會出現一些不可預料的事情,這是考驗你插件編寫是否安全、可靠的事情,也是最難的部分。數組

舉一個最簡單的例子,如何刪除代碼中的全部console?

let a=33;
console.log(12121212);
var b;
console.warn(12121212);
aaaa,cccc
console.error(12121212);
dd=0;
let c;
export default function ({types:t}) {
  return {
    name: "刪除全部的console",
    visitor: {
      CallExpression(path,state){
          if(path.get('callee').isMemberExpression()){
               if(path.get('callee').get('object').isIdentifier()){
                          if(path.get('callee').get('object').get('name').node=='console')path.remove()
               }
        }
      }
    },
  };
  
}

CallExpression遍歷方法也就是console.log(...)對應的AST type屬性,當走進CallExpression函數後,咱們能夠獲取path和state兩個參數,path包含了當前節點的相關信息,按照前端的思惟能夠理解爲dom節點,能夠往上或者往下查找節點,當前節點path包含了不少信息,方便咱們編寫插件,而state中包含了插件的options和數據,options就是babelrc中plugins引入插件時,添加的options,在state中能夠接收到它。

剛開始寫插件的時候,徹底當成dom節點直接獲取節點中的信息是很是危險的(我也是看了babel多個插件後知道的),每往下取一個信息時都要去判斷這個類型是否跟咱們的ast樹同樣,這樣就能夠去除掉其餘的狀況,例如其餘的CallExpression也走到這個函數中了,可是它可能並無callee或者object,代碼執行到這邊就會出錯或者誤傷,嚴謹的控制節點獲取流程將會幫助咱們省去不少沒必要要的麻煩。

代碼中獲取callee節點能夠有兩種方式,一種是path.node.callee,還有一種是path.get('callee'),我的比較喜歡後者,由於能夠直接調用方法(例如isMemberExpression),不然你就要像這樣去判斷t.isMemberExpression(path.node.callee),不夠優雅。

當咱們條件判斷到當前node是console,直接用remove方法就能夠刪除ast節點了,編譯後的代碼:

let a=33;
var b;
aaaa,cccc
dd=0;
let c;

babel官方已經發布了一個刪除console的插件,能夠對比下發現,思路和步驟基本一致,babel官方開發的更加全面,考慮了其餘兩個狀況。

插件編寫第二站 -- 做用域的影響

function a(n){
    n*n
}
let n=1

考慮下如何改寫函數中n變成_n?

export default function ({ types: t }) {
  let paramsName='';
  
  return {
    name: "給function中的參數加上下劃線",
    visitor: {
      FunctionDeclaration(path) {
        if(!path.get('params').length||!path.get('params')[0])return;
        paramsName=path.get('params')[0].get('name').node;
        path.traverse({
          Identifier(path){
            if(path.get('name').node==paramsName)path.replaceWith(t.Identifier('_'+paramsName));
          }
        });
        
      },
      
    }
  };
}

按照第一個例子的思路,咱們很容易就能夠把n給改爲_n,可是這時候fucntion外面的let n=1,也會被改寫,因此咱們在FunctionDeclaration方法中調用了path.traverse,把須要遍歷的方法Identifier包裹在其中,這樣就保護了外面代碼的安全,這種方式保證了插件編寫的安全性

插件編寫第三站 -- bindings

const aaaa=1;
const bb=4;
function b(){
    let aaaa=2;
      aaaa=3;
}
aaaa=34;

讓咱們來接着作另一個例子,如何將const改爲var,而且對const聲明的值給予只讀保護?

export default function (babel, options) {
  return {
    name: "const polyfill",
    visitor: {
      VariableDeclaration(path) {
        if(path.get('kind').node!='const')return;
        path.node.kind='var';
      },
      ExpressionStatement(path){
          if(!path.get('expression').isAssignmentExpression())return;
        let nodeleft=path.get('expression').get('left');
          if(!nodeleft.isIdentifier())return;
        if(path.scope.bindings[nodeleft.get('name').node].kind=='const')console.error('Assignment to constant variable');
      }
    },
  };
  
}

VariableDeclaration方法中將const改爲了let,ExpressionStatement方法中用來觀察const的變量是否被修改,因爲function有本身的做用域,因此aaaa能夠被從新聲明和修改,這裏用到了bindings屬性,能夠查看該節點的變量申明類型,當發現kind爲const時才發出error警告,這個例子是對bindings的一次應用。

插件編寫第四站 -- 建立節點

當咱們替換一個節點或者插入一個節點到容器中,咱們須要按照節點的構建規則來建立,下面的例子是將n*n修改爲n+100

function square(n) {
   return n * n;
}

先給出答案,代碼以下:

export default function ({types:t}) {
  return {
    name: "將n*n修改爲n+100",
    visitor: {
      BinaryExpression(path){
          path.replaceWith(t.binaryExpression('+', path.node.left, t.Identifier('100')));
        path.stop();
      }
    },
  };
}

如今咱們要把BinaryExpression這個type的節點給替換掉,就要按照BinaryExpression節點的規則來建立,能夠參考babel-types網站的說明文檔:

clipboard.png

咱們須要分別構建operator、left、right這三種類型的節點,再查看ast中對這三個節點的描述

clipboard.png

OK,left和right都是Identifier類型,而operator是字符串,字符串直接寫入「+」就能夠替換掉了,而Identifier類型節點的建立還要查看babel-types給出的文檔:

clipboard.png

咱們只要給出string類型的name就能夠了,因此咱們能夠成功建立本身的節點了。

總結

ast explorer真的是一個很好的網站,而且能夠在插件中寫console,能夠在控制檯中實時看到console的結果,對咱們理解ast節點用很大的幫助,另外以上介紹插件的例子仍是太少,插件編寫要注意的遠不止這些方面,可是本人沒時間想出那麼多的例子來很好的介紹,因此你們能夠直接閱讀這篇文檔來深刻了解。

相關文章
相關標籤/搜索