如何用 Babel 為代碼自動(dòng)引入依賴
前言
最近在嘗試玩一玩已經(jīng)被大家玩膩的?Babel,今天給大家分享如何用?Babel?為代碼自動(dòng)引入依賴,通過一個(gè)簡(jiǎn)單的例子入門?Babel?插件開發(fā)。
需求
const?a?=?require('a');
import?b?from?'b';
console.log(axuebin.say('hello?babel'));
同學(xué)們都知道,如果運(yùn)行上面的代碼,一定是會(huì)報(bào)錯(cuò)的:
VM105:2?Uncaught?ReferenceError:?axuebin?is?not?defined
我們得首先通過?import axuebin from 'axuebin'?引入?axuebin?之后才能使用。。
為了防止這種情況發(fā)生(一般來說我們都會(huì)手動(dòng)引入),或者為你省去引入這個(gè)包的麻煩(其實(shí)有些編譯器也會(huì)幫我們做了),我們可以在打包階段分析每個(gè)代碼文件,把這個(gè)事情做了。
在這里,我們就基于最簡(jiǎn)單的場(chǎng)景做最簡(jiǎn)單的處理,在代碼文件頂部加一句引用語句:
import?axuebin?from?'axuebin';
console.log(axuebin.say('hello?babel'));
前置知識(shí)
什么是 Babel
簡(jiǎn)單地說,Babel?能夠轉(zhuǎn)譯?ECMAScript 2015+?的代碼,使它在舊的瀏覽器或者環(huán)境中也能夠運(yùn)行。我們?nèi)粘i_發(fā)中,都會(huì)通過?webpack?使用?babel-loader?對(duì)?JavaScript?進(jìn)行編譯。
Babel 是如何工作的
首先得要先了解一個(gè)概念:抽象語法樹(Abstract Syntax Tree, AST),Babel?本質(zhì)上就是在操作?AST?來完成代碼的轉(zhuǎn)譯。
了解了?AST?是什么樣的,就可以開始研究?Babel?的工作過程了。
Babel?的功能其實(shí)很純粹,它只是一個(gè)編譯器。
大多數(shù)編譯器的工作過程可以分為三部分,如圖所示:

Parse(解析)?將源代碼轉(zhuǎn)換成更加抽象的表示方法(例如抽象語法樹) Transform(轉(zhuǎn)換)?對(duì)(抽象語法樹)做一些特殊處理,讓它符合編譯器的期望 Generate(代碼生成)?將第二步經(jīng)過轉(zhuǎn)換過的(抽象語法樹)生成新的代碼
所以我們?nèi)绻胍薷?Code,就可以在?Transform?階段做一些事情,也就是操作?AST。
AST 節(jié)點(diǎn)
我們可以看到?AST?中有很多相似的元素,它們都有一個(gè)?type?屬性,這樣的元素被稱作節(jié)點(diǎn)。一個(gè)節(jié)點(diǎn)通常含有若干屬性,可以用于描述?AST?的部分信息。
比如這是一個(gè)最常見的?Identifier?節(jié)點(diǎn):
{
??type:?'Identifier',
??name:?'add'
}
所以,操作?AST?也就是操作其中的節(jié)點(diǎn),可以增刪改這些節(jié)點(diǎn),從而轉(zhuǎn)換成實(shí)際需要的?AST。
更多的節(jié)點(diǎn)規(guī)范可以查閱?https://github.com/estree/estree[1]
AST 遍歷
AST?是深度優(yōu)先遍歷的,遍歷規(guī)則不用我們自己寫,我們可以通過特定的語法找到的指定的節(jié)點(diǎn)。
Babel?會(huì)維護(hù)一個(gè)稱作?Visitor?的對(duì)象,這個(gè)對(duì)象定義了用于?AST?中獲取具體節(jié)點(diǎn)的方法。
一個(gè)?Visitor?一般是這樣:
const?visitor?=?{
??ArrowFunction(path)?{
????console.log('我是箭頭函數(shù)');
??},
??IfStatement(path)?{
????console.log('我是一個(gè)if語句');
??},
??CallExpression(path)?{}
};
visitor?上掛載以節(jié)點(diǎn)?type?命名的方法,當(dāng)遍歷?AST?的時(shí)候,如果匹配上?type,就會(huì)執(zhí)行對(duì)應(yīng)的方法。
操作 AST 的例子
通過上面簡(jiǎn)單的介紹,我們就可以開始任意造作了,肆意修改?AST?了。先來個(gè)簡(jiǎn)單的例子熱熱身。
箭頭函數(shù)是?ES5?不支持的語法,所以?Babel?得把它轉(zhuǎn)換成普通函數(shù),一層層遍歷下去,找到了?ArrowFunctionExpression?節(jié)點(diǎn),這時(shí)候就需要把它替換成?FunctionDeclaration?節(jié)點(diǎn)。所以,箭頭函數(shù)可能是這樣處理的:
import?*?as?t?from?"@babel/types";
const?visitor?=?{
??ArrowFunction(path)?{
????path.replaceWith(t.FunctionDeclaration(id,?params,?body));
??}
};
開發(fā) Babel 插件的前置工作
在開始寫代碼之前,我們還有一些事情要做一下:
分析 AST
將原代碼和目標(biāo)代碼都解析成?AST,觀察它們的特點(diǎn),找找看如何增刪改?AST?節(jié)點(diǎn),從而達(dá)到自己的目的。
我們可以在?https://astexplorer.net[2]?上完成這個(gè)工作,比如文章最初提到的代碼:
const?a?=?require('a');
import?b?from?'b';
console.log(axuebin.say('hello?babel'));
轉(zhuǎn)換成?AST?之后是這樣的:

