老师:这份 Babel 小抄拿去作弊

1,972 阅读12分钟

What is Babel?

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。(我摊牌了,直接从 Babel 中文官网复制),我们一般用 Babel 做下面几件事:

  • 语法转换(es-higher -> es-lower);
  • 通过 Polyfill 处理在目标环境无法转换的特性(通过 core-js 实现);
  • 源码转换(codemodsjscodeshift);
  • 静态分析(lint、根据注释生成 API 文档等);

Babel 真的可以为所欲为!😎😎😎

Babel7 最小配置

不知道你刚玩 Babel 时,有没那种被“初恋伤害”的感觉?如果有,那么请细品这一小节,它会让你欲罢不能。

在开始品“初恋的味道”前,咱们先做一些准备:

新建一个目录 babel-test 然后创建 package.json 文件:

mkdir babel-test && cd babel-test

// 本文全部用 yarn
yarn init -y

安装好 @babel/core@babel/cli

yarn add @babel/core @babel/cli -D

万事俱备,现在只需要买一盘 🌰 ,你就可以牵她的手啦:

// 在根目录新建 index.js 文件,然后键入下面的 🌰
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

初恋嘛,刚碰到对方的汗毛,你就脸红了,然后想看看对方的反应,对吧?所以执行下面的命令看看有什么结果:

// babel 是前面安装了 @babel/cli 才能用哦~
npx babel ./index.js --out-file build.js

执行完上面的命令,会在根目录输出一个 build.js 文件,打开一看:

let {
  x,
  y,
  ...z
} = {
  x: 1,
  y: 2,
  a: 3,
  b: 4
};
console.log(x); // 1

console.log(y); // 2

console.log(z); // { a: 3, b: 4 }

What the xxx? 这 ** 不就只是格式化了嘛!放在 IE10 上一致性,又是一个不眠之夜的信号:

ie-error

惊不惊喜意不意外?caniuse 一查,我尼玛,哪个*逼用扩展运算符啊,不知道我们要兼容IE 啊!

object-rest-spread-caniuse

但是作为勇猛的追求者,我们怎能因为对方手缩了一下就放弃呢!进到 Babel 插件页面,看需要什么插件能处理扩展运算符——可以看到这是一个 ES2018 的特性,通过 @babel/plugin-proposal-object-rest-spread 插件就可以用啦。

冲!再一次伸出你黝黑的手。在项目的根目录(package.json 文件所在的目录)下创建一个名为 babel.config.json 的文件(具体创建 .babelrc、还是 babel.config.js ,可以依据自己的场景选择,文件可以参考配置 Babel),并输入如下内容:

// 先到终端输入 yarn add @babel/plugin-proposal-object-rest-spread -D,安装依赖先
{
    "plugins": ["@babel/plugin-proposal-object-rest-spread"]
}

然后再执行:

npx babel ./index.js --out-file build.js

然后再打开 build.js 文件,这时可以看到扩展运算符已经见不到啦:

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

let _x$y$a$b = {
  x: 1,
  y: 2,
  a: 3,
  b: 4
},
    {
  x,
  y
} = _x$y$a$b,
    z = _objectWithoutProperties(_x$y$a$b, ["x", "y"]);

console.log(x); // 1

console.log(y); // 2

console.log(z); // { a: 3, b: 4 }

刷新 IE 浏览器,打开 F12 看看调试程序面板:

ie-error-destructuing

她再一次缩手了,心痛不?但是作为一名戴着红领巾,头上印着小红花的男人,绝不气馁!看到错误的代码位置,能识别到 IE 连解构赋值都不支持。同样的过程,查 caniuse@babel/plugin-transform-destructuring (提示:点击可以直接跳转到对应页面哦!)

这一次,再去牵她的手,gogogo

// 先到终端输入 yarn add @babel/plugin-transform-destructuring -D,安装依赖先
{
    "plugins": [
        "@babel/plugin-proposal-object-rest-spread",
        "@babel/plugin-transform-destructuring"
    ]
}

安装完之后再编译一次,可以看到生成的代码如下:

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

let _x$y$a$b = {
  x: 1,
  y: 2,
  a: 3,
  b: 4
},
    x = _x$y$a$b.x,
    y = _x$y$a$b.y,
    z = _objectWithoutProperties(_x$y$a$b, ["x", "y"]);

console.log(x); // 1

console.log(y); // 2

console.log(z); // { a: 3, b: 4 }

再次看 IE浏览器的反应:

ie-success

皇天不负有心人,IE 成了,你也牵手成功了!

