前言:AST已經深刻的存在咱們項目腳手架中,可是咱們缺不瞭解他,本文帶領你們一塊兒體驗AST,感覺一下解決問題另外一種方法
在講以前先簡單介紹一下什麼AST,抽象語法樹(Abstract Syntax Tree
)簡稱 AST
,是源代碼的抽象語法結構的樹狀表現形式。
平時不少庫都有他的影子:
例如 babel
, es-lint
, node-sass
, webpack
等等。javascript
OK 讓咱們看下代碼轉換成 AST
是什麼樣子。css
const ast = 'tree'
這是一行簡單的聲明代碼,咱們看下他轉換成AST的樣子java
咱們發現整個樹的根節點是 Program,他有一個子節點 body,body 是一個數組,數組中還有一個子節點 VariableDeclaration,VariableDeclaration中表示const ast = 'tree'
這行代碼的聲明,具體的解析以下:node
type: 描述語句的類型,此處是一個變量聲明類型 kind: 描述聲明類型,相似的值有'var' 'let' declarations: 聲明內容的數組,其中每一項都是一個對象 ------------type: 描述語句的類型,此處是一個變量聲明類型 ------------id: 被聲明字段的描述 ----------------type: 描述語句的類型,這裏是一個標識符 ----------------name: 變量的名字 ------------init: 變量初始化值的描述 ----------------type: 描述語句的類型,這裏是一個標識符 ----------------name: 變量的值
大致上的結構是這樣,body下的每一個節點還有一些字段沒有給你們說明,例如:位置信息,以及一些沒有值的key都作了隱藏,推薦你們能夠去 asteplorer這個網站去試試看。webpack
總結一下, AST就是把代碼經過編譯器變成樹形的表達形式。git
如何生成把純文本的代碼變成AST呢?編輯器生成語法樹通常分爲三個步驟es6
比方說上面的例子 const ast = 'tree'
,會被分析爲const、ast、=、'tree'
github
const ast = 'tree'; [ { type: 'keyword', value: 'const' }, { type: 'identifier', value: 'a' }, { type: 'punctuator', value: '=' }, { type: 'numeric', value: '2' }, ]
當詞法分析源代碼的時候,它會一個一個字母地讀取代碼,因此很形象地稱之爲掃描-scans;當它遇到空格,操做符,或者特殊符號的時候,它會認爲一個話已經完成了。web
2.語法分析:也稱爲解析器。它會將詞法分析出來的數組轉化成樹形的表達形式。同時,驗證語法,語法若是有錯的話,拋出語法錯誤。數組
3.生成樹:當生成樹的時候,解析器會刪除一些不必的標識tokens(好比不完整的括號),所以AST不是100%與源碼匹配的,可是已經能讓咱們知道如何處理了。說個題外話,解析器100%覆蓋全部代碼結構生成樹叫作CST(具體語法樹)
有不少的第三方庫能夠用來實戰操做,能夠去asteplorer這個網站去找你喜歡的第三方庫,這裏不限於javascript
,其餘的語言也能夠在這個網站上找到。
如圖:
關於javascript
的第三方庫,這裏給你們推薦 babel
的核心庫babylon
// yarn add babylon import * as babylon from 'babylon'; const code = ` const ast = 'tree' ` const ast = babylon.parse(code); // ast
ok,如今咱們已經知道如何把咱們的代碼變成 AST
了,可是現實中,咱們常常會使用到代碼的轉換,比方說 jsx -> js, es6 -> es5, 是的就是 babel
,咱們來看看babel
是如何轉換代碼的。
大致上babel
轉換代碼分爲三步
1. 經過`babylon`生成`AST` 2. 遍歷`AST`同時經過指定的訪問器訪問須要修改的節點 3. 生成代碼
看一個簡單的例子一塊兒理解一下
生成AST
import * as babylon from 'babylon'; // 這裏也可使用 import parser from '@babel/parser'; 這個來生成語法樹 const code = ` const ast = 'tree' console.log(ast); ` const ast = babylon.parse(code); // ast
遍歷AST
同時經過訪問器CallExpression
來訪問console.log(ast)
並刪除它
import traverse from '@babel/traverse' import t from '@babel/types'; // 2 遍歷 const visitor = { CallExpression(path) { const { callee } = path.node; if ( t.isMemberExpression(callee) && callee.object.name === 'console' && callee.property.name === 'log' ) { path.remove(); } }, } traverse.default(ast, visitor);
生成新代碼
import generator from '@babel/generator'; generator.default(ast);
簡單的答疑:CallExpression表示這是一個調用,爲何還要作更深刻的判斷呢,由於直接的函數調用 foo() 這也是一個CallExpression,A.foo()這也是一個CallExpression, 因此要更深刻的判斷
好的,代碼轉換完成!值得慶祝。咱們能夠看到第一步生成AST
和第三步生成新代碼都由babel
替咱們作了,咱們真正操做的地方在於第二步:經過訪問器操做須要操做的節點。
因而可知咱們開發babel-plugin的時候,也只須要關注visitor這部分就好。
上述代碼改成babel-plugin示例:
module.export = function plugin({ types: t}) { return { visitor: { CallExpression(path) { const { node } = path; if (t.isMemberExpression(node.callee) && node.callee.object.name === 'console' && node.callee.property.name === 'log' ) { path.remove(); } }, }, }; }
將這個插件加入到你的babel
插件列表中,能夠看到它真的生效了,一切都是這麼簡單。so amazing!
開頭提到的經常使用庫prettire
, eslint
, css-loader
等等其實都是先生成AST
,而後再操做AST
,最後在生成代碼。只不過操做AST
的過程很複雜,觸類旁通在項目裏,組件庫升級,組件批量替換均可以使用這個思路。甚至能夠根據業務作一些本身業務方的babel-plugin
都行。
感謝您的閱讀,有問題能夠在評論區交流~
幫助連接
如何開發一個babel-plugin
《AST for JavaScript developers》