最近在嘗試玩一玩已經被你們玩膩的 Babel
,今天給你們分享「如何用 Babel
爲代碼自動引入依賴」,經過一個簡單的例子入門 Babel
插件開發。前端
const a = require('a');
import b from 'b';
console.log(axuebin.say('hello babel'));
複製代碼
同窗們都知道,若是運行上面的代碼,必定是會報錯的:node
VM105:2 Uncaught ReferenceError: axuebin is not defined
複製代碼
咱們得首先經過 import axuebin from 'axuebin'
引入 axuebin
以後才能使用。。webpack
爲了防止這種狀況發生(通常來講咱們都會手動引入),或者爲你省去引入這個包的麻煩(其實有些編譯器也會幫咱們作了),咱們能夠在打包階段分析每一個代碼文件,把這個事情作了。git
在這裏,咱們就基於最簡單的場景作最簡單的處理,在代碼文件頂部加一句引用語句:github
import axuebin from 'axuebin';
console.log(axuebin.say('hello babel'));
複製代碼
簡單地說,Babel
可以轉譯 ECMAScript 2015+
的代碼,使它在舊的瀏覽器或者環境中也可以運行。咱們平常開發中,都會經過 webpack
使用 babel-loader
對 JavaScript
進行編譯。web
首先得要先了解一個概念:抽象語法樹(Abstract Syntax Tree, AST),Babel
本質上就是在操做 AST
來完成代碼的轉譯。shell
瞭解了 AST
是什麼樣的,就能夠開始研究 Babel
的工做過程了。express
Babel
的功能其實很純粹,它只是一個編譯器。npm
大多數編譯器的工做過程能夠分爲三部分,如圖所示:json
因此咱們若是想要修改 Code
,就能夠在 Transform
階段作一些事情,也就是操做 AST
。
咱們能夠看到 AST
中有不少類似的元素,它們都有一個 type
屬性,這樣的元素被稱做「節點」。一個節點一般含有若干屬性,能夠用於描述 AST
的部分信息。
好比這是一個最多見的 Identifier
節點:
{
type: 'Identifier',
name: 'add'
}
複製代碼
因此,操做 AST
也就是操做其中的節點,能夠增刪改這些節點,從而轉換成實際須要的 AST
。
更多的節點規範能夠查閱 github.com/estree/estr…
AST
是深度優先遍歷的,遍歷規則不用咱們本身寫,咱們能夠經過特定的語法找到的指定的節點。
Babel
會維護一個稱做 Visitor
的對象,這個對象定義了用於 AST
中獲取具體節點的方法。
一個 Visitor
通常是這樣:
const visitor = {
ArrowFunction(path) {
console.log('我是箭頭函數');
},
IfStatement(path) {
console.log('我是一個if語句');
},
CallExpression(path) {}
};
複製代碼
visitor
上掛載以節點 type
命名的方法,當遍歷 AST
的時候,若是匹配上 type
,就會執行對應的方法。
經過上面簡單的介紹,咱們就能夠開始任意造做了,肆意修改 AST
了。先來個簡單的例子熱熱身。
箭頭函數是 ES5
不支持的語法,因此 Babel
得把它轉換成普通函數,一層層遍歷下去,找到了 ArrowFunctionExpression
節點,這時候就須要把它替換成 FunctionDeclaration
節點。因此,箭頭函數多是這樣處理的:
import * as t from "@babel/types";
const visitor = {
ArrowFunction(path) {
path.replaceWith(t.FunctionDeclaration(id, params, body));
}
};
複製代碼
在開始寫代碼以前,咱們還有一些事情要作一下:
將「原代碼」和「目標代碼」都解析成 AST
,觀察它們的特色,找找看如何「增刪改」 AST
節點,從而達到本身的目的。
咱們能夠在 astexplorer.net 上完成這個工做,好比文章最初提到的代碼:
const a = require('a');
import b from 'b';
console.log(axuebin.say('hello babel'));
複製代碼
轉換成 AST
以後是這樣的:
能夠看出,這個 body
數組對應的就是根節點的三條語句,分別是:
const a = require('a')
import b from 'b'
console.log(axuebin.say('hello babel'))
咱們能夠打開 VariableDeclaration
節點看看:
它包含了一個 declarations
數組,裏面有一個 VariableDeclarator
節點,這個節點有 type
、id
、init
等信息,其中 id
指的是表達式聲明的變量名,init
指的是聲明內容。
經過這樣查看/對比 AST
結構,就能分析出「原代碼」和「目標代碼」的特色,而後能夠開始動手寫程序了。
咱們要「增刪改」節點,固然要知道節點的一些規範,好比新建一個 ImportDeclaration
須要傳遞哪些參數。
準備工做都作好了,那就開始吧。
咱們的 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;
}
複製代碼
而後咱們準備一個測試文件 test.js
,代碼爲:
// test.js
const a = require('a');
import b from 'b';
require('c');
import 'd';
console.log(axuebin.say('hello babel'));
複製代碼
咱們此次須要作的事情很簡單,作兩件事:
AST
中是否含有引用
axuebin
包的節點
AST
,插入一個
ImportDeclaration
節點
咱們來分析一下 test.js
的 AST
,看一下這幾個節點有什麼特徵:
ImportDeclaration
節點的 AST
如圖所示,咱們須要關心的特徵是 value
是否等於 axuebin
, 代碼這樣寫:
if (path.isImportDeclaration()) {
return path.get('source').isStringLiteral() && path.get('source').node.value === TARGET_PKG_NAME;
}
複製代碼
其中,能夠經過 path.get
來獲取對應節點的 path
,嗯,比較規範。若是想獲取對應的真實節點,還須要 .node
。
知足上述條件則能夠認爲當前代碼已經引入了 axuebin
包,不用再作處理了。
對於 VariableDeclaration
而言,咱們須要關心的特徵是,它是不是一個 require
語句,而且 require
的是 axuebin
,代碼以下:
/**
* 判斷是否 require 了正確的包
* @param {*} node 節點
*/
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);
}
複製代碼
require('c')
,語句咱們通常不會用到,咱們也來看一下吧,它對應的是 ExpressionStatement
節點,咱們須要關心的特徵和 VariableDeclaration
一致,這也是我把 isTrueRequire
抽出來的緣由,因此代碼以下:
if (path.isExpressionStatement()) {
return isTrueRequire(path.get('expression').node);
}
複製代碼
若是上述分析都沒找到代碼裏引用了 axuebin
,咱們就須要手動插入一個引用:
import axuebin from 'axuebin';
複製代碼
經過 AST
分析,咱們發現它是一個 ImportDeclaration
:
簡化一下就是這樣:
{
"type": "ImportDeclaration",
"specifiers": [
"type": "ImportDefaultSpecifier",
"local": {
"type": "Identifier",
"name": "axuebin"
}
],
"source": {
"type": "StringLiteral",
"value": "axuebin"
}
}
複製代碼
固然,不是直接構建這個對象放進去就行了,須要經過 babel
的語法來構建這個節點(遵循規範):
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);
複製代碼
這樣就插入了一個 import
語句。
Babel Types
模塊是一個用於AST
節點的Lodash
式工具庫,它包含了構造、驗證以及變換AST
節點的方法。
咱們 node index.js
一下,test.js
就變成:
import axuebin from "axuebin"; // 已經自動加在代碼最上邊
const a = require('a');
import b from 'b';
require('c');
import 'd';
console.log(axuebin.say('hello babel'));
複製代碼
若是咱們還想幫他再多作一點事,還能作什麼呢?
「既然都自動引用了,那固然也要自動安裝一下這個包呀!」
/**
* 判斷是否安裝了某個包
* @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('請先安裝 npm');
return;
}
const { code } = shell.exec(`${npm.stdout} install ${pkg} -S`);
if (code) {
console.log(`安裝 ${pkg} 失敗,請手動安裝`);
}
};
// biu~
if (!hasPkg(TARGET_PKG_NAME)) {
installPkg(TARGET_PKG_NAME);
}
複製代碼
判斷一個應用是否安裝了某個依賴,有沒有更好的辦法呢?
我也是剛開始學 Babel
,但願經過這個 Babel
插件的入門例子,可讓你們瞭解 Babel
其實並無那麼陌生,你們均可以玩起來 ~
完整代碼見:github.com/axuebin/bab…
歡迎關注公衆號「前端試煉」,公衆號平時會分享一些實用或者有意思的東西,發現代碼之美。專一深度和最佳實踐,但願打造一個高質量的公衆號。偶爾還會分享一些攝影 ~
公衆號後臺回覆「加羣」,拉你進交流划水聊天羣,有看到好文章/代碼都會發在羣裏。
若是你不想加羣,只是想加我也是能夠。