细心的你不知道有没发现,在这两个 Babel 插件名字底下都有一个显眼的 NOTE

NOTE: This plugin is included in @babel/preset-env

啥意思呢?女生说希望你下次胆子再大点,一次就能牵上然后不放,为啥要多次尝试呢!

就此引出 @babel/preset-env ,跟着文档先把这个包装上,配置文件的 presets 字段配上。然后前面两个插件去掉。

yarn remove @babel/plugin-transform-destructuring @babel/plugin-proposal-object-rest-spread

yarn add @babel/preset-env -D

babel.config.json 改成如下:

{
  "presets": [
    [
      "@babel/preset-env"
    ]
  ]
}

然后再执行一次构建命令,可以看到输出的 build.js 文件是一样的!

惊叹的同时也在想:

  • 为什么一个预设就能满足转换需求呢?它是怎么做到的?
  • Babel 怎么知道我要支持 IE 浏览器,如果我只使用 Chrome,那么这个转换不是多余了么?而且不仅仅是浏览器,Babel 在桌面端、node 的场景都不少,它是怎么精确控制转换的?

回答上面的问题之前,突然想到一件事,之前在公司 review 代码时,看到很多童鞋为了使用 TypeScript 而被 TypeScript 支配(比如 AnyScript 的叫法由来)。希望都能从技术、工具、框架本身的诞生背景、作用去思考!如果不做到比较精细的类型声明和限制,为何用它?

Babel 也一样,Babel6 到 Babel7 的升级

compat-table

这个库维护着每个特性在不同环境的支持情况,来看看上面用到的解构赋值的支持:

{
  name: 'destructuring, declarations',
  category: 'syntax',
  significance: 'medium',
  spec: 'http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-assignment',
  mdn: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment',
  subtests: [
    {
      name: 'with arrays',
      exec: function(){/*
        var [a, , [b], c] = [5, null, [6]];
        return a === 5 && b === 6 && c === void undefined;
      */},
      res: {
        tr: true,
        babel6corejs2: babel.corejs,
        ejs: true,
        es6tr: true,
        jsx: true,
        closure: true,
        typescript1corejs2: true,
        firefox2: true,
        opera10_50: false,
        safari7_1: true,
        ie11: false,
        edge13: edge.experimental,
        edge14: true,
        xs6: true,
        chrome49: true,
        node6: true,
        node6_5: true,
        jxa: true,
        duktape2_0: false,
        graalvm19: true,
        graalvm20: true,
        graalvm20_1: true,
        jerryscript2_0: false,
        jerryscript2_2_0: true,
        hermes0_7_0: true,
        rhino1_7_13: true
      }
    }

ie11false 的,怪不得第一次牵手失败了。

browserslist

这个包应该比较熟悉了,可以通过 query 查询具体的浏览器列表,下面安装上这个包然后来实操一波:

yarn add browserslist -D

// 查询的条件各种骚操作都有,具体可参考 https://github.com/browserslist/browserslist#queries
npx browserslist "> 0.25%, not dead"
and_chr 91
and_ff 89
and_uc 12.12
android 4.4.3-4.4.4
chrome 91
chrome 90
chrome 89
chrome 87
chrome 85
edge 91
firefox 89
firefox 88
ie 11
ios_saf 14.5-14.7
ios_saf 14.0-14.4
ios_saf 13.4-13.7
op_mini all
opera 76
safari 14.1
safari 14
safari 13.1
samsung 14.0
samsung 13.0

有了上面两个包,那 preset-env 实现特性精细控制岂不是洒洒水。继续实操,我们把开头那个 🌰 改成不需要支持 ie11 试试看:

{
  "presets": [
    [
	    "@babel/preset-env",
        {
    		"targets": {
                "chrome": 55
            }        
        }
    ]
  ]
}

preset-env 控制浏览器版本是通过配置 targets 字段。构建一下看看结果:

"use strict";

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

let _x$y$a$b = {
  x: 1,
  y: 2,
  a: 3,
  b: 4
},
    {
  x,
  y
} = _x$y$a$b,
    z = _objectWithoutProperties(_x$y$a$b, ["x", "y"]);

console.log(x); // 1

console.log(y); // 2

console.log(z); // { a: 3, b: 4 }

从上面源码可以看出来,扩展运算符被转换了,解构赋值没有被转换。被转换的特性通过模块内定义了两个方法 _objectWithoutProperties_objectWithoutPropertiesLoose。如果我有两个文件都使用了扩展运算符,然后输出一个文件,结果会怎样呢?根目录下新建一个 index2.js 文件:

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

然后分别执行下面两条命令:

npx babel ./index.js ./index2.js --out-file build.js

结果是 _objectWithoutProperties_objectWithoutPropertiesLoose 居然都会重复声明两次。这对于需要转换的特性,我使用很多次,转换后输出的文件不是爆炸了么?此时需要一个插件来控制代码量——@babel/plugin-transform-runtime 。对于这种转换函数,在外部模块化,用到的地方直接引入即可。实操:

// 先安装 @babel/plugin-transform-runtime 包
yarn add @babel/plugin-transform-runtime -D

然后配置 babel

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome": "55"
                }
            }
        ]
    ],
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

