深刻浅出 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✨,继续关注我,点赞给我支持



扩展资料

相关文章
相关标签/搜索