【Babel 小玩具】如何用 Babel 爲代碼自動引入依賴

前言

最近在嘗試玩一玩已經被你們玩膩的 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

簡單地說,Babel 可以轉譯 ECMAScript 2015+ 的代碼,使它在舊的瀏覽器或者環境中也可以運行。咱們平常開發中,都會經過 webpack 使用 babel-loaderJavaScript 進行編譯。web

Babel 是如何工做的

首先得要先了解一個概念:抽象語法樹(Abstract Syntax Tree, AST),Babel 本質上就是在操做 AST 來完成代碼的轉譯。shell

瞭解了 AST 是什麼樣的,就能夠開始研究 Babel 的工做過程了。express

Babel 的功能其實很純粹,它只是一個編譯器。npm

大多數編譯器的工做過程能夠分爲三部分,如圖所示:json

  • Parse(解析) 將源代碼轉換成更加抽象的表示方法(例如抽象語法樹)
  • Transform(轉換) 對(抽象語法樹)作一些特殊處理,讓它符合編譯器的指望
  • Generate(代碼生成) 將第二步通過轉換過的(抽象語法樹)生成新的代碼

因此咱們若是想要修改 Code,就能夠在 Transform 階段作一些事情,也就是操做 AST

AST 節點

咱們能夠看到 AST 中有不少類似的元素,它們都有一個 type 屬性,這樣的元素被稱做節點。一個節點一般含有若干屬性,能夠用於描述 AST 的部分信息。

好比這是一個最多見的 Identifier 節點:

{
  type'Identifier',
  name'add'
}
複製代碼

因此,操做 AST 也就是操做其中的節點,能夠增刪改這些節點,從而轉換成實際須要的 AST

更多的節點規範能夠查閱 github.com/estree/estr…

AST 遍歷

AST 是深度優先遍歷的,遍歷規則不用咱們本身寫,咱們能夠經過特定的語法找到的指定的節點。

Babel 會維護一個稱做 Visitor 的對象,這個對象定義了用於 AST 中獲取具體節點的方法。

一個 Visitor 通常是這樣:

const visitor = {
  ArrowFunction(path) {
    console.log('我是箭頭函數');
  },
  IfStatement(path) {
    console.log('我是一個if語句');
  },
  CallExpression(path) {}
};
複製代碼

visitor 上掛載以節點 type 命名的方法,當遍歷 AST 的時候,若是匹配上 type,就會執行對應的方法。

操做 AST 的例子

經過上面簡單的介紹,咱們就能夠開始任意造做了,肆意修改 AST 了。先來個簡單的例子熱熱身。

箭頭函數是 ES5 不支持的語法,因此 Babel 得把它轉換成普通函數,一層層遍歷下去,找到了 ArrowFunctionExpression 節點,這時候就須要把它替換成 FunctionDeclaration 節點。因此,箭頭函數多是這樣處理的:

import * as t from "@babel/types";

const visitor = {
  ArrowFunction(path) {
    path.replaceWith(t.FunctionDeclaration(id, params, body));
  }
};
複製代碼

開發 Babel 插件的前置工做

在開始寫代碼以前,咱們還有一些事情要作一下:

分析 AST

原代碼目標代碼都解析成 AST,觀察它們的特色,找找看如何增刪改 AST 節點,從而達到本身的目的。

咱們能夠在 astexplorer.net 上完成這個工做,好比文章最初提到的代碼:

const a = require('a');
import b from 'b';
console.log(axuebin.say('hello babel'));
複製代碼

轉換成 AST 以後是這樣的:

能夠看出,這個 body 數組對應的就是根節點的三條語句,分別是:

  • VariableDeclaration: const a = require('a')
  • ImportDeclaration: import b from 'b'
  • ExpressionStatement: console.log(axuebin.say('hello babel'))

咱們能夠打開 VariableDeclaration 節點看看:

它包含了一個 declarations 數組,裏面有一個 VariableDeclarator 節點,這個節點有 typeidinit 等信息,其中 id 指的是表達式聲明的變量名,init 指的是聲明內容。

經過這樣查看/對比 AST 結構,就能分析出原代碼目標代碼的特色,而後能夠開始動手寫程序了。

查看節點規範

節點規範:github.com/estree/estr…

咱們要增刪改節點,固然要知道節點的一些規範,好比新建一個 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, {
    sourceMapsfalse,
    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 / 編寫對應 type 代碼

咱們此次須要作的事情很簡單,作兩件事:

  1. 尋找當前 AST 中是否含有引用 axuebin 包的節點
  2. 若是沒引用,則修改 AST,插入一個 ImportDeclaration 節點

咱們來分析一下 test.jsAST,看一下這幾個節點有什麼特徵:

ImportDeclaration 節點

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 節點

對於 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);
}
複製代碼

ExpressionStatement 節點

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…

交流討論

歡迎關注公衆號前端試煉,公衆號平時會分享一些實用或者有意思的東西,發現代碼之美。專一深度和最佳實踐,但願打造一個高質量的公衆號。偶爾還會分享一些攝影 ~

公衆號後臺回覆「加羣」,拉你進交流划水聊天羣,有看到好文章/代碼都會發在羣裏。

若是你不想加羣,只是想加我也是能夠。

相關文章
相關標籤/搜索