再执行上面的构建命令,得到以下结果:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));

let _x$y$a$b = {
  x: 1,
  y: 2,
  a: 3,
  b: 4
},
    {
  x,
  y
} = _x$y$a$b,
    z = (0, _objectWithoutProperties2.default)(_x$y$a$b, ["x", "y"]);
console.log(x); // 1

console.log(y); // 2

console.log(z); // { a: 3, b: 4 }

let _a$b$c = {
  a: 1,
  b: 2,
  c: 3
},
    {
  a,
  b
} = _a$b$c,
    c = (0, _objectWithoutProperties2.default)(_a$b$c, ["a", "b"]);

对比上面的转换结果,这次转换结果精简了不少。并且函数声明都是通过外部引入。

再来看下面这段代码:

const a = [1,2,3,4,6];
console.log(a.includes(7))

通过 @babel/compat-data 可以看下 includes 特性的兼容性:

"es7.array.includes": {
    "chrome": "47",
    "opera": "34",
    "edge": "14",
    "firefox": "43",
    "safari": "10",
    "node": "6",
    "ios": "10",
    "samsung": "5",
    "electron": "0.36"
}

chrome 47+ 支持数组 includes API,我们把 babel.config.jsontargets 改成 45 然后执行转换命令,结果如下:

"use strict";

var a = [1, 2, 3, 4, 6];
console.log(a.includes(7));

可以得出结论:虽然不支持 Array.prototype.inlcudes,但是 babel 默认不会对实例方法做转换。这时候就需要引入 @babel/polyfill 打补丁。(⚠️ 安装 polyfill 包是 dependency 哦!因为在生产环境上垫片是要在你的代码前执行。)

在项目入口文件或者在打包工具比如 webpackentry 一把梭把全部 polyfill 引进来:

// app.js
import '@babel/polyfill';

// webpack.config.js
module.exports = {
  entry: ["@babel/polyfill", "./app/js"],
};

其中很多特性的垫片我们都用不着,那么能不能也结合上述的 broswer targets 和代码中使用到的函数去做定制的垫片呢? Of course,在这里推荐一个在线定制 polyfill网站 ,选择完自己的垫片,然后生成一个 CDN URL。在项目中直接引入就可以啦,这可以用于微型的网站,对于超大型的项目,不可能自己一个一个方法去选择吧。这就要引出 useBuiltIns 配置,它定义了 @babel/preset-env 怎么处理垫片。可选的值有:

  • usage:每个文件引用使用到的特性;
  • entry:入口处全部引入;
  • false:不引入。

have-one-example

// index.js
const a = [1,2,3,4,6];

console.log(a.includes(7))

new Promise(() => {})

然后将 babel 的配置改成如下:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome": "45",
                    "ie": 11
                },
                "useBuiltIns": "usage",
                "corejs": 3
            }
        ]
    ],
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

在终端执行 babel ./index.js --out-file build.js,看看 build.js 的结果:

"use strict";

require("core-js/modules/es.array.includes.js");

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

var a = [1, 2, 3, 4, 6];
console.log(a.includes(7));
new Promise(function () {});

niubility!对于不支持的特性都引入了特定的 core-js 垫片。这怎么做到的呢?这还是归功于 AST,它可以结合代码的实际情况,进行超级细的按需引用。感兴趣的童鞋可以看看 core-jsbabel 的协作方式哦。

小结

通过 🌰 去一步一步分析 Babel7 最小最优配置的产生,其中还涉及一些写配置中无感知的处理机制,比如 compat-tablebrowserslist。读完本节,相信你对 babel7 配置方法有一个清晰的了解。

@babel 系列包

Babel 是一个 Monorepo 项目,packages 下面有 146 个包。Unbelievable!包虽多,我们可以将它们划分为几个类别:

