名詞解釋
AST:Abstract Syntax Tree, 抽象語法樹
DI: Dependency Injection, 依賴注入
===============================================================
Babel的解析引擎
Babel使用的引擎是babylon,babylon並非由babel團隊自己開發的,而是fork的acorn專案,acorn的專案本人在很早之前在興趣部落1.0在構建中使用,為了是做一些程式碼的轉換,是很不錯的一款引擎,不過acorn引擎只提供基本的解析ast的能力,遍歷還需要配套的acorn-travesal, 替換節點需要使用acorn-,而這些開發,在Babel的外掛體系開發下,變得一體化了
Babel的工作過程
Babel會將原始碼轉換AST之後,通過便利AST樹,對樹做一些修改,然後再將AST轉成code,即成原始碼。
上面提到Babel是fork acon專案,我們先來看一個來自興趣部落專案的,簡單的ACON示例
一個簡單的ACON轉換示例
解決的問題
將
1 2 3 |
Model.task('getData', function($scope, dbService){ }); |
轉換成
1 2 3 |
Model.task('getData', ['$scope', 'dbService', function($scope, dbService){ }]); |
熟悉angular的同學都能看到這段程式碼做的是對DI的自動提取功能,使用ACON手動擼程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
var code = 'let a = 1; // ....'; var acorn = require("acorn"); var traverse = require("ast-traverse"); var alter = require("alter"); var ast = acorn.parse(code); var ctx = []; traverse(ast, { pre: function(node, parent, prop, idx){ if(node.type === "MemberExpression") { var object = node.object; var objectName = object.name; var property = node.property; var propertyName = property.name; // 這裡要進行替換 if (objectName === "Model" && (propertyName === "service" || propertyName === "task")) { // 第一個就為serviceName 第二個是function var arg = parent.arguments; var serviceName = arg[0]; var serviceFunc = arg[1]; for (var i = 0; i < arg.length; i++) { if (arg[i].type === "FunctionExpression") { serviceFunc = arg[i]; break; } } if (serviceFunc.type === "FunctionExpression") { var params = serviceFunc.params; var body = serviceFunc.body; var start = serviceFunc.start; var end = serviceFunc.end; var funcStr = source.substring(start, end); //params裡是注入的程式碼 var injectArr = []; for (var j = 0; j < params.length; j++) { injectArr.push(params[j].name); } var injectStr = injectArr.join('","') var replaceString = '["' + injectStr + '", ' + funcStr + ']'; if(params.length){ ctx.push({ start: start, end: end, str: replaceString }) } } } } } }); var distStr = alter(code, ctx); console.log(distStr); |
可以從上面的過程看到acorn的特點
1.acorn做為一款優秀的原始碼解析器
2.acorn並不提供對AST樹的修改能力
3.acorn並不提供AST樹的還原能力
4.修改原始碼仍然靠原始碼修改字串的方式
Babel正是擴充套件了acorn的能力,使得轉換變得更一體化
Babel的前序工作——Babylon、babel-types:code轉換為AST
Babel轉AST樹的過程涉及到語法的問題,轉AST樹一定有對就的語法,如果在解析過程中,出現了不符合Babel語法的程式碼,就會報錯,Babel轉AST的解析過程在Babylon中完成
解析成AST樹使用babylon.parse方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import babylon from 'babylon'; let code = ` let a = 1, b = 2; function sum(a, b){ return a + b; } sum(a, b); `; let ast = babylon.parse(code); console.log(ast); |
結果如下
AST如下
關於AST樹的詳細定義Babel有文件
https://github.com/babel/babylon/blob/master/ast/spec.md
關於AST樹的定義
1 2 3 4 |
interface Node { type: string; loc: SourceLocation | null; } |
其中位置定義如下
1 2 3 4 5 |
interface SourceLocation { source: string | null; start: Position; end: Position; } |
1 2 3 4 |
interface Position { line: number; // >= 1 column: number; // >= 0 } |
節點又包含行號和列號
再看Program的定義
1 2 3 4 5 6 |
interface Program <: Node { type: "Program"; sourceType: "script" | "module"; body: [ Statement | ModuleDeclaration ]; directives: [ Directive ]; } |
Program是繼承自Node節點,型別是Program, sourceType有兩種,一種是script,一種是module,程式體是一個宣告體Statement或者模組宣告體ModuleDeclaration節點陣列
Babylon支援的語法
Babel或者說Babylon支援的語法現階段是不可以第三方擴充套件的,也就是說我們不可以使用babylon做一些奇奇怪的語法,換句話說
不要希望通過babel的外掛體系來轉換自己定義的語法規則
那麼babylon支援的語法有哪些呢,除了常規的js語法之外,babel暫時只支援如下的語法
Plugins
- estree
- jsx
- flow
- doExpressions
- objectRestSpread
- decorators (Based on an outdated version of the Decorators proposal. Will be removed in a future version of Babylon)
- classProperties
- exportExtensions
- asyncGenerators
- functionBind
- functionSent
- dynamicImport
如果要真要自定義語法,可以在babylon的plugins目錄下自定義語法
https://github.com/babel/babylon/tree/master/src/plugins
Babel-types,擴充套件的AST樹
上面提到的babel的AST文件中,並沒有提到JSX的語法樹,那麼JSX的語法樹在哪裡定義呢,同樣jsx的AST樹也應該在這個文件中指名,然而babel團隊還沒精力準備出來
實際上,babel-types有擴充套件AST樹,babel-types的definitions就是天然的文件,具體的原始碼定義在這裡
舉例一個AST節點如查是JSXElement,那麼它的定義可以在jsx.js中找到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
defineType("JSXElement", { builder: ["openingElement", "closingElement", "children", "selfClosing"], visitor: ["openingElement", "children", "closingElement"], aliases: ["JSX", "Immutable", "Expression"], fields: { openingElement: { validate: assertNodeType("JSXOpeningElement"), }, closingElement: { optional: true, validate: assertNodeType("JSXClosingElement"), }, children: { validate: chain( assertValueType("array"), assertEach(assertNodeType("JSXText", "JSXExpressionContainer", "JSXSpreadChild", "JSXElement")) ), }, }, }); |
openingElement: 必須是一個JSXOpeningElement節點
closingElement: 必須是一個JSXClosingElement節點
children: 必須是一個陣列,陣列元素必須是JSXText、JSXExpressionContainer、JSXSpreadChild中的一種型別
selfClosing: 未指明驗證
使用 babel-types.[TYPE]方法就可以構造這樣的一個AST節點
1 2 3 4 5 6 7 8 |
var types = require('babel-types'); var jsxElement = types.JSXElement( types.OpeningElement(...), types.JSXClosingElement(...), [...], true ); |
同樣驗證是否一個JSXElement節點,也可以使用babel-types.isTYPE方法
比如
1 2 3 |
var types = require('babel-types'); types.isJSXElement(astNode); |
所以用JSXElement語法定義可以直接看該檔案,簡單做個梳理如下
其中,斜體代表非終結符,粗體為終結符
Babel的中序工作——Babel-traverse、遍歷AST樹,外掛體系
- 遍歷的方法
一旦按照AST中的定義,解析成一顆AST樹之後,接下來的工作就是遍歷樹,並且在遍歷的過程中進行轉換
Babel負責便利工作的是Babel-traverse包,使用方法
1 2 3 4 5 6 7 8 9 10 11 12 |
import traverse from "babel-traverse"; traverse(ast, { enter(path) { if ( path.node.type === "Identifier" && path.node.name === "n" ) { path.node.name = "x"; } } }); |
遍歷結點讓我們可以獲取到我們想要操作的結點的可能,在遍歷一個節點時,存在enter和exit兩個時刻,一個是進入結點時,這個時候節點的子節點還沒觸達,遍歷子節點完成的時刻,會離開該節點,所以會有exit方法觸發
訪問節點,可以使用的引數是path引數,path這個引數並不直接等同於節點,path的屬性有幾個重要的組成,如下
舉個例子,如下的程式碼會將所有function變成另外的function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import traverse from "babel-traverse"; import types from "babel-types"; traverse(ast, { enter(path) { let node = path.node; if(types.isFunctionDeclaration(node)){ path.replaceWithSourceString(`function add(a, b) { return a + b; }`); } } }); |
結果如下
1 2 3 4 5 |
- function square(n) { - return n * n; + function add(a, b) { + return a + b; } |
注意這裡我們使用babel-types來判別node的型別,使用path的replaceWithSourceString方法來替換節點
但這裡在babel的文件中也有提示,儘量少用replaceWithSourceString方法,該方法一定會呼叫babylon.parse解析程式碼,在遍歷中解析程式碼,不如將解析程式碼放到遍歷外面去做
其實上面的過程只是定義瞭如何遍歷節點的時候轉換節點
babel將上面的便利操作對外開放出去了,這就構成了babel的外掛體系
babel的外掛體系——結點的轉換定義
babel的外掛就是定義如何轉換當前結點,所以從這裡可以看出babel的外掛能做的事情,只能轉換ast樹,而不能在作用在前序階段(語法分析)
這裡不得不提下babel的外掛體系是怎麼樣的,babel的外掛分為兩部分
- babel-preset-xxx
- babel-plugin-xxx
preset: 預設, preset和plugin其實是一個東西,preset定義了一堆plugin list
這裡值得一提的是,preset的順序是倒著的,plugin的順序是正的,也就是說
preset: [‘es2015’, ‘react’], 其實是先使用react外掛再用es2015
plugin: [‘transform-react’, ‘transfrom-async-function’] 的順序是正的遍歷節點的時候先用transform-react再用transfrom-async-function
babel外掛編寫
如果是自定義外掛,還在開發階段,要先在babel的配置檔案指明babel外掛的路徑
1 2 3 4 5 6 7 8 9 10 11 |
{ "extensions": [".jsx", ".js"], "presets": ["react", "es2015"], "plugins": [ [ path.resolve(SERVER_PATH, "pourout/babel-plugin-transform-xxx"), {} ], ] } |
babel的自定義外掛寫法是多樣,上面只是一個例子,可以傳入option,具體可以參考babel的配置文件
上面的程式碼寫成babel的外掛如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module.exports = function(babel) { var types = babel.types; // plugin contents return { visitor: { FunctionDeclaration: { enter: function(path){ path.replaceWithSourceString(`function add(a, b){ return a + b}`); } } } }; }; |
Babel的外掛包return一個function, 包含babel的引數,function執行後返回一個包含visitor的物件,物件的屬性是遍歷節點匹配到該型別的處理方法,該方法依然包含enter和exit方法
一些AST樹的建立方法
在寫外掛的過程中,經常要建立一些AST樹,常用的方法如下
- 使用babel-types定義的建立方法建立
比如建立一個var a = 1;
1 2 3 4 5 6 7 8 9 |
types.VariableDeclaration( 'var', [ types.VariableDeclarator( types.Identifier('a'), types.NumericLiteral(1) ) ] ) |
如果使用這樣建立一個ast節點,肯定要累死了
- 使用replaceWithSourceString方法建立替換
- 使用template方法來建立AST結點
template方法其實也是babel體系中的一部分,它允許使用一些模板來建立ast節點
比如上面的var a = 1可以使用
1 2 3 4 5 6 |
var gen = babel.template(`var NAME = VALUE;`); var ast = gen({ NAME: t.Identifier('a'), VALUE: t.NumberLiteral(1) }); |
當然也可以簡單寫
1 2 3 4 |
var gen = babel.template(`var a = 1;`); var ast = gen({ }); |
接下來就可以用path的增、刪、改操作進行轉換了
Babel的後序工作——Babel-generator、AST樹轉換成原始碼
Babel-generator的工作就是將一顆ast樹轉回來,具體操作如下
1 2 3 |
import generator from "babel-generator"; let code = generator(ast); |
Babel的外圍工作——Babel-register,動態編譯
通常我們都是使用webpack編譯後程式碼再執行程式碼的,使用Babel-register允許我們不提前編譯程式碼就可以執行程式碼,這在node端是非常便利的
在node端,babel-regiser的核心實現是下面這兩個程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function loader(m, filename) { m._compile(compile(filename), filename); } function registerExtension(ext) { var old = oldHandlers[ext] || oldHandlers[".js"] || require.extensions[".js"]; require.extensions[ext] = function (m, filename) { if (shouldIgnore(filename)) { old(m, filename); } else { loader(m, filename, old); } }; } |
通過定義require.extensions方法,可以覆蓋require方法,這樣呼叫require的時候,就可以走babel的編譯,然後使用m._compile方法執行程式碼
但這個方法在node是不穩定的方法
結語
最後,就像babylon官網感覺acorn一樣,babel為前端界做了一件awesome的工作,有了babel,不僅僅可以讓我們的新技術的普及提前幾年,我們可以通過寫外掛做更多的事情,比如做自定義規則的驗證,做node的直出node端的適配工作等等。
參考資料
babel官網: https://babeljs.io
babel-github: https://github.com/babel
babylon: https://github.com/babel/babylon
acorn: https://github.com/marijnh/acorn
babel-ast文件: https://github.com/babel/babylon/blob/master/ast/spec.md
babel外掛cookbook: https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
babel-packages: https://github.com/babel/babel/tree/7.0/packages
babel-types-definitions: https://github.com/babel/babel/tree/7.0/packages/babel-types/src/definitions