深刻淺出 Babel 下篇:既生 Plugin 何生 Macros

接着上篇文章: 《深刻淺出 Babel 上篇:架構和原理 + 實戰 🔥》 歡迎轉載,讓更多人看到個人文章,轉載請註明出處javascript


這篇文章乾貨很多於上篇文章,這篇咱們深刻討論一下宏這個玩意 —— 我想咱們對宏並不陌生,由於不少程序員第一門語言就是 C/C++; 一些 Lisp 方言也支持宏(如 ClojureScheme), 據說它們的宏寫起來很優雅;一些現代的編程語言對宏也有必定的支持,如 RustNimJuliaElixir,它們是如何解決技術問題, 實現類Lisp的宏系統的?宏在這些語言中扮演這什麼角色...html

若是沒讀過上篇文章,請先閱讀一下,避免影響對本篇文章內容的理解。前端


文章大綱vue


關於宏

Wiki 上面對‘宏’的定義是:宏(Macro), 是一種批處理的稱謂,它根據一系列的預約義規則轉換必定的文本模式。解釋器編譯器在遇到宏時會自動進行這一模式轉換,這個轉換過程被稱爲「宏展開(Macro Expansion)」。對於編譯語言,宏展開在編譯時發生,進行宏展開的工具常被稱爲宏展開器。java

你能夠認爲,宏就是用來生成代碼的代碼,它有能力進行一些句法解析和代碼轉換。宏大體能夠分爲兩種: 文本替換語法擴展react


文本替換式

你們或多或少有接觸過宏,不少程序員第一門語言是C/C++(包括C的衍生語言Objective-C), 在C中就有宏的概念。使用#define指令定義一個宏:git

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
複製代碼

若是咱們的程序使用了這個宏,就會在編譯階段被展開,例如:程序員

MIN(a + b, c + d)
複製代碼

會被展開爲:github

((a + b) < (c + d) ? (a + b) : (c + d))
複製代碼

除了函數宏, C 中還有對象宏, 咱們一般使用它來聲明'常量':vue-cli

#define PI 3.1214
複製代碼

如上圖,宏本質上不是C語言的一部分, 它由C預處理器提供,預處理器在編譯以前對源代碼進行文本替換,生成‘真正’的 C 代碼,再傳遞給編譯器。

固然 C 預處理器不只僅會處理宏,它還包含了頭文件引入、條件編譯、行控制等操做

除此以外,GNU m4是一個更專業/更強大/更通用的預處理器(宏展開器)。這是一個通用的宏展開器,不只能夠用於 C,也能夠用於其餘語言和文本文件的處理(參考這篇有趣的文章:《使用 GNU m4 爲 Markdown 添加目錄支持》), 關於m4能夠看讓這世界再多一份 GNU m4 教程 系列文章.

文本替換式宏很容易理解、實現也簡單,由於它們只是純文本替換, 換句話說它就像‘文本編輯器’。因此相對而言,這種形式的宏能力有限,好比它不會檢驗語法是否合法, 使用它常常會出現問題

因此隨着現代編程語言表達能力愈來愈強,不少語言都再也不推薦使用宏/不提供宏,而是使用語言自己的機制(例如函數)來解決問題,這樣更安全、更容易理解和調試。沒用宏機制,現代語言能夠經過提供強大的反射機制或者動態編程特性(如Javascript的Proxy、Python的裝飾器)來彌補缺失宏致使的元編程短板。 因此反過來推導,之因此C語言須要宏,正是由於C語言的表達能力太弱了



語法擴展式

‘真正’的宏起源於Lisp. 這個得益於Lisp語言自己的一些特性:


  • 它的語法很是簡單。只有S-表達式(s-expression)(特徵爲括號化的前綴表示法, 能夠認爲S-表達式就是近似的 Lisp 的抽象語法樹(AST))
  • 數據即代碼。S-表達式自己就是樹形數據結構。另外 Lisp 支持數據和代碼之間的轉換