@babel/helper-xx 有 28 个,@babel/plugin-xx 有 98 个。剩下的工具包、集成包总共也才 20 个。我们挑一些有意思的 package 来了解它们的作用。

@babel/standalone

babel-standalone 提供独立构建的 Babel 用于浏览器和其他非 Node 环境,比如在线 IDEJSFiddleJS Bin、还有 Babel 官方的 try it out 都是基于这个包。我们也来玩儿~

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>standalone</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.0.0-beta.3/babel.min.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
    <div id="app"></div>
    <script type="text/babel">
        const codeStr = `const getMessage = () => "Babel, 为所欲为";`;
        const code = Babel.transform(codeStr, { presets: ["env"] }).code;
        document.querySelector('#app').innerHTML = code;
    </script> 
</body>
</html>

上述代码直接运行在浏览器,得到的 code 如下:

"use strict"; var getMessage = function getMessage() { return "Babel, 为所欲为"; };

跟在 node 环境构建出来的结果是一样的。

@babel/plugin-xx

满足这种标记的都是 Babel 插件。主要用来加强 transformparser 能力。举个 🌰:

// index.js
const code = require("@babel/core").transformSync(
    `var b = 0b11;var o = 0o7;const u="Hello\\u{000A}\\u{0009}!";`
).code;

console.log(code)

执行 node index.js,返回结果:

var b = 0b11;
var o = 0o7;
const u = "Hello\u{000A}\u{0009}!";

原样返回,如果我要识别二进制整数、十六进制整数、Unicode 字符串文字、换行符和制表符,那么就需要加上 @babel/plugin-transform-literals 。加上之后执行结果如下:

var b = 3;
var o = 7;
const u = "Hello\n\t!";

通过上述 Demo 了解到 plugin 的作用。

打开 babel/packages,我们可以看到 plugins 主要有三种类型:

babel-plugin-type

  1. babel-plugin-transform-xx:转换插件,主要用来加强转换能力,上面的 @babel/plugin-transform-literals 就属于这种;
  2. babel-plugin-syntax-xx:语法插件,主要是扩展编译能力,比如不在 async 函数作用域里面使用 await,如果不引入 @babel/plugin-syntax-top-level-await,是没办法编译成 AST 树的。并且会报 Unexpected reserved word 'await' 这种类似的错误。
  3. babel-plugin-proposal-xx:用来编译和转换在提案中的属性,在 Plugins List 中可以看到这些插件,比如 class-propertiesdecorators

小结

通过简单了解 babel-standalonebabel-plugin-xx 系列包,只能感叹 Babel 生态真滴强,为所欲为真不是瞎说的。还有一些低层的 package,我们在之前也有接触过,例如 @babel/core@babel/parser@babel/generator@babel/code-frame 等等,下面我会通过写一个 plugin 去感受这些 package 的作用。

写一个 plugin

这一节属于 Babel 进阶内容,阔以饮杯靓靓的茶 🍵 再继续。

drink-coffee

需求是这样的(好玩儿...),我有下面这段代码:

// 假设 spliceText 是全局函数,有大量的使用
function spliceText (...args) {
  return args[0].replace(/(\{(\d)\})/g, (...args2) => {
    return args[Number(args2[2]) + 1]
  })   
}

spliceText('我有一只小{0},我从来都不{1}', '毛驴', '骑')    // 有一只小毛驴,我从来都不骑
spliceText('我叫{0},今年{1}岁,特长是{2}', '小余', 18, '睡觉') // 叫小余,今年18岁,特长是睡觉
spliceText('有趣的灵魂')    // 有趣的灵魂

因为公司的代码规范说传参最多不能超过 2 个。我先修改函数定义:

function spliceTextCopy (str, obj) {
    return str.replace(/(\{(\d)\})/g, (...args2) => {
      return obj[args2[2]]
    })   
}

使用方式变成第二个参数传对象:

spliceTextCopy('我有一只小{0},我从来都不{1}', {0: '毛驴', 1: '骑'})    // 有一只小毛驴,我从来都不骑
spliceTextCopy('我叫{0},今年{1}岁,特长是{2}', {0: '小余', 1: 18, 2: '睡觉'}) // 叫小余,今年18岁,特长是睡觉
spliceTextCopy('有趣的灵魂')    // 有趣的灵魂

函数的调用方式如上,但前面也备注了,spliceText 有大量的使用,不想手动一个一个去改。现在就来试试使用 babel transform 去处理。

Babel 工作流经典图:

babel-workflow

分析

