babel 修改抽象語法樹——入門與實踐

weixin_33866037發表於2018-10-08

問題由來

最近有個想法,之前用Angular2.x 的時候,官方提供了ng-cli 可以一鍵生成component、service、directive 等程式碼檔案,並且還可以修改對應的routes 配置檔案,使得元件自動加入app 的前端路由中(一鍵生成或修改數個檔案)。這使得前端開發效率大為提高,我們不必再手動去建立那麼多資料夾、檔案,並且手動修改route 配置。但是vue-cli 沒有提供這種功能,所以我們想要寫個node.js 指令碼去做這個工作。除了用正則替換的解決辦法,更科學的實際上就需要用到修改程式碼抽象語法樹的方法。

babel 編譯過程

我們通常用 babel 去編譯ES6/ES7 為ES5,以便於 js 指令碼執行在各種瀏覽器上。這個編譯的過程實際上是語法轉換的過程,比如箭頭函式轉為函式表示式,this 的顯式繫結等等。那麼babel 在做這個工作的時候實際上經歷了幾個步驟,parse => transform (AST) => generate

1950967-ca668be7be3ed984.png
https://www.sitepoint.com/understanding-asts-building-babel-plugin/

所以要想完成這幾個步驟,babel 提供了幾個實用工具(Babylon,babel-traverse,babel-generator),我們的思路就是找到route 配置表中該插入新路由的地方,插入新路由並且儲存檔案。
babel.transform 核心函式接受原始碼字串和options 作為輸入,返回一個Object 包含幾個屬性:新的程式碼字串,sourcemap,ast 語法樹物件。

babel.transform("code();", options, function(err, result) {
  result.code;
  result.map;
  result.ast;
});

實踐新增一個route物件

以下是等待修改的路由配置檔案,

// './src/router.ts'
export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: About
    }
    // to be append new route.
  ]
})

以下是通過 babel 修改route 配置檔案的過程

var fs = require('fs');
let babel = require('babel-core');
let t = require('babel-types');
let template = require('@babel/template');
// 讀取需要修改的原始碼內容
var content = fs.readFileSync('./src/router.ts').toString();

const newRoute = {
  path: '/list',
  name: 'list'
  // component: ListComponent
};

// 定義一個 babel 外掛,攔截並修改 routes 的陣列表示式
let visitor = {
  ArrayExpression(path) {
    const elements = path.node.elements;
    console.warn(`routes number:  ${elements.length}`);
    // 新增一個構建出來的 route 物件
    elements.push(t.objectExpression([
      t.objectProperty(t.identifier('path'), t.stringLiteral(newRoute.path)),
      t.objectProperty(t.identifier('name'), t.stringLiteral(newRoute.name)),
      t.objectProperty(t.identifier('component'), t.identifier('ListComponent'))
    ]));
  }
}

// 通過 plugin 轉換原始碼 parse 出來的AST 抽象語法樹,並且返回結果
let result= babel.transform(content, {
   plugins: [
     { visitor }
   ]
 });

 console.warn(`res: ${result.code}`);
 // 把新程式碼寫入新檔案.
 fs.writeFileSync('newRoute.ts', result.code);

比較關鍵的部分就是在visitor 這個自定義的外掛中,攔截ArrayExpression,這是routes: [] 對應的路由陣列。而這個陣列表示式包含了一個elements 陣列,每個物件在AST 中都是ObjectExpression 型別。不論是陣列表示式,還是物件表示式,都是對應 babel-types 中不同的節點(node)型別。所以我們在構建新的 AST 節點時,可以參考AST explorer 中已有的節點型別。

例如這裡我們要新增一個route 物件,則是用babel-types 中的 types.objectExpression(objectProperty[]) 生成一個,根據智慧提示傳入引數,要求是objectProperty 陣列,那麼我們又利用 types.objectProperty(key: identifier, value: string) 生成一個。

t.objectExpression([ 
  // 物件中的第一個屬性 path: string;
  t.objectProperty(t.identifier('path'), t.stringLiteral(newRoute.path)),
  // ...
]);
1950967-fa83265aced3bf9f.png
AST explorer 中原始碼和AST的對應關係

根據官方docs,也可以利用 AST explorer 去尋找對應關係,結合 IDE 的智慧提示來構建你所需要的 AST 語法樹,就可以自動轉換成你想要的程式碼了。實際上,搞懂了babel ,就可以做出ng-cli generate 這樣智慧高效的功能了。

參考文章:
https://www.sitepoint.com/understanding-asts-building-babel-plugin/
http://welefen.com/post/understanding-asts-by-building-your-own-babel-plugin.html
babel 官方文件
AST Explorer

相關文章