Loading...
墨滴

jouryjc

2021/07/11  阅读:40  主题:橙心

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

What is Babel?

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。(我摊牌了,直接从 `Babel` 中文官网[1]复制),我们一般用 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 } = { x1y2a3b4 };
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
} = {
  x1,
  y2,
  a3,
  b4
};
console.log(x); // 1

console.log(y); // 2

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

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

ie-error
ie-error

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

object-rest-spread-caniuse
object-rest-spread-caniuse

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

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

// 先到终端输入 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, excludedif (source == nullreturn {}; 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) >= 0continueif (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

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

let _x$y$a$b = {
  x1,
  y2,
  a3,
  b4
},
    {
  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-error-destructuing

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

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

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

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

function _objectWithoutProperties(source, excludedif (source == nullreturn {}; 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) >= 0continueif (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

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

let _x$y$a$b = {
  x1,
  y2,
  a3,
  b4
},
    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-success

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

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

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

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

就此引出 @babel/preset-env[8] ,跟着文档先把这个包装上,配置文件的 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 的升级[9]

  • 废弃了 stage-xes20xxpreset,改成 preset-envplugin-proposal-xx;这样能更好地控制需要支持的特性;
  • preset-env[10] 依赖 `browserslist`[11], `compat-table`[12], and `electron-to-chromium`[13] 实现了特性的精细按需引入。

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',
      execfunction(){/*
        var [a, , [b], c] = [5, null, [6]];
        return a === 5 && b === 6 && c === void undefined;
      */
},
      res: {
        trtrue,
        babel6corejs2: babel.corejs,
        ejstrue,
        es6trtrue,
        jsxtrue,
        closuretrue,
        typescript1corejs2true,
        firefox2true,
        opera10_50false,
        safari7_1true,
        ie11false,
        edge13: edge.experimental,
        edge14true,
        xs6true,
        chrome49true,
        node6true,
        node6_5true,
        jxatrue,
        duktape2_0false,
        graalvm19true,
        graalvm20true,
        graalvm20_1true,
        jerryscript2_0false,
        jerryscript2_2_0true,
        hermes0_7_0true,
        rhino1_7_13true
      }
    }

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, excludedif (source == nullreturn {}; 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) >= 0continueif (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

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

let _x$y$a$b = {
  x1,
  y2,
  a3,
  b4
},
    {
  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 } = { x1y2a3b4 };
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[14] 。对于这种转换函数,在外部模块化,用到的地方直接引入即可。实操:

// 先安装 @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 = {
  x1,
  y2,
  a3,
  b4
},
    {
  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 = {
  a1,
  b2,
  c3
},
    {
  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 = [12346];
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网站 [15],选择完自己的垫片,然后生成一个 CDN URL。在项目中直接引入就可以啦,这可以用于微型的网站,对于超大型的项目,不可能自己一个一个方法去选择吧。这就要引出 useBuiltIns[16] 配置,它定义了 @babel/preset-env 怎么处理垫片。可选的值有:

  • usage:每个文件引用使用到的特性;
  • entry:入口处全部引入;
  • false:不引入。
have-one-example
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 = [12346];
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[17] 提供独立构建的 Babel 用于浏览器和其他非 Node 环境,比如在线 IDEJSFiddle[18]JS Bin[19]、还有 Babel 官方的 try it out[20] 都是基于这个包。我们也来玩儿~

<!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
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[21] 中可以看到这些插件,比如 class-properties[22]decorators[23]

小结

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

写一个 plugin

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

drink-coffee
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'小余'1182'睡觉'}) // 叫小余,今年18岁,特长是睡觉
spliceTextCopy('有趣的灵魂')    // 有趣的灵魂

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

Babel 工作流经典图:

babel-workflow
babel-workflow

分析

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

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

测试用例

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

yarn add jest babel-plugin-tester -D

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

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

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

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

npx jest --watchAll

编码

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

首先把架子搭好:

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 通过**访问者模式**[28]去依次访问每个节点。declare 函数装饰器,给内部参数 api 附加了如上面用到的 assertVersion 方法。

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

babel-ast-01
babel-ast-01

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

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

babel-ast-02
babel-ast-02

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

babel-ast-03
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
unit-test-result

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

总结

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

参考资料

[1]

Babel 中文官网: https://www.babeljs.cn/docs/

[2]

caniuse: https://caniuse.com/

[3]

Babel 插件页面: https://www.babeljs.cn/docs/plugins-list#es2018

[4]

@babel/plugin-proposal-object-rest-spread: https://www.babeljs.cn/docs/babel-plugin-proposal-object-rest-spread

[5]

配置 Babel: https://www.babeljs.cn/docs/configuration

[6]

caniuse: https://caniuse.com/?search=Destructuring

[7]

@babel/plugin-transform-destructuring: https://www.babeljs.cn/docs/babel-plugin-transform-destructuring

[8]

@babel/preset-env: https://www.babeljs.cn/docs/babel-preset-env

[9]

Babel6 到 Babel7 的升级: https://www.babeljs.cn/docs/v7-migration

[10]

preset-env: https://www.babeljs.cn/docs/babel-preset-env

[11]

browserslist: https://github.com/browserslist/browserslist

[12]

compat-table: https://github.com/kangax/compat-table

[13]

electron-to-chromium: https://github.com/Kilian/electron-to-chromium

[14]

@babel/plugin-transform-runtime: https://www.babeljs.cn/docs/babel-plugin-transform-runtime

[15]

网站 : https://polyfill.io/v3/url-builder/

[16]

useBuiltIns: https://babeljs.io/docs/en/babel-preset-env#usebuiltins

[17]

babel-standalone: https://babeljs.io/docs/en/babel-standalone

[18]

JSFiddle: https://jsfiddle.net/

[19]

JS Bin: https://jsbin.com/?html,js,console,output

[20]

try it out: https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=Q&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.14.7&externalPlugins=

[21]

Plugins List: https://babeljs.io/docs/en/plugins-list

[22]

class-properties: https://babeljs.io/docs/en/babel-plugin-proposal-class-properties

[23]

decorators: https://babeljs.io/docs/en/babel-plugin-proposal-decorators

[24]

astexplorer: https://astexplorer.net/

[25]

babel-plugin-tester: https://github.com/babel-utils/babel-plugin-tester

[26]

jest: https://jestjs.io/

[27]

Babel 插件手册: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

[28]

访问者模式: http://c.biancheng.net/view/1397.html

[29]

astexplorer: https://astexplorer.net/

[30]

github: https://github.com/Jouryjc/babel-plugin-demo

jouryjc

2021/07/11  阅读:40  主题:橙心

作者介绍

jouryjc