可以看出,這個(gè)?body?數(shù)組對(duì)應(yīng)的就是根節(jié)點(diǎn)的三條語句,分別是:
VariableDeclaration:? const a = require('a')ImportDeclaration:? import b from 'b'ExpressionStatement:? console.log(axuebin.say('hello babel'))
我們可以打開?VariableDeclaration?節(jié)點(diǎn)看看:

它包含了一個(gè)?declarations?數(shù)組,里面有一個(gè)?VariableDeclarator?節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)有?type、id、init?等信息,其中?id?指的是表達(dá)式聲明的變量名,init?指的是聲明內(nèi)容。
通過這樣查看/對(duì)比?AST?結(jié)構(gòu),就能分析出原代碼和目標(biāo)代碼的特點(diǎn),然后可以開始動(dòng)手寫程序了。
查看節(jié)點(diǎn)規(guī)范
節(jié)點(diǎn)規(guī)范:https://github.com/estree/estree[3]
我們要增刪改節(jié)點(diǎn),當(dāng)然要知道節(jié)點(diǎn)的一些規(guī)范,比如新建一個(gè)?ImportDeclaration?需要傳遞哪些參數(shù)。
寫代碼
準(zhǔn)備工作都做好了,那就開始吧。
初始化代碼
我們的?index.js?代碼為:
//?index.js
const?path?=?require('path');
const?fs?=?require('fs');
const?babel?=?require('@babel/core');
const?TARGET_PKG_NAME?=?'axuebin';
function?transform(file)?{
??const?content?=?fs.readFileSync(file,?{
????encoding:?'utf8',
??});
??const?{?code?}?=?babel.transformSync(content,?{
????sourceMaps:?false,
????plugins:?[
??????babel.createConfigItem(({?types:?t?})?=>?({
????????visitor:?{
????????}
??????}))
????]
??});
??return?code;
}
然后我們準(zhǔn)備一個(gè)測(cè)試文件?test.js,代碼為:
//?test.js
const?a?=?require('a');
import?b?from?'b';
require('c');
import?'d';
console.log(axuebin.say('hello?babel'));
分析 AST / 編寫對(duì)應(yīng) type 代碼
我們這次需要做的事情很簡(jiǎn)單,做兩件事:
尋找當(dāng)前? AST?中是否含有引用?axuebin?包的節(jié)點(diǎn)如果沒引用,則修改? AST,插入一個(gè)?ImportDeclaration?節(jié)點(diǎn)
我們來分析一下?test.js?的?AST,看一下這幾個(gè)節(jié)點(diǎn)有什么特征:
ImportDeclaration 節(jié)點(diǎn)

ImportDeclaration?節(jié)點(diǎn)的?AST?如圖所示,我們需要關(guān)心的特征是?value?是否等于?axuebin, 代碼這樣寫:
if?(path.isImportDeclaration())?{
??return?path.get('source').isStringLiteral()?&&?path.get('source').node.value?===?TARGET_PKG_NAME;
}
其中,可以通過?path.get?來獲取對(duì)應(yīng)節(jié)點(diǎn)的?path,嗯,比較規(guī)范。如果想獲取對(duì)應(yīng)的真實(shí)節(jié)點(diǎn),還需要?.node。
滿足上述條件則可以認(rèn)為當(dāng)前代碼已經(jīng)引入了?axuebin?包,不用再做處理了。
VariableDeclaration 節(jié)點(diǎn)

對(duì)于?VariableDeclaration?而言,我們需要關(guān)心的特征是,它是否是一個(gè)?require?語句,并且?require?的是?axuebin,代碼如下:
/**
?*?判斷是否?require?了正確的包
?*?@param?{*}?node?節(jié)點(diǎn)
?*/
const?isTrueRequire?=?node?=>?{
??const?{?callee,?arguments?}?=?node;
??return?callee.name?===?'require'?&&?arguments.some(item?=>?item.value?===?TARGET_PKG_NAME);
};
if?(path.isVariableDeclaration())?{
??const?declaration?=?path.get('declarations')[0];
??return?declaration.get('init').isCallExpression?&&?isTrueRequire(declaration.get('init').node);
}
ExpressionStatement 節(jié)點(diǎn)