因爲 Lisp 這種簡單的語法結構,使得數據和程序之間只有一線之隔(quote修飾就是數據, 沒有quote就是程序), 換句話說就是程序和數據之間能夠靈活地轉換。這種數據即程序、程序即數據的概念,使得Lisp能夠輕鬆地自定義宏. 不妨來看一下Lisp定義宏的示例:

; 使用defmacro定義一個nonsense宏, 接收一個function-name參數. 宏須要返回一個quoted
; ` 這是quote函數的簡寫,表示quote,即這段‘程序’是一段‘數據’, 或者說將‘程序’轉換爲‘數據’. quote不會被‘求值’
; defun 定義一個函數
; , 這是unquote函數的簡寫, 表示unquote,即將‘數據’轉換爲‘程序’. unquote會進行求值
; intern 將字符串轉換爲symbol,即標識符

(defmacro nonsense (function-name)
  `(defun ,(intern (concat "nonsense-" function-name)) (input) ; 定義一個nonsense-${function-name} 方法
     (print (concat ,function-name input))))                   ; 輸入`${function-name}${input}`
複製代碼
若是你不理解上面程序的含義,這裏有一個Javascript的實現

注意:‘宏’通常在編譯階段被展開, 下面代碼只是爲了協做你理解上述的Lisp代碼

function nonsense(name) {
  let rtn
  eval(`rtn = function nonsense${name}(input) { console.log('${name}', input) }`)
  return rtn
}
複製代碼

應用宏展開:

(nonsense "apple")           ; 展開宏,這裏會建立一個nonsense-apple函數
(nonsense-apple " is good")  ; 調用剛剛建立的宏
                             ; => "apple is good"
複製代碼

對於Lisp而言,宏有點像一個函數, 只不過這個函數必須返回一個quoted數據; 當調用這個宏時,Lisp會使用unquote函數將宏返回的quoted數據轉換爲程序


經過上面的示例,你會感嘆Lisp的宏實現居然如此清奇,如此簡單。 搞得我想跟着題葉學一波Clojure,可是後來我學了Elixir 😂.


Lisp宏的靈活性得益於簡單的語法(S-表達式能夠等價於它的AST),對於複雜語法的語言(例如Javascript),要實現相似Lisp的宏就可貴多. 所以不多有現代語言提供宏機制可能也是這個緣由。

儘管如此,如今不少技術難點慢慢被解決,不少現代語言也引入'類' Lisp的宏機制,如RustJulia, 還有Javascript的 Sweet.js



Sweet.js

Sweet.js 和 Rust 師出同門,因此兩個的宏語法和很是接近(初期)。 不過須要注意的是: 官方認爲 Sweet.js 目前仍處於實驗階段,並且Github最後提交時間停留在2年前,社區上也未見大規模的使用。因此不要在生產環境中使用它,可是不妨礙咱們去學習一個現代編程語言的宏機制。

咱們先使用 Sweet.js 來實現上面咱們經過 Lisp 實現的nosense宏, 對比起來更容易理解:

import { unwrap, fromIdentifier, fromStringLiteral } from '@sweet-js/helpers' for syntax;

