埋點是一個常見的需求,就是在函數裏面上報一些信息。像一些性能的埋點,每一個函數都要處理,很繁瑣。能不能自動埋點呢?javascript
答案是能夠的。埋點只是在函數裏面插入了一段代碼,這段代碼不影響其餘邏輯,這種函數插入不影響邏輯的代碼的手段叫作函數插樁。java
咱們能夠基於 babel 來實現自動的函數插樁,在這裏就是自動的埋點。node
好比這樣一段代碼:git
import aa from 'aa'; import * as bb from 'bb'; import {cc} from 'cc'; import 'dd'; function a () { console.log('aaa'); } class B { bb() { return 'bbb'; } } const c = () => 'ccc'; const d = function () { console.log('ddd'); }
咱們要實現埋點就是要轉成這樣:github
import _tracker2 from "tracker"; import aa from 'aa'; import * as bb from 'bb'; import { cc } from 'cc'; import 'dd'; function a() { _tracker2(); console.log('aaa'); } class B { bb() { _tracker2(); return 'bbb'; } } const c = () => { _tracker2(); return 'ccc'; }; const d = function () { _tracker2(); console.log('ddd'); };
有兩方面的事情要作:api
小冊《babel 插件通關祕籍》中有具體 api 的詳細介紹。babel
引入模塊這種功能顯然不少插件都須要,這種插件之間的公共函數會放在 helper,這裏咱們使用 @babel/helper-module-imports。函數
const importModule = require('@babel/helper-module-imports'); // 省略一些代碼 importModule.addDefault(path, 'tracker',{ nameHint: path.scope.generateUid('tracker') })
首先要判斷是否被引入過:在 Program 根結點裏經過 path.traverse 來遍歷 ImportDeclaration,若是引入了 tracker 模塊,就記錄 id 到 state,並用 path.stop 來終止後續遍歷;沒有就引入 tracker 模塊,用 generateUid 生成惟一 id,而後放到 state。性能
固然 default import 和 namespace import 取 id 的方式不同,須要分別處理下。ui
咱們把 tracker 模塊名做爲參數傳入,經過 options.trackerPath 來取。
Program: { enter (path, state) { path.traverse({ ImportDeclaration (curPath) { const requirePath = curPath.get('source').node.value; if (requirePath === options.trackerPath) {// 若是已經引入了 const specifierPath = curPath.get('specifiers.0'); if (specifierPath.isImportSpecifier()) { state.trackerImportId = specifierPath.toString(); } else if(specifierPath.isImportNamespaceSpecifier()) { state.trackerImportId = specifierPath.get('local').toString();// tracker 模塊的 id } path.stop();// 找到了就終止遍歷 } } }); if (!state.trackerImportId) { state.trackerImportId = importModule.addDefault(path, 'tracker',{ nameHint: path.scope.generateUid('tracker') }).name; // tracker 模塊的 id state.trackerAST = api.template.statement(`${state.trackerImportId}()`)();// 埋點代碼的 AST } } }
咱們在記錄 tracker 模塊的 id 的時候,也生成調用 tracker 模塊的 AST,使用 template.statement.
函數插樁要找到對應的函數,這裏要處理的有:ClassMethod、ArrowFunctionExpression、FunctionExpression、FunctionDeclaration 這些節點。
固然有的函數沒有函數體,這種要包裝一下,而後修改下 return 值。若是有函數體,就直接在開始插入就好了。
'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) { const bodyPath = path.get('body'); if (bodyPath.isBlockStatement()) { // 有函數體就在開始插入埋點代碼 bodyPath.node.body.unshift(state.trackerAST); } else { // 沒有函數體要包裹一下,處理下返回值 const ast = api.template.statement(`{${state.trackerImportId}();return PREV_BODY;}`)({PREV_BODY: bodyPath.node}); bodyPath.replaceWith(ast); } }
這樣咱們就實現了自動埋點。
咱們來試下效果:
const { transformFromAstSync } = require('@babel/core'); const parser = require('@babel/parser'); const autoTrackPlugin = require('./plugin/auto-track-plugin'); const fs = require('fs'); const path = require('path'); const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.js'), { encoding: 'utf-8' }); const ast = parser.parse(sourceCode, { sourceType: 'unambiguous' }); const { code } = transformFromAstSync(ast, sourceCode, { plugins: [[autoTrackPlugin, { trackerPath: 'tracker' }]] }); console.log(code);
效果以下:
咱們實現了自動埋點!
上面實現的是一種狀況,實際上可能有的函數不須要埋點,這種能夠本身作一下過濾,或者在函數上寫上註釋,而後根據註釋來過濾,就像 eslint 支持 / eslint-disable / 的設置同樣。關於註釋的操做,能夠看另外一個案例《自動生成 API 文檔》。
函數插樁是在函數中插入一段邏輯但不影響函數本來邏輯,埋點就是一種常見的函數插樁,咱們徹底能夠用 babel 來自動作。
實現思路分爲引入 tracker 模塊和函數插樁兩部分:
引入 tracker 模塊須要判斷 ImportDeclaration 是否包含了 tracker 模塊,沒有的話就用 @babel/helper-module-import 來引入。
函數插樁就是在函數體開始插入一段代碼,若是沒有函數體,須要包裝一層,而且處理下返回值。、
代碼在這裏,建議本身實現一遍。
詳見小冊《babel 插件通關祕籍》。