摘要:接著上一篇文章深入了解一的處理步驟的三個主要處理步驟分別是解析,轉(zhuǎn)換,生成。模塊是的代碼生成器,它讀取并將其轉(zhuǎn)換為代碼和源碼映射抽象語法樹抽象語法樹在以上三個神器中都出現(xiàn)過,所以對于編譯器來說至關(guān)重要。
接著上一篇文章《深入了解babel(一)》
Babel 的處理步驟Babel 的三個主要處理步驟分別是: 解析(parse),轉(zhuǎn)換(transform),生成(generate)。對應(yīng)著babel-core源碼中分別用到的babylon、babel-traverse、babel-generator。
(1)BabylonBabylon 是 Babel 的解析器。最初是 從Acorn項目fork出來的。Acorn非常快,易于使用。
import * as babylon from "babylon"; const code = `function square(n) { return n * n; }`; babylon.parse(code); // Node { // type: "File", // start: 0, // end: 38, // loc: SourceLocation {...}, // program: Node {...}, // comments: [], // tokens: [...] // }(2)babel-traverse
Babel Traverse(遍歷)模塊維護了整棵樹的狀態(tài),并且負責(zé)替換、移除和添加節(jié)點。我們可以和 Babylon 一起使用來遍歷和更新節(jié)點。
import * as babylon from "babylon"; import traverse from "babel-traverse"; const code = `function square(n) { return n * n; }`; const ast = babylon.parse(code); traverse(ast, { enter(path) { if ( path.node.type === "Identifier" && path.node.name === "n" ) { path.node.name = "x"; } } });(3)babel-generator
Babel Generator模塊是 Babel 的代碼生成器,它讀取AST并將其轉(zhuǎn)換為代碼和源碼映射
import * as babylon from "babylon"; import generate from "babel-generator"; const code = `function square(n) { return n * n; }`; const ast = babylon.parse(code); generate(ast, {}, code); // { // code: "...", // map: "..." // }抽象語法樹(AST)
ast抽象語法樹在以上三個神器中都出現(xiàn)過,所以ast對于編譯器來說至關(guān)重要。以下列舉了一些ast的應(yīng)用:
瀏覽器會把js源碼通過解析器轉(zhuǎn)為抽象語法樹,再進一步轉(zhuǎn)化為字節(jié)碼或直接生成機器碼
JSLint、JSHint對代碼錯誤或風(fēng)格的檢查,發(fā)現(xiàn)一些潛在的錯誤
IDE的錯誤提示、格式化、高亮、自動補全等等
UglifyJS
代碼打包工具webpack、rollup
CoffeeScript、TypeScript、JSX等轉(zhuǎn)化為原生Javascript
自己動手寫插件presets預(yù)設(shè)就是關(guān)于一系列插件的集合,presets的存在減少了babelrc配置文件的體積,不用看到一大堆的插件數(shù)組,并且保證了每個用戶配置的插件清單一模一樣,所以插件對于babel來說至關(guān)重要,前端開發(fā)者如何開發(fā)一個自定義插件決定了今后對代碼編譯的掌控程度,babel插件就像一把手術(shù)刀對js源碼進行精準、可靠的改裝。
本人在寫練習(xí)寫插件的過程中主要用到了以下兩個方法:
ast explorer
基于babel-core在IDE中編寫代碼
引用babel-core模塊進行編碼方式如下:
const {transform,generate}=require("babel-core"); const myPlugin=require("./myPlugin"); const code = `d = a + b + c`; var es5Code = transform(code, { plugins: [myPlugin] }) console.log(es5Code.code);ast explorer
本人比較青睞的babel插件在線編寫方式,可以實時看到編譯后的結(jié)果以及對應(yīng)的AST部分,結(jié)合babel-types可以很快的寫出手術(shù)刀式的插件,下面這張圖是ast explorer解析出來的json:
export default function (babel) { const {types:t}=babel; return { name: "可有可無的插件名字", visitor: { VariableDeclaration(path,state){ console.log(path); } }, }; }
每一個插件都要返回帶有visitor字段的對象,而visitor對象中存放你的遍歷方法,本人總結(jié)為等價于上面ast explorer截圖中的type屬性(例如:VariableDeclaration),遍歷方法是指插件根據(jù)遍歷方法讓ast中的節(jié)點走進你寫的遍歷方法函數(shù)中。遍歷方法就像js中的addeventlistener,可以重復(fù)寫多個監(jiān)聽函數(shù),所以當多個插件疊合在一起就會出現(xiàn)一些不可預(yù)料的事情,這是考驗?zāi)悴寮帉懯欠癜踩⒖煽康氖虑椋彩亲铍y的部分。
舉一個最簡單的例子,如何刪除代碼中的所有console?
let a=33; console.log(12121212); var b; console.warn(12121212); aaaa,cccc console.error(12121212); dd=0; let c;
export default function ({types:t}) { return { name: "刪除所有的console", visitor: { CallExpression(path,state){ if(path.get("callee").isMemberExpression()){ if(path.get("callee").get("object").isIdentifier()){ if(path.get("callee").get("object").get("name").node=="console")path.remove() } } } }, }; }
CallExpression遍歷方法也就是console.log(...)對應(yīng)的AST type屬性,當走進CallExpression函數(shù)后,我們可以獲取path和state兩個參數(shù),path包含了當前節(jié)點的相關(guān)信息,按照前端的思維可以理解為dom節(jié)點,可以往上或者往下查找節(jié)點,當前節(jié)點path包含了很多信息,方便我們編寫插件,而state中包含了插件的options和數(shù)據(jù),options就是babelrc中plugins引入插件時,添加的options,在state中可以接收到它。
剛開始寫插件的時候,完全當成dom節(jié)點直接獲取節(jié)點中的信息是非常危險的(我也是看了babel多個插件后知道的),每往下取一個信息時都要去判斷這個類型是否跟我們的ast樹一樣,這樣就可以去除掉其他的情況,例如其他的CallExpression也走到這個函數(shù)中了,但是它可能并沒有callee或者object,代碼執(zhí)行到這邊就會出錯或者誤傷,嚴謹?shù)目刂乒?jié)點獲取流程將會幫助我們省去很多不必要的麻煩。
代碼中獲取callee節(jié)點可以有兩種方式,一種是path.node.callee,還有一種是path.get("callee"),個人比較喜歡后者,因為可以直接調(diào)用方法(例如isMemberExpression),否則你就要像這樣去判斷t.isMemberExpression(path.node.callee),不夠優(yōu)雅。
當我們條件判斷到當前node是console,直接用remove方法就可以刪除ast節(jié)點了,編譯后的代碼:
let a=33; var b; aaaa,cccc dd=0; let c;
babel官方已經(jīng)發(fā)布了一個刪除console的插件,可以對比下發(fā)現(xiàn),思路和步驟基本一致,babel官方開發(fā)的更加全面,考慮了其他兩個情況。
插件編寫第二站 -- 作用域的影響function a(n){ n*n } let n=1
考慮下如何改寫函數(shù)中n變成_n?
export default function ({ types: t }) { let paramsName=""; return { name: "給function中的參數(shù)加上下劃線", visitor: { FunctionDeclaration(path) { if(!path.get("params").length||!path.get("params")[0])return; paramsName=path.get("params")[0].get("name").node; path.traverse({ Identifier(path){ if(path.get("name").node==paramsName)path.replaceWith(t.Identifier("_"+paramsName)); } }); }, } }; }
按照第一個例子的思路,我們很容易就可以把n給改成_n,但是這時候fucntion外面的let n=1,也會被改寫,所以我們在FunctionDeclaration方法中調(diào)用了path.traverse,把需要遍歷的方法Identifier包裹在其中,這樣就保護了外面代碼的安全,這種方式保證了插件編寫的安全性
插件編寫第三站 -- bindingsconst aaaa=1; const bb=4; function b(){ let aaaa=2; aaaa=3; } aaaa=34;
讓我們來接著做另外一個例子,如何將const改成var,并且對const聲明的值給予只讀保護?
export default function (babel, options) { return { name: "const polyfill", visitor: { VariableDeclaration(path) { if(path.get("kind").node!="const")return; path.node.kind="var"; }, ExpressionStatement(path){ if(!path.get("expression").isAssignmentExpression())return; let nodeleft=path.get("expression").get("left"); if(!nodeleft.isIdentifier())return; if(path.scope.bindings[nodeleft.get("name").node].kind=="const")console.error("Assignment to constant variable"); } }, }; }
VariableDeclaration方法中將const改成了let,ExpressionStatement方法中用來觀察const的變量是否被修改,由于function有自己的作用域,所以aaaa可以被重新聲明和修改,這里用到了bindings屬性,可以查看該節(jié)點的變量申明類型,當發(fā)現(xiàn)kind為const時才發(fā)出error警告,這個例子是對bindings的一次應(yīng)用。
插件編寫第四站 -- 創(chuàng)建節(jié)點當我們替換一個節(jié)點或者插入一個節(jié)點到容器中,我們需要按照節(jié)點的構(gòu)建規(guī)則來創(chuàng)建,下面的例子是將n*n修改成n+100
function square(n) { return n * n; }
先給出答案,代碼如下:
export default function ({types:t}) { return { name: "將n*n修改成n+100", visitor: { BinaryExpression(path){ path.replaceWith(t.binaryExpression("+", path.node.left, t.Identifier("100"))); path.stop(); } }, }; }
現(xiàn)在我們要把BinaryExpression這個type的節(jié)點給替換掉,就要按照BinaryExpression節(jié)點的規(guī)則來創(chuàng)建,可以參考babel-types網(wǎng)站的說明文檔:
我們需要分別構(gòu)建operator、left、right這三種類型的節(jié)點,再查看ast中對這三個節(jié)點的描述
OK,left和right都是Identifier類型,而operator是字符串,字符串直接寫入“+”就可以替換掉了,而Identifier類型節(jié)點的創(chuàng)建還要查看babel-types給出的文檔:
我們只要給出string類型的name就可以了,所以我們可以成功創(chuàng)建自己的節(jié)點了。
總結(jié)ast explorer真的是一個很好的網(wǎng)站,并且可以在插件中寫console,可以在控制臺中實時看到console的結(jié)果,對我們理解ast節(jié)點用很大的幫助,另外以上介紹插件的例子還是太少,插件編寫要注意的遠不止這些方面,但是本人沒時間想出那么多的例子來很好的介紹,所以大家可以直接閱讀這篇文檔來深入了解。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/89342.html
摘要:接著上一篇文章深入了解一的處理步驟的三個主要處理步驟分別是解析,轉(zhuǎn)換,生成。模塊是的代碼生成器,它讀取并將其轉(zhuǎn)換為代碼和源碼映射抽象語法樹抽象語法樹在以上三個神器中都出現(xiàn)過,所以對于編譯器來說至關(guān)重要。 接著上一篇文章《深入了解babel(一)》 Babel 的處理步驟 Babel 的三個主要處理步驟分別是: 解析(parse),轉(zhuǎn)換(transform),生成(generate)。對...
摘要:目前羅列的只是的情況。例如,包含了。的執(zhí)行過程是,首先讀取配置中的條件,根據(jù)這些條件從模塊可得出該條件下的所有瀏覽器最低版本號列表,而又為的轉(zhuǎn)譯插件提供了瀏覽器的最低版本號列表,兩個瀏覽器版本號列表的查詢可得出一個轉(zhuǎn)譯插件的集合。 babel的定義 Babel 是 JavaScript 編譯器,更確切地說是源碼到源碼的編譯器,通常也叫做轉(zhuǎn)換編譯器(transpiler)。 babel-...
摘要:目前羅列的只是的情況。例如,包含了。的執(zhí)行過程是,首先讀取配置中的條件,根據(jù)這些條件從模塊可得出該條件下的所有瀏覽器最低版本號列表,而又為的轉(zhuǎn)譯插件提供了瀏覽器的最低版本號列表,兩個瀏覽器版本號列表的查詢可得出一個轉(zhuǎn)譯插件的集合。 babel的定義 Babel 是 JavaScript 編譯器,更確切地說是源碼到源碼的編譯器,通常也叫做轉(zhuǎn)換編譯器(transpiler)。 babel-...
閱讀 914·2021-11-22 13:54
閱讀 2843·2021-09-28 09:36
閱讀 2980·2019-08-30 15:55
閱讀 1952·2019-08-30 15:44
閱讀 544·2019-08-29 12:31
閱讀 2564·2019-08-28 18:18
閱讀 1199·2019-08-26 13:58
閱讀 1383·2019-08-26 13:44