在前文《babel是怎樣工做的》中介紹了 Bable 中的的AST,此次我們給 bable 寫一個插件,文中會覆蓋大部份的用法,若是你對某些細節不是很明白,能夠去看一下官方的 Babel 手冊,中文版在這裏:javascript
https://github.com/jamiebuild...前端
不過有的部分尚未翻譯完。java
首先要找到要修改的節點,假設咱們要幫一個特定的函數 myFunction
加上調試用的信息,在這裏只加上文件名就好了,而這個 myFunction
長成這樣:node
function myFunction(data, optionalFilename)
使用者能夠本身在 optionalFilename
加上想要的名字,或者用插件加上去,第一步就是打開 AST Explorer(https://astexplorer.net/)並寫一個簡單的測試程序來肯定 AST 應該是什麼樣的:git
myFunction(foo, __filename)
結果是這樣:程序員
因爲咱們要可以判斷使用者傳入的幾個參數,也要能肯定使用者是在調用咱們的函數,因此應該在 CallExpression
中進行處理:es6
// babel 的 plugin 能夠用 module.exports 或 es6 的 export default // 函數的第一個參數是使用者正在使用的 `@babel/core` module.exports = function ({ types: t }) { return { name: 'add-debug-information', // plugin 的名字,個加不加都行 // pre(state) {}, // 要處理一個新的檔案時會調用這個函數 // post(state) {}, // 文件處理完成時要調用的函數 visitor: { CallExpression(path) { console.log(path) // 這樣就能夠獲得 CallExpression }, // babel 能夠在進入或是離開節點時調用 plugin 的函數,不過由於一般會須要在進入節點時處理, // 因此 babel 讓使用者能夠簡寫成上面那樣,若是要在進入和離開時存取節點的話要寫成像下面這樣 // CallExpression: { // enter() { // 進入時 // }, // leave() { // 離開時 // }, // } } } }
下一步就是要判斷是否是咱們要作處理的節點了,這裏先只簡單的判斷兩個條件,函數名是 myFunction
而且只能用一個參數:github
// 這裏只寫 CallExpression 的內容 if ( t.isIdentifier(path.node.callee, { name: 'myFunction' }) && // 判斷函數名是 `myFunction` ,這裏的 t 就是 babel 傳來的 types // 另外也能夠直接判斷 node 的 name,好比:t.isIdentifier(path.node.callee) && path.node.callee.name === 'myFunction' path.node.arguments.length < 2) { // 肯定沒有傳入第二個參數 // 處理目標節點 }
若是要判斷的目標比較複雜,目前也沒有比較好的方法,只能這樣比較。另外由於 babel 中只能拿到到 AST 信息,若是要判斷類型等幾乎是沒有什麼辦法的,因此實際在寫插件時必須考慮全部合理的寫法,若是真的沒辦法處理時必定要要告訴使用者必須按照某種格式寫,不然不會被處理。面試
在已經找到目標目標的前提下,要把文件名加入到參數中。這裏直接加入 node 中的 `__filename
變量,這個變量在 node 的模塊中是那個原始碼文件的文件名。segmentfault
// 在上面的 if 中 path.pushContainer('arguments', t.identifier({ name: '__filename' })) // 若是要加載開頭,能夠用 unshiftContainer
那麼爲何要用 pushContainer
修改 AST 的內容呢?直接用 push
加到 arguments
中不行嗎?這裏最大的差異在於 plugin 新增了節點,若是有上游的添加、刪除等改變,babel 也必需要便利新的節點,因此要用 babel 的 API 讓它知道有節點被改變了。
完整的代碼以下:
module.exports = function ({ types: t }) { return { name: 'add-debug-information', visitor: { CallExpression(path) { if (t.isIdentifier(path.node.callee, { name: 'myFunction' }) && path.node.arguments.length > 1) { path.pushContainer('arguments', t.identifier('__filename')) } } } } }
接下來再來看看其餘例子。
假如要在正式環境把除錯信息移除的話,就把 myFunction
第二個之後的參數都移除掉:
module.exports = function ({ types: t }) { return { visitor: { CallExpression(path) { if (t.isIdentifier(path.node.callee, {name: 'myFunction'})) { while (path.node.arguments.length > 1) { // 只要參數數量超過 1 個 path.get(`arguments.1`).remove() //就把第二個參數移除,而下一個會補上來,因此下一次循環會再移除掉下一個 } } } } }; }
path
的 get
能夠用於獲取指定位置的 Path
對象,可用於處理特定的子節點。
此次需求變成了在代碼中加上對 NODE_ENV
的判斷,若是是生產環境就不要除錯信息,結果像這樣:
// 原來 myFunction(data) // 變爲 process.env.NODE_ENV === 'production' ? myFunction(data) : myFunction(data, __filename)
一般上面的代碼在正式環境中不會真的多出一個判斷,由於通常 bundler 會把 NODE_ENV 換成字串常量,而後再由 minifier 移除掉不須要的部分。
由於要產出的代碼變多了,此次就用 template
module.exports = function ({ types: t, template }) { // 這裏用到的 `%%data%%` 表明稍後咱們能夠放節點去取代那個位置,只須要用兩個 `%` 包起來便可, // 這個是 babel 7.4 之後才支持的語法,若是想支持之前的版本,就要把它改爲 `DATA` (必定要全大寫) // template 的返回值是一個函數 const tpl = template(`process.env.NODE_ENV === 'production' ? myFunction(%%data%%) : myFunction(%%data%%, %%source%%);`) // 用來標記已經遍歷過的節點,用 Symbol 能夠防止產生命名上的衝突 const visited = Symbol() return { visitor: { CallExpression(path) { // 檢查節點是否遍歷過 if (path.node[visited]) { return } if (t.isIdentifier(path.node.callee, {name: 'myFunction'})) { // 替換節點 path.replaceWith( // tpl 是一個函數,只須要把 placeholder 的部份傳進去,就會返回 AST tpl({ // 這裏要避免使用者沒有傳入第一個參數的狀況,否則後面的參數會變成第一個參數 // 也能夠拋出 error 或者讓 myFunction 在運行時進行判斷 data: path.node.arguments[0] || t.identifier('undefined'), // 若是使用者本身提供了除錯信息,那麼就用使用者提供的,否則就用 __filename source: path.node.arguments[1] || t.identifier('__filename') }) ) // 把節點下的 `myFunction` 都標記爲遍歷過 path.node.consequent[visited] = true path.node.alternate[visited] = true } } } } }
前面說過,要是新加入節點的話,babel 也會去遍歷它,而咱們加入的節點中就包含了要處理的目標節點,若是不進行特殊處理的話就會一直無限的遍歷下去,因此要給添加的節點加上本身的標記,這樣就能夠避免重複處理。
在上一個例子中,爲了要避免使用者少傳參數而給了默認值,那若是要在少傳參數時拋出錯誤又要怎麼作呢。
module.exports = function ({ types: t, template }) { // 和上一個例子差很少 const tpl = template(`process.env.NODE_ENV === 'production' ? myFunction(%%data%%) : myFunction(%%data%%, %%source%%);`) const visited = Symbol() // 建立一個函數來幫助拋出 error function throwMissingArgument(path) { // 這裏用 path 上的 buildCodeFrameError ,這樣顯示的時候就可以標記有問題的代碼在什麼地方 throw path.buildCodeFrameError('`myFunction` required at least 1 argument') } return { visitor: { CallExpression(path) { if (path.node[visited]) { return } if (t.isIdentifier(path.node.callee, {name: 'myFunction'})) { path.replaceWith( tpl({ // 這裏改用 throwMissingArgument data: path.node.arguments[0] || throwMissingArgument(path), source: path.node.arguments[1] || t.identifier('__filename') }) ) path.node.consequent[visited] = true path.node.alternate[visited] = true } } } } }
若是沒傳參數的話應該會看到 babel 輸出了這樣的 error
code.js: `myFunction` expect at least 1 argument > 1 | myFunction() | ^^^^^^^^^^^^
到此爲止,咱們終於寫出了本身的第一個 babel 插件。