syntax nosense = function (ctx) {
  let name = ctx.next().value;
  let funcName = 'nonsense' + unwrap(name).value

  return #`function ${fromIdentifier(name, funcName)} () {
    console.log(${fromStringLiteral(name, unwrap(name).value)} + input)
  }`;
};

nosense Apple
nosenseApple(" is Good") // Apple is Good
複製代碼

首先,Sweet.js使用syntax關鍵字來定義一個宏,其語法相似於const或者let

本質上一個宏就是一個函數, 只不過在編譯階段被執行. 這個函數接收一個 TransformerContext 對象,你也經過這個對象獲取宏應用傳入的語法對象(Syntax Object)數組,最終這個宏也要返回語法對象數組

什麼是語法對象?語法對象是 Sweet.js 關於語法的內部表示, 你能夠類比上文Lisp的 quoted 數據。在複雜語法的語言中,沒辦法使用 quoted 這麼簡單的序列來表示語法,而使用 AST 則更復雜,開發者更難以駕馭。因此大部分宏實現會參考 Lisp 的S-表達式,取折中方案,將傳入的程序轉換爲Tokens,再組裝成相似quoted的數據結構

舉個例子,Sweet.js 會將 foo,bar('baz', 1)轉換成這樣的數據結構:

從上圖可知,Sweet.js 會將傳入的程序解析成嵌套的Token序列,這個結構和Lisp的S-表達式很是類似。也就是, 說對於閉合的詞法單元會被嵌套存儲,例如上例的('baz', 1).

Elixir 也採用了相似的quote/unquote機制,能夠結合着一塊兒理解


TransformerContext實現了迭代器方法,因此咱們經過調用它的next()來遍歷獲取語法對象。最後宏必須返回一個語法對象數組,Sweet.js 使用了相似字符串模板語法(稱爲語法模板)來簡化開發,這個模板最終轉換爲語法對象數組。

須要注意的是語法模板的內嵌值只能是語法對象、語法對象序列或者TransformerContext.

舊版本使用了模式匹配,和Rust語法相似,我我的更喜歡這個,不知爲什麼廢棄了
macro define {
    rule { $x } => {
        var $x
    }

    rule { $x = $expr } => {
        var $x = $expr
    }
}

define y;
define y = 5;
複製代碼

說了這麼多,相似Sweet.js 語法對象 的設計是現代編程語言爲了貼近 Lisp 宏的一個關鍵技術點。我發現ElixirRust等語言也使用了相似的設計。 除了數據結構的設計,現代編程語言的宏機制還包含如下特性:


1️⃣ 衛生宏(Hygiene)

衛生宏指的是在宏內生成的變量不會污染外部做用域,也就是說,在宏展開時,Sweet.js 會避免宏內定義的變量和外部衝突.

舉個例子,咱們建立一個swap宏,交換變量的值:

syntax swap = (ctx) => {
 const a = ctx.next().value
 ctx.next() // 吃掉','
 const b = ctx.next().value
 return #`
 let temp = ${a}
 ${a} = ${b}
 ${b} = temp
 `;
}

swap foo,bar
複製代碼

展開會輸出爲

let temp_10 = foo; // temp變量被重命名爲temp_10
foo = bar;
bar = temp_10;
複製代碼

若是你想引用外部的變量,也能夠。不過不建議這麼作,宏不該該假定其被展開的上下文:

syntax swap = (ctx) => {
  // ...
  return #`
  temp = ${a} // 不使用 let 聲明
  ${a} = ${b}
  ${b} = temp
  `;
}
複製代碼

2️⃣ 模塊化

Sweet.js 的宏是模塊化的:

'lang sweet.js';
// 導出宏
export syntax class = function (ctx) {
  // ...
};
複製代碼

導入:

import { class } from './es2015-macros';

class Droid {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }

  rollWithIt(it) {
    return this.name + " is rolling with " + it;
  }
}
複製代碼

相對Babel(編譯器)來講,Sweet.js的宏是模塊化/顯式的。Babel你須要在配置文件中配置各類插件和選項,尤爲是團隊項目構建有統一規範和環境時,項目構建腳本修改可能有限制。而模塊化的宏是源代碼的一部分,而不是構建腳本的一部分,這使得它們能夠被靈活地使用、重構以及廢棄

下文介紹的 babel-plugin-macros 最大的優點就在這裏, 一般咱們但願構建環境是統一的、穩定的、開發人員應該專一於代碼的開發,而不是如何去構建程序,正是由於代碼多變性,才催生出了這些方案


須要注意的是宏是在編譯階段展開的,因此沒法運行用戶代碼,例如:

let log = msg => console.log(msg); // 用戶代碼, 運行時被求值,因此沒法被訪問

syntax m = ctx => {
  // 宏函數在編譯階段被執行
  log('doing some Sweet things'); // ERROR: 未找到變量log
  // ...
};
複製代碼

Sweet.js 和其餘語言的宏同樣,有了它你能夠:

  • 新增語法糖(和Sweet.js 同樣甜), 實現複合本身口味的語法或者某些實驗性的語言特性
  • 自定義操做符, 很強大
  • 消滅重複的代碼,提高語言的表達能力。
  • ...
  • 別炫技

🤕很遺憾!Sweet.js 基本死了。因此如今當個玩具玩玩尚可,切勿用於生產環境。即便沒有死,Sweet.js 這種非標準的語法, 和現有的Javascript工具鏈生態格格不入,開發和調試都會比較麻煩(好比Typescript).

歸根到底,Sweet.js 的失敗,是社區拋棄了它。Javascript語言表達能力愈來愈強,版本迭代快速,加上有了Babel和Typescript這些解決方案,實在拿不出什麼理由來使用 Sweet.js

Sweet.js 相關論文能夠看這裏


小結

這一節扯得有點多,將宏的歷史和分類講了個遍。 最後的總結是Elixir官方教程裏面的一句話:顯式好於隱式,清晰的代碼優於簡潔的代碼(Clear code is better than concise code)

能力越大、責任越大。宏強大,比正常程序要更難以駕馭,你可能須要必定的成本去學習和理解它, 因此能不用宏就不用宏,宏是應該最後的法寶.



既生 Plugin 何生 Macro

🤓還沒完, 一會兒扯了好遠,掰回正題。既然 Babel 有了 Plugin 爲何又冒出了個 babel-plugin-macros?

若是你尚不瞭解Babel Macro,能夠先讀一下官方文檔, 另外Creact-React-APP 已經內置

這個得從 Create-React-App(CRA) 提及,CRA 將全部的項目構建邏輯都封裝在react-scripts 服務中。這樣的好處是,開發者不須要再關心構建的細節, 另外構建工具的升級也變得很是方便, 直接升級 react-scripts便可

若是本身維護構建腳本的話,升一次級你須要升級一大堆的依賴,若是你要維護跨項目的構建腳本,那就更蛋疼了。

我在《爲何要用vue-cli3?》 裏闡述了 CRA 以及 Vue-cli這類的工具對團隊項目維護的重要性。

CRA 是強約定的,它是按照React社區的最佳實踐給你準備的,爲了保護封裝帶來的紅利,它不推薦你去手動配置Webpack、Babel... 因此才催生除了 babel-plugin-macros, 你們能夠看這個 Issue: RFC - babel-macros

因此爲 Babel 尋求一個'零配置'的機制是 babel-plugin-macros 誕生的主要動機

這篇文章正好證明了這個動機:《Zero-config code transformation with babel-plugin-macros》, 這篇文章引述了一個重要的觀點:"Compilers are the New Frameworks"

的確,Babel 在現代的前端開發中扮演着一個很重要的角色,愈來愈多的框架或庫會建立本身的 Babel 插件,它們會在編譯階段作一些優化,來提升用戶體驗、開發體驗以及運行時的性能。好比:

  • babel-plugin-lodash 將lodash導入轉換爲按需導入
  • babel-plugin-import 上篇文章提過的這個插件,也是實現按需導入
  • babel-react-optimize 靜態分析React代碼,利用必定的措施優化運行效率。好比將靜態的props或組件抽離爲常量
  • root-import 將基於根目錄的導入路徑重寫爲相對路徑
  • styled-components 典型的CSS-in-js方案,利用Babel 插件來支持服務端渲染、預編譯模板、樣式壓縮、清除死代碼、提高調試體驗。
  • preval 在編譯時預執行代碼
  • babel-plugin-graphql-tag 預編譯GraphQL查詢
  • ...

上面列舉的插件場景中,並非全部插件都是通用的,它們要麼是跟某一特定的框架綁定、要麼用於處理特定的文件類型或數據。這些非通用的插件是最適合使用macro取代的

preval 舉個例子. 使用插件形式, 你首先要配置插件:

{
  "plugins": ["preval"]
}
複製代碼

代碼:

// 傳遞給preval的字符串會在編譯階段被執行
// preval插件會查找preval標識符,將字符串提取出來執行,在將執行的結果賦值給greeting
const greeting = preval` const fs = require('fs') module.exports = fs.readFileSync(require.resolve('./greeting.txt'), 'utf8') `
複製代碼

使用Macro方式:

// 首先你要顯式導入
import preval from 'preval.macro'

// 和上面同樣
const greeting = preval` const fs = require('fs') module.exports = fs.readFileSync(require.resolve('./greeting.txt'), 'utf8') `
複製代碼

這二者達到的效果是同樣的,但意義卻不太同樣。有哪些區別?

  • 1️⃣ 很顯然,Macro不須要配置.babelrc(固然babel-plugin-macros這個基座須要裝好). 這個對於CRA這種不推薦配置構建腳本的工具來講頗有幫助

  • 2️⃣ 由隱式轉換爲了顯式。上一節就說了「顯式好於隱式」。你必須在源代碼中經過導入語句聲明你使用了 Macro; 而基於插件的方式,你可能不知道preval這個標識符哪裏來的? 如何被應用?什麼時候被應用?並且一般你還須要和其餘工具鏈的配合,例如ESlint、Typescript聲明等等。

    Macro 由代碼顯式地應用,咱們更明確它被應用的目的和時機,對源代碼的侵入性最小。由於中間多了 babel-plugin-macro 這一層,咱們下降了對構建環境的耦合,讓咱們的代碼更方便被遷移。

  • 3️⃣ Macro相比Plugin 更容易被實現。由於它專一於具體的 AST 節點,見下文

  • 4️⃣ 另外,當配置出錯時,Macro能夠獲得更好的錯誤提示

有利有弊,Babel Macro 確定也有些缺陷,例如相對於插件來講只能顯式轉換,這樣代碼可能會比較囉嗦,不過我的以爲在某些場景利大於弊, 能顯式的就顯式。


那麼Babel Macro也是宏?相對於 Sweet.js 這些'正統'的宏機制有哪些不足

  • 首先 Babel Macro 必須是合法的 Javascript 語法。不支持自定義語法,也要分兩面討論,合法的Javascript語法不至於打破現有的工具協做鏈,若是容許用戶毫無限制地建立新的語法,未來指不定會和標準的語法發生歧義。 反過來不能自定義語法的‘宏’,是否顯得不太地道,不夠'強大'?

  • 由於必須是合法的Javascript語法,Babel Macro 實現DSL(Domain-specific languages)能力就弱化了

  • 再者,Babel Macro 和 Babel Plugin沒有本質的區別,相比Sweet.js提供了顯式定義和應用宏的語法,Babel Macro直接操做 AST 則要複雜得多,你仍是須要了解一些編譯原理,這把通常的開發者擋在了門外。

Babel 能夠實現自定義語法,只不過你須要Fork @babel/parser, 對它進行改造(能夠看這篇文章《精讀《用 Babel 創造自定義 JS 語法》》)。這個有點折騰,不太推薦


總之,Babel Macro 本質上和Babel Plugin沒有什麼區別,它只是在Plugin 之上封裝了一層(分層架構模式的強大),建立了一個新的平臺,讓開發者能夠在源代碼層面顯式地應用代碼轉換。因此,任何適合顯式去轉換的場景都適合用Babel Macro來作

  • 特定框架、庫的代碼轉換。如 styled-components
  • 動態生成代碼。preval
  • 特定文件、語言的處理。例如graphql-tag.macroyaml.macrosvgr.macro
  • ... (查看awesome-babel-macros)


如何寫一個 Babel Macro

因此,Babel Macro是如何運做的呢? babel-plugin-macros 要求開發者必須顯式地導入 Macro,它會遍歷匹配全部導入語句,若是導入源匹配/[./]macro(\.js)?$/正則,就會認爲你在啓用Macro。例以下面這些導入語句都匹配正則:

import foo from 'my.macro'
import { bar } from './bar/macro'
import { baz as _baz} from 'baz/macro.js'
// 不支持命名空間導入
複製代碼

Ok, 當匹配到導入語句後,babel-plugin-macros就會去導入你指定的 macro 模塊或者npm包(Macro 便可以是本地文件,也能夠是公開的 npm 包, 或者是npm包中的子路徑)。

那麼 macro 文件裏面要包含什麼內容呢?以下:

const { createMacro } = require('babel-plugin-macros')

module.exports = createMacro(({references, state, babel}) => {
  // ... macro 邏輯
})
複製代碼

macro 文件必須默認導出一個由 ceateMacro 建立的實例, 在其回調中能夠獲取到一些關鍵對象:

  • babel 和普通的Babel插件同樣,Macro 能夠獲取到一個 babel-core 對象
  • state 這個咱們也比較熟悉,Babel 插件的 visitor 方法的第二個參數就是它, 咱們能夠經過它獲取一些配置信息以及保存一些自定義狀態
  • references 獲取 Macro 導出標識符的全部引用。上一篇文章介紹了做用域,你應該還沒忘記綁定和引用的概念。以下

假設用戶這樣子使用你的 Macro:

import foo, {bar, baz as Baz} from './my.macro' // 建立三個綁定

// 下面開始引用這些綁定
foo(1)
foo(2)

bar`by tagged Template`
;<Baz>by JSX</Baz>
複製代碼

那麼你將拿到references結構是這樣的:

{
  // key 爲'綁定', value 爲'引用數組'
  default: [NodePath/*Identifier(foo)*/, NodePath/*Identifier(foo)*/], // 默認導出,即foo
  bar: [NodePath/*Identifier(bar)*/],
  baz: [NodePath/*JSXIdentifier(Baz)*/], // 注意key爲baz,不是Baz
}
複製代碼

查看詳細開發指南
AST Explorer 也支持 babel-plugin-macros,能夠玩一下. 下面的實戰實例,也建議在這裏探索一下

接下來你就能夠遍歷references, 對這些節點進行轉換,實現你想要的宏功能。開始實戰!



實戰

這一次咱們模範preval 建立一個eval.macro Macro, 利用它在編譯階段執行(eval)一些代碼。例如:

import evalm from 'eval.macro'
const x = evalm` function fib(n) { const SQRT_FIVE = Math.sqrt(5); return Math.round(1/SQRT_FIVE * (Math.pow(0.5 + SQRT_FIVE/2, n) - Math.pow(0.5 - SQRT_FIVE/2, n))); } fib(20) `

// ↓ ↓ ↓ ↓ ↓ ↓

const x = 6765
複製代碼

建立 Macro 文件. 按照上一節的介紹,① 咱們使用createMacro來建立一個 Macro實例, ② 並從references 中拿出全部導出標識符的引用路徑, ③接着就是對這些引用路徑進行AST轉換:

const { createMacro, MacroError } = require('babel-plugin-macros')

function myMacro({ references, state, babel }) {
  // 獲取默認導出的全部引用
  const { default: defaultImport = [] } = references;
  
  // 遍歷引用並進行求值
  defaultImport.forEach(referencePath => {
    if (referencePath.parentPath.type === "TaggedTemplateExpression") {
      const val = referencePath.parentPath.get("quasi").evaluate().value
      const res = eval(val)
      const ast = objToAst(res)
      referencePath.parentPath.replaceWith(ast)
    } else {
      // 輸出友好的報錯信息
      throw new MacroError('只支持標籤模板字符串, 例如:evalm`1`')
    }
  });
}

module.exports = createMacro(myMacro);
複製代碼

爲了行文簡潔,本案例中只支持標籤模板字符串 形式調用,可是標籤模板字符串中可能包含內插的字符串,例如:

hello` hello world ${foo} + ${bar + baz} `
複製代碼

其 AST 結構以下:


咱們須要將 TaggedTemplateExpression 節點轉換爲字符串。手動去拼接會很麻煩,好在每一個 AST 節點的 Path 對象都有一個evaluate 方法,這個方法能夠對節點進行‘靜態求值’:

t.evaluate(parse("5 + 5")) // { confident: true, value: 10 }
t.evaluate(parse("!true")) // { confident: true, value: false }
// ❌兩個變量相加沒法求值,由於變量值在運行時才存在,這裏confident爲false: 
t.evaluate(parse("foo + foo")) // { confident: false, value: undefined }
複製代碼

所以這樣子的標籤模板字符串是沒法求值的:

evalm`1 + ${foo}` // 包含變量
evalm`1 + ${bar(1)}` // 包含函數調用
複製代碼

這個和 Typescriptenum, 還有一些編譯語言的常量是同樣的,它們在編譯階段被求值,只有一些原始值以及一些原始值的表達式才支持在編譯階段被求值.


So,上面的代碼還不夠健壯,咱們再優化一下,在求值失敗時給用戶更好的提示:

defaultImport.forEach(referencePath => {
    if (referencePath.parentPath.type === "TaggedTemplateExpression") {
      const evaluated = referencePath.parentPath.get("quasi").evaluate();
      // 轉換標籤模板字符串失敗
      if (!evaluated.confident) {
        throw new MacroError("標籤模板字符串內插值只支持原始值和原始值表達式");
      }

      try {
        const res = eval(evaluated.value);
        const ast = objToAst(res);
        // 替換掉調用節點
        referencePath.parentPath.replaceWith(ast);
      } catch (err) {
        throw new MacroError(`求值失敗: ${err.message}`);
      }
    } else {
      throw new MacroError("只支持標籤模板字符串, 例如:evalm`1 + 1`");
    }
  });
複製代碼

接下來將執行後的值轉換爲 AST,而後替換掉TaggedTemplateExpression:

function objToAst(res) {
    let str = JSON.stringify(res);
    if (str == null) {
      str = "undefined";
    }
    const variableDeclarationNode = babel.template(`var x = ${str}`, {})();
    // 取出初始化表達式的 AST
    return variableDeclarationNode.declarations[0].init;
  }
複製代碼

這裏@babel/template 就派上用場了,它能夠將字符串代碼解析成 AST,固然直接使用parse方法解析也是能夠的。


Ok, 文章到這裏基本結束了。本文對‘宏’進行了深刻的討論,從 C 語言的文本替換宏到瀕死的Sweet.js, 最後介紹了babel-plugin-macros.

Babel Macro 本質上仍是Babel 插件,只不過它是模塊化的,你要使用它必須顯式地導入。和‘正統’宏相比, Babel Macro 直接操做 AST,須要你掌握編譯原理, ‘正統’宏能夠實現的東西, Babel Macro也能夠實現(例如衛生宏). 雖然相比Babel插件略有簡化,仍是比較囉嗦。另外Babel Macro 不能建立新的語法,這使得它能夠和現有的工具生態保持兼容。

最後!打開腦洞 🧠,Babel Macro 能夠作不少有意思的東西,查看《Awesome babel macros》。不過要謹記:‘顯式好於隱式,清晰的代碼優於簡潔的代碼’


截止 2019.10.10 掘金粉絲數已經突破 ✨2000✨,繼續關注我,點贊給我支持



擴展資料

相關文章
相關標籤/搜索