本文將講解babel是如何運行的般婆,AST的結(jié)構(gòu)弧哎,以及怎么創(chuàng)建一個babel的插件。
再講babel之前淳蔼,先不講babel侧蘸,AST的這些概念,先帶你實現(xiàn)一個簡易的babel解析器鹉梨,這樣再回過頭來講這些概念就容易理解多了讳癌。
tiny-compiler 編譯器
想象一下我們有一些新特性的語法,其中add
subtract
是普通的函數(shù)名存皂,需要轉(zhuǎn)義到正常的javascript語法晌坤,以便讓瀏覽器能夠兼容的運行。
(add 2 2)
要轉(zhuǎn)義成如下
add(2, 2)
編譯器都分為三個步驟:
Parsing
解析Transformation
轉(zhuǎn)義Code Generation
代碼生成
Parsing 解析
Parsing
階段分成兩個子階段旦袋,
Lexical Analysis
詞法分析Syntactic Analysis
語法分析骤菠,先寫好我們要轉(zhuǎn)化的代碼
// 這是我們要轉(zhuǎn)化的code
Lexical Analysis 詞法分析
Lexical Analysis
詞法分析可以理解為把代碼拆分成最小的獨立的語法單元,去描述每一個語法疤孕,可以是操作符商乎,數(shù)字,標(biāo)點符號等祭阀,最后生成token數(shù)組鹉戚。
// 第一步,Lexical Analysis柬讨,轉(zhuǎn)化成tokens類似如下
那我們開始實現(xiàn)它吧崩瓤,干!
function tokenizer(input) {
Syntactic Analysis 語法分析
Syntactic Analysis
語法分析就是根據(jù)上一步的tokens數(shù)組轉(zhuǎn)化成語法之前的關(guān)系踩官,這就是Abstract Syntax Tree
,也就是我們常說的AST
却桶。
// 第二步,Syntactic Analysis,轉(zhuǎn)化成AST類似如下
我們再來實現(xiàn)一個parser颖系,轉(zhuǎn)化成AST嗅剖。
function parser(tokens) {
從上述代碼來看,跟階段AST是根節(jié)點是type=Program
嘁扼,body是一個嵌套的AST數(shù)組結(jié)構(gòu)信粮。再單獨處理了number和string類型之后,再遞歸的調(diào)用walk函數(shù)趁啸,以解決嵌套的括號表達式强缘。
Transformation 轉(zhuǎn)義
traverser 遍歷器
我們最終的目的肯定是想轉(zhuǎn)化成我們想要的代碼,那怎么轉(zhuǎn)化呢不傅?答案就是更改我們剛剛得到的AST結(jié)構(gòu)旅掂。
那怎么去改AST呢?直接去操作這個樹結(jié)構(gòu)肯定是不現(xiàn)實的访娶,所以我們需要遍歷這個AST商虐,利用深度優(yōu)先遍歷的方法遍歷這些節(jié)點,當(dāng)遍歷到某個節(jié)點時崖疤,再去調(diào)用這個節(jié)點對應(yīng)的方法秘车,再方法里面改變這些節(jié)點的值就輕而易舉了。
想象一下我們有這樣的一個visitor
劫哼,就是上文說道的遍歷時調(diào)用的方法
var visitor = {
由于深度優(yōu)先遍歷的特性叮趴,我們遍歷到一個節(jié)點時有enter
和exit
的概念,代表著遍歷一些類似于CallExpression
這樣的節(jié)點時沦偎,這個語句疫向,enter表示開始解析,exit表示解析完畢。比如說上文中:
* -> Program (enter)
然后有一個函數(shù)豪嚎,接受ast
和vistor
作為參數(shù)搔驼,實現(xiàn)遍歷,類似于:
traverse(ast, {
先實現(xiàn)traverser吧侈询。
function traverser(ast, visitor) {
transformer 轉(zhuǎn)換器
有了traverser遍歷器后舌涨,就開始遍歷吧,先看看前后兩個AST
的對比扔字。
這里注意多了一中ExpressionStatement
的type囊嘉,以表示subtract(4, 2)
這樣的結(jié)構(gòu)。
遍歷的過程就是把左側(cè)AST
轉(zhuǎn)化成右側(cè)AST
革为。
function transformer(ast) {
CodeGeneration 代碼生成
那最后一個階段就是用心生成的AST
生成我們最后的代碼了扭粱,也是生成AST
的一個反過程。
function codeGenerator(node) {
總結(jié)
這樣我們一個tiny-compiler就寫好了震檩,最后可以執(zhí)行下面的代碼去試試啦琢蛤。
從上述代碼中就可以看出來蜓堕,一個代碼轉(zhuǎn)化的過程就把包括了tokenizer
詞法分析階段,parser
預(yù)發(fā)分析階段(AST生成)博其,transformer
轉(zhuǎn)義階段源请,codeGenerator
代碼生成階段蚓炬。那么在寫babel-plugin的時候扁凛,其實就是在寫其中的transformer
病附,其他的部分已經(jīng)被babel完美的實現(xiàn)了。
babel plugin 概念
先上手看一個簡單的babel plugin示例
這個plugin造成的效果:
// 源代碼
就是把所有的bool類型的值轉(zhuǎn)化成 !0 或者 !1峰髓,這是代碼壓縮的時候使用的一個技巧傻寂。
那么逐行來分析這個簡單的plugin。一個plugin就是一個function儿普,入?yún)⒕褪莃abel對象崎逃,這里利用到了babel中types對象,來自于@babel/types這個庫眉孩,然后操作path對象進行節(jié)點替換操作。
path
path
是肯定會用到的一個對象勒葱。我們可以用過path訪問到當(dāng)前節(jié)點浪汪,父節(jié)點,也可以去調(diào)用添加凛虽、更新死遭、移動和刪除節(jié)點有關(guān)的其他很多方法。舉幾個示例
// 訪問當(dāng)前節(jié)點的屬性凯旋,用path.node.property訪問node的屬性
@babel/types
可以理解它為一個工具庫呀潭,類似于Lodash
,里面封裝了非常多的幫做方法至非,一般用處如下
- 檢查節(jié)點 一般在類型前面加
is
就是判斷是否該類型
// 判斷當(dāng)前節(jié)點的left節(jié)點是否是identifier類型
- 構(gòu)建節(jié)點
直接手寫復(fù)雜的AST
結(jié)構(gòu)是不現(xiàn)實的钠署,所以有了一些幫助方法去構(gòu)建這些節(jié)點,示例:
// 調(diào)用binaryExpression和identifier的構(gòu)建方法荒椭,生成ast
其中每一種節(jié)點都有自己的構(gòu)造方法谐鼎,都有自己特定的入?yún)ⅲ敿氄垍⒖脊俜轿臋n
scope
最后講一下作用域的概念趣惠,每一個函數(shù)狸棍,每一個變量都有自己的作用域,在編寫babel plugin的時候要特別小心味悄,再改變或者添加代碼的時候要注意不要破壞了原有的代碼結(jié)構(gòu)草戈。
用path.scope
中的一些方法可以操作作用域,示例:
// 檢查變量n是否被綁定(是否在上下文已經(jīng)有引用)
plugin實戰(zhàn)
寫一個自定義plugin是什么步驟呢侍瑟?
這個plugin用來干嘛
源代碼的AST
轉(zhuǎn)換后代碼的AST
tip: 可以去這個網(wǎng)站查看代碼的AST唐片。
plugin的目的
現(xiàn)在就做一個自定義的plugin,大家在應(yīng)用寫代碼的時候可以通過webpack配置alias,比如說配置@
-> ./src
牵触,這樣import的時候就直接從src目錄下找所需要的代碼了淮悼,那么大家有在寫組件的時候用過這個功能嗎?這就是我們這個plugin的目的揽思。
代碼
我們有如下配置
"alias": {
源代碼以及要轉(zhuǎn)化的代碼如下:
// ./src/index.js
AST
源碼的AST展示如下
那我們看見是不是只需要找到ImportDeclaration
節(jié)點中將source
改成轉(zhuǎn)換之后的代碼是不是就可以了袜腥。
開始寫plugin
const localPath = require('path');
用plugin
回到我們的babel配置文件中來,這里我們用的是babel.config.json
{
這樣一個plugin的流程就走完了钉汗,歡迎大家多多交流羹令。