根据上图,我们梳理需求的逻辑:

  1. 先用 astexplorer 查看生成的抽象语法树,也就是查看用 @babel/parser 处理的结果;
  2. 使用 @babel/traverse 遍历语法树,找到满足函数名是 spliceText函数调用表达式(CallExpression;
  3. 做节点的转换,将 CallExpressionarguments 字段做转换、改变函数名;
  4. 使用 @babel/generator 生成最终的代码;

测试用例

Babel 插件的测试套件 babel-plugin-tester ,这个工具基于 jest 封装的,所以项目还是要安装 jest 工具哦。

yarn add jest -D

写好测试用例先,就可以开始我们的编码之旅啦。先按照上面的需求写一下用例:

import plugin from '../plugin/index';
import pluginTester from 'babel-plugin-tester';

pluginTester({
    plugin: plugin,
    tests: {
        'no-params': {
            code: `spliceText('有趣的灵魂')`,
            snapshot: true
        },
        'has-params': {
            code: `spliceText('我有一只小{0},我从来都不{1}', '毛驴', '骑')`,
            snapshot: true
        }
    }
})

⚠️ 说明一下这里为什么采用快照测试?babel-plugin-tester 输出的结果会将单引号改成双引号,但是 @babel/core transformFromAst 之后的结果又没有做这个改变。导致不符合我的预期。所以用快照的形式来查看结果。这个问题不影响我们的结果,后续查到原因再同步出来。有了测试用例,我们就用 jest 将其跑起来:

npx jest --watchAll

编码

编码之前,可以先通过 Babel 插件手册了解如何创建插件 😆

首先把架子搭好:

const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) => {
    api.assertVersion(7);
    
    return {
        name: 'my-test-plugin',
      	pre (file) {
          
        },
        visitor: {
            
        },
        post (file) {
          
        }
    };
});

prepost 是在遍历开始和结束时间调用,一般用来做访问节点时的数据缓存。visitor 通过访问者模式去依次访问每个节点。declare 函数装饰器,给内部参数 api 附加了如上面用到的 assertVersion 方法。

使用 astexplorer 来分析字段,先从简单的用例开始,从 spliceText('有趣的灵魂')spliceTextCopy('有趣的灵魂')

babel-ast-01

astexplorer 左下角面板支持在线修改并查看效果哦!第一步比较简单,访问 Identifier 节点,如果名称是老函数名 spliceText,将其 name 改成 spliceTextCopy

然后再来看看有参数的情况 spliceText('我有一只小{0},我从来都不{1}', '毛驴', '骑') ,这个除了改变函数名之外,还需要将第二个及之后的参数转换成对象的形式。

babel-ast-02

对象在 AST 里怎么表达呢?可以先在 astexplorer 随便写一个对象,然后再去查看对应的 node type

babel-ast-03

依据上面的分析,来补充我们的插件代码:

const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) => {
    api.assertVersion(7);
    const { types } = api;
    
    return {
        name: 'my-test-plugin',
        pre (file) {
            
        },
        visitor: {
            Identifier (path, state) {
                if (path.node.name === 'spliceText') {
                    path.node.name = 'spliceTextCopy';
                    
                    // 拿到当前 Identifier 的父节点也就是整个表达式
                    const parent = path.parent;
                    const args = parent.arguments;

                    // 只有一个参数,不需要处理
                    if (args.length === 1) {
                        return;
                    }

                    // 构建空对象的 ast
                    const params = types.objectExpression([]);

                    // 从 1 开始,遍历参数,然后塞进对象表达式的 properties 中
                    for (let i = 1, len = args.length; i < len; i++) {
                        // types.objectProperty 创建对象属性的 ast
                        params.properties.push(
                            types.objectProperty(
                                types.numericLiteral(i-1),
                                args[i]
                            )
                        )
                    }

                    parent.arguments.splice(1);
                    parent.arguments.push(params);
                    path.skip();
                }
            }
        },
        post (file) {

        }
    };
});

去看看测试结果:

unit-test-result

漂亮,快照结果都符合预期。这个插件的功能就完成啦。然后就可以将插件发布到 npm 仓库,在自己的项目 babel.config.json 引入该插件就大功告成啦。

总结

本文从平时工作角度出发,一步一步分享 babel7 的最小最优配置的由来,然后简单了解 babelpackages,分享了 @babel/standalone 这个有意思的包和插件系列的分类。最后从需求分析、测试、编码的业务开发流程分享写一个 babel plugin 的心路历程。本文 demo 已经同步到 github 。 更多内容可以关注小余公众号:

扫码_搜索联合传播样式-白色版.png