require('c'),語句我們一般不會(huì)用到,我們也來看一下吧,它對(duì)應(yīng)的是?ExpressionStatement?節(jié)點(diǎn),我們需要關(guān)心的特征和?VariableDeclaration?一致,這也是我把?isTrueRequire?抽出來的原因,所以代碼如下:
if?(path.isExpressionStatement())?{
??return?isTrueRequire(path.get('expression').node);
}
插入引用語句
如果上述分析都沒找到代碼里引用了?axuebin,我們就需要手動(dòng)插入一個(gè)引用:
import?axuebin?from?'axuebin';
通過?AST?分析,我們發(fā)現(xiàn)它是一個(gè)?ImportDeclaration:

簡(jiǎn)化一下就是這樣:
{
??"type":?"ImportDeclaration",
??"specifiers":?[
????"type":?"ImportDefaultSpecifier",
????"local":?{
??????"type":?"Identifier",
??????"name":?"axuebin"
????}
??],
??"source":?{
????"type":?"StringLiteral",
????"value":?"axuebin"
??}
}
當(dāng)然,不是直接構(gòu)建這個(gè)對(duì)象放進(jìn)去就好了,需要通過?babel?的語法來構(gòu)建這個(gè)節(jié)點(diǎn)(遵循規(guī)范):
const?importDefaultSpecifier?=?[t.ImportDefaultSpecifier(t.Identifier(TARGET_PKG_NAME))];
const?importDeclaration?=?t.ImportDeclaration(importDefaultSpecifier,?t.StringLiteral(TARGET_PKG_NAME));
path.get('body')[0].insertBefore(importDeclaration);
這樣就插入了一個(gè)?import?語句。
Babel Types?模塊是一個(gè)用于?AST?節(jié)點(diǎn)的?Lodash?式工具庫(kù),它包含了構(gòu)造、驗(yàn)證以及變換?AST?節(jié)點(diǎn)的方法。
結(jié)果
我們?node index.js?一下,test.js?就變成:
import?axuebin?from?"axuebin";?//?已經(jīng)自動(dòng)加在代碼最上邊
const?a?=?require('a');
import?b?from?'b';
require('c');
import?'d';
console.log(axuebin.say('hello?babel'));
彩蛋
如果我們還想幫他再多做一點(diǎn)事,還能做什么呢?
既然都自動(dòng)引用了,那當(dāng)然也要自動(dòng)安裝一下這個(gè)包呀!
/**
?*?判斷是否安裝了某個(gè)包
?*?@param?{string}?pkg?包名
?*/
const?hasPkg?=?pkg?=>?{
??const?pkgPath?=?path.join(process.cwd(),?`package.json`);
??const?pkgJson?=?fs.existsSync(pkgPath)???fse.readJsonSync(pkgPath)?:?{};
??const?{?dependencies?=?{},?devDependencies?=?{}?}?=?pkgJson;
??return?dependencies[pkg]?||?devDependencies[pkg];
}
/**
?*?通過?npm?安裝包
?*?@param?{string}?pkg?包名
?*/
const?installPkg?=?pkg?=>?{
??console.log(`開始安裝?${pkg}`);
??const?npm?=?shell.which('npm');
??if?(!npm)?{
????console.log('請(qǐng)先安裝?npm');
????return;
??}
??const?{?code?}?=?shell.exec(`${npm.stdout}?install?${pkg}?-S`);
??if?(code)?{
????console.log(`安裝?${pkg}?失敗,請(qǐng)手動(dòng)安裝`);
??}
};
//?biu~
if?(!hasPkg(TARGET_PKG_NAME))?{
??installPkg(TARGET_PKG_NAME);
}
判斷一個(gè)應(yīng)用是否安裝了某個(gè)依賴,有沒有更好的辦法呢?
總結(jié)
我也是剛開始學(xué)?Babel,希望通過這個(gè)?Babel?插件的入門例子,可以讓大家了解?Babel?其實(shí)并沒有那么陌生,大家都可以玩起來 ~
完整代碼見:https://github.com/axuebin/babel-inject-dep-demo[4]
Babel 用戶手冊(cè)[5] Babel 插件手冊(cè)[6] ast 分析[7] 節(jié)點(diǎn)規(guī)范[8]
參考資料
https://github.com/estree/estree:?https://github.com/estree/estree
[2]?https://astexplorer.net:?https://astexplorer.net
[3]?https://github.com/estree/estree:?https://github.com/estree/estree
[4]?https://github.com/axuebin/babel-inject-dep-demo:?https://github.com/axuebin/babel-inject-dep-demo
[5]?Babel 用戶手冊(cè):?https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/user-handbook.md
[6]?Babel 插件手冊(cè):?https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
[7]?ast 分析:?https://astexplorer.net/
[8]?節(jié)點(diǎn)規(guī)范:?https://github.com/estree/estree
最后
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

