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