在 是时候好好认识下AST这个熟悉而又陌生的朋友了 这篇博客中,讲解了AST相关的知识,我在摸索AST的过程中顺腾摸瓜,顺便也把Babel好好研究了一番,于是趁热打铁输出了这篇博客,废话不多说,开搞
何为Babel
注意:因为目前Babel已经更新到了7.13,所以下文也都是基于 Babel7 进行讲解
打开 Babel 的官网,映入眼帘的是很醒目的一句话:Babel 是一个 JavaScript 编译器,由此可见官方对Babel的定位是一个js的编译器。其实准确来说,它应该是一个转换编译器(transpiler),借助它可以实现”源码到源码”的编译。Babel的工作流程大致分为以下三步
- 解析(parse),将源码转为AST
- 转换(transform),这是Babel插件主要参与的环节,主要是对AST进行修剪(新增,删除,更新AST节点)
- 生成(generate),将修剪后的AST转换为指定格式的源码
Babel本身是不进行任何编译转换的,所有具体的编译转换操作都被下发到了各个插件中(plugins),不同的插件负责不同目的的转换,比如 @babel/plugin-transform-arrow-functions负责转换es6下的箭头函数,而 @babel/plugin-transform-runtime 负责整合Babel提供的 helpers 到 @babel/runtime 公共库中,从而减少代码体积
由此可见Babel更类似于一个插件的 装配工厂,负责插件的安装与调度,从而生产出我们所需要的内容
Babel基于这种插件架构理论上可以实现任何转换,但我认为Babel最重要的转换还是下面两种
- 把用最新标准编写的js代码向下编译成目标环境所支持的低版本js代码
- 提供polyfill,抹平不同环境下对高版本js(es6,es7…)内置api支持的差异
在大致了解Babel后,接下来,开始深入探索Babel咯,准备好了吗?
Babel的配置与使用
要想探索Babel,总得先跑起来吧,怎么跑呢?其实只需要下载 @babel/cli 和 @babel/core 后,就能以命令行的方式运行Babel,安装命令为 npm install @babel/cli @babel/core --save-dev
注意这里 @babel/cli 是安装在项目里的,而不是全局安装的,虽然也可以全局安装,但是不建议,原因有两点
- 在同一台机器上的不同项目或许会依赖不同版本的 Babel-cli
- 全局安装意味着对工作环境有隐式依赖,这会让项目没有很好的移植性
当安装完Babel-cli后,就可以开始编译我们的源代码了
默认情况下,编译输出的内容会打印到控制台中,但这肯定不是我们想要的效果,通常情况下都是输出到指定的文件中,想要达到这个目的只需要传递参数给Babel-cli就可以了,常用的参数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| /* 默认不使用任何参数,会将输出的内容打印到控制台中 */ babel source.js
/* 指定输出的内容到文件中,使用 -o */ babel source.js -o target.js
/* 文件被修改后 自动编译该文件,使用 -w */ babel source.js -w
/* 额外输出sourcemap文件,使用 -s,注意这里必须指定输出的文件,否则不会额外生成sourcemap文件,而是inline形式 */ babel source.js -o target.js -s
/* 内联输出sourcemap,使用 -s inline */ babel source.js -s inline
/* 编译整个src目录下的文件并输出到lib目录,使用 -d,这不会覆盖lib目录下的任何其他文件或目录 */ babel src -d lib
/* 忽略部分文件,使用 --ignore */ babel src -d lib --ignore "src/**/*.spec.js","src/**/*.test.js"
/* 使用插件,使用 --plugins */ babel source.js --plugins=@babel/proposal-class-properties,@babel/transform-modules-amd
/* 使用presets,使用 --presets */ babel source.js --presets=@babel/preset-env,@babel/flow
|
可以看到Babel-cli支持的参数是很多的,但是如果每次编译都需要手动输入这么多内容,那没人会喜欢这个Babel的,毕竟我们的时间都很宝贵,Babel当然也考虑到了这一点,所以提供了配置文件来定制化Babel的功能
配置文件提供了很多方式,列举如下
- babel.config.json,这是Babel7推荐的使用方式
- babel.config.js,当需要通过编程的方式动态输出配置时可以考虑使用,注意需要导出配置对象 module.exports = {}
- .babelrc
- .babelrc.json
- .babelrc.js,当需要通过编程的方式动态输出配置时可以考虑使用,注意需要导出配置对象 module.exports = {}
- 可以选择将配置信息作为 babel 键(key)的值添加到 package.json 文件中,注意 babel 键是顶层属性
有了配置文件,接下来就该添加配置信息啦,下面列出常用配置项的说明,走你~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
{ "plugins": [ "@babel/some-plugin" ] }
{ "plugins": [ [ "@babel/some-plugin", { opt: 'opt' } ] ] }
{ "presets": ["@babel/preset-react"] }
{ "presets": [ [ "@babel/preset-react", { "pragma": "dom", "pragmaFrag": "DomFrag", "throwIfNamespace": false, "runtime": "classic" } ] ] }
{ "targets": "> 0.25%, not dead" }
{ "targets": { "chrome": "58", "ie": "11" } }
|
需要注意的是,plugins与presets的执行是存在顺序的,规则如下:
- plugins在presets之前运行
- plugins顺序从前往后
- presets顺序是相反的(从后往前)
除此之外,还有两个小技巧教给你
- 如果插件名称为@babel/plugin-XXX,可以使用短名称@babel/XXX
- 如果预设名称为@babel/preset-XXX,可以使用短名称@babel/XXX
经过上文的讲解,我相信你对Babel的配置与使用已经了然于胸了,接下来再介绍三个Babel提供的工具函数,借助这些工具函数可以使我们在Babel之外的环境实现源代码的转换,所以一起来瞅瞅吧~
Babel提供的工具函数
它们分别是:
- @babel/parser,它也是Babel内部使用的解析器,用于转换源码为AST,使用非常简单
babelParser.parse(code, [options])
- @babel/traverse,用于遍历AST从而达到更新、删除、新增节点的目的,通常与 @babel/parser 一起使用
- @babel/types,用于AST节点的 Lodash 式工具库, 它包含了构造、验证以及变换AST节点的方法,该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用,使用示例如下
1 2 3 4 5 6 7 8 9 10
| import traverse from "babel-traverse"; import * as t from "babel-types";
traverse(ast, { enter(path) { if (t.isIdentifier(path.node, { name: "n" })) { path.node.name = "x" } } })
|
看了这么多官方提供的插件工具,是不是也想自己捣鼓一个Babel插件出来呢?
创建与使用自定义Babel插件
其实Babel插件的创建非常简单,talk is cheap, show me the code,示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
module.exports = function(babel) { return { visitor: { Identifier(path,state) { const name = path.node.name path.node.name = name .split("") .reverse() .join("") }, Identifier: { enter() { console.log("Entered!"); }, exit() { console.log("Exited!"); } } } } }
|
插件定义好了,那么我们应该怎么使用呢?其实使用Babel插件的方式也很简单,分为两种
- 将创建好的插件上传到npm,然后下载下来作为依赖,并在配置文件中的 plugins 字段加上插件的名称就可以了
- 如果不想上传到npm,而是集成到自己的项目中,那么就需要在修改配置文件时,在 plugins 字段加上我们插件文件的路径,而不是插件的名称,示例如下
1 2 3 4 5 6
| module.exports = { "plugins": [ "./myBabelPlugin.js" ] }
|
现在我们应该已经能熟练地创建并使用自定义插件了,很开心吧🤪?接下来准备聊聊关于polyfill的内容,从而可以让我们对Babel有个更全面的认知,那么开始吧
使用polyfill的三种姿势
我们知道Babel可以将高版本的js语法转为低版本的,是的,这很酷😍!但是,Babel不会转译高版本中新增的类或方法(Promise、Array.isArray、’a’.repeat(5)),很明显,这是存在问题的,因为这些新的api在低版本浏览器下根本不存在。这个时候大名鼎鼎的 @babel/polyfill 就登场了,它是一个垫片库,用于抹平不同环境下存在的差异,让所有环境都处于同一水平线上,这样我们就可以畅通无阻地使用各种最新的api了😍
@babel/polyfill 本身由自定义的 regenerator runtime 和 core-js 组成, core-js 提供绝大部分js新特性的polyfill,regenerator runtime 主要提供(generator,yield)与(async,await)两组的polyfill
其实 @babel/runtime 也可以做到抹平浏览器差异的事情,并且做的更好,下面会讲解关于 @babel/runtime 与 @babel/polyfill 的三种使用姿势,让你能对此有个更全面的认知,所有方式都是以下面的代码为示例进行说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Array.isArray(params)
[1].includes(1)
''.charAt(1)
''.repeat(2)
const exampleFunc = ()=>{ console.log(a(1,2)) }
class exampleClass { constructor(a,b){ this.a = a this.b = b } }
const asyncFunc = async()=>{ await 1 }
|
【姿势一】👉 全量导入@babel/polyfill
这种方式分为以下两步
- 将它作为依赖项(注意不是开发依赖项,因为它是需要跑在代码运行时的)进行安装
- 在项目入口文件的第一行引入它
import "@babel/polyfill"
,这样做是因为它承担着抹平环境差异的责任,如果不是最先执行,那么我们的业务代码就会因为使用了新特性而报错
我们先看下这种方式构建出来的代码长啥样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| "use strict";
require("@babel/polyfill");
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
Array.isArray(params)[1].includes(1); ''.charAt(1); ''.repeat(2);
var exampleFunc = function exampleFunc() { console.log(a(1, 2)); };
var exampleClass = function exampleClass(a, b) { _classCallCheck(this, exampleClass);
this.a = a; this.b = b; };
var asyncFunc = function () { var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 1;
case 2: case "end": return _context.stop(); } } }, _callee); }));
return function asyncFunc() { return _ref.apply(this, arguments); }; }();
|
这种全量导入带来的后果就是构建产物的体积非常大,原因显而易见:这种使用方式是全量导入 @babel/polyfill,因此导致一股脑把所有新特性的polyfill都导入了,而不管我们有没有使用到
这种方式显然是不合理的,我们想要的是类似 按需引入 的效果,伟大的Babel也考虑到了这一点,请继续往下看
【姿势二】👉 按需导入@babel/polyfill
想要使用这种方式,改动的地方并不多,主要是以下两处
- 移除入口文件顶部导入@babel/polyfill的代码
import “@babel/polyfill
- 为 @babel/preset-env 添加配置项
{ useBuiltIns: "usage",corejs: 3 }
这里必须要指定corejs的版本,否则在编译时会报错。当前 @babel/polyfill (v7.12.1)默认会安装corejs2,因为corejs2已经停止维护,所以推荐使用corejs3,这种方式需要我们手动安装corejs3 npm install --save core-js@3
我们看下经过这种方式构建出来的代码长什么样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| "use strict";
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
require("regenerator-runtime/runtime.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.string.includes.js");
require("core-js/modules/es.array.is-array.js");
require("core-js/modules/es.string.repeat.js");
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
Array.isArray(params)[1].includes(1); ''.charAt(1); ''.repeat(2);
var exampleFunc = function exampleFunc() { console.log(a(1, 2)); };
var exampleClass = function exampleClass(a, b) { _classCallCheck(this, exampleClass);
this.a = a; this.b = b; };
var asyncFunc = function () { var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 1;
case 2: case "end": return _context.stop(); } } }, _callee); }));
return function asyncFunc() { return _ref.apply(this, arguments); }; }();
|
从构建出的代码可以看到所有用到的新特性polyfill都是单独进行引入的,这样就可以实现 按需引入 的目的啦😝
你是不是觉得这种方式已经是最优解了?答案当然不是了🤪,不然就不会有第三种方式了,其实细心观察输出的代码,我们会发现两个问题
- 类似
asyncGeneratorStep
、_asyncToGenerator
、_classCallCheck
这些Babel提供的helper函数(用于转换新特性语法的工具函数)会重复定义在每一个需要用到的文件中,这样会导致产生大量冗余代码从而增加构建产物的体积
- 因为 @babel/polyfill 会修改全局内置方法与对象(Array.isArray,’abc’.repeat(5)),所以会污染全局环境
下面介绍的方式就是为解决上述问题而出现的,一起来瞅瞅吧~
这种方式需要用到 @babel/plugin-transform-runtime 与 @babel/runtime 这两个包,废话不多说,先安装上!
1 2
| npm install --save-dev @babel/plugin-transform-runtime npm install --save @babel/runtime
|
接下来就是如何配置它们了,上代码!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = { "presets": [ [ "@babel/preset-env" ] ],
"plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": 3 } ] ] }
|
同样的,我们看下通过这种方式输出的代码长啥样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| "use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _isArray = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/is-array"));
var _repeat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/repeat"));
var _context, _context2;
(0, _includes["default"])(_context = (0, _isArray["default"])(params)[1]).call(_context, 1); ''.charAt(1); (0, _repeat["default"])(_context2 = '').call(_context2, 2);
var exampleFunc = function exampleFunc() { console.log(a(1, 2)); };
var exampleClass = function exampleClass(a, b) { (0, _classCallCheck2["default"])(this, exampleClass); this.a = a; this.b = b; };
var asyncFunc = function () { var _ref = (0, _asyncToGenerator2["default"])( _regenerator["default"].mark(function _callee() { return _regenerator["default"].wrap(function _callee$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return 1;
case 2: case "end": return _context3.stop(); } } }, _callee); }));
return function asyncFunc() { return _ref.apply(this, arguments); }; }();
|
从输出的代码可以很明显地看到上述的两个问题在这里都得到了解决
- 通过将所有helper函数封装到 @babel/runtime-corejs3 中再按需引入,这样就避免了在每个文件重复定义helper函数的问题
- 通过使用从 @babel/runtime-corejs3 导出的工具函数的方式,从而避免了全局污染
三种方式的比较🧐
通过上文的讲解,可以知道第三种方式应该是最优解,既能减小构建产物的体积,也能不污染全局环境
构建产物的体积是一个很重要的衡量指标,为了比对这三种方式构建产物的体积,我通过结合webpack来对上述示例中的代码进行构建,下面贴出结果
方式 |
构建产物体积 |
全量导入@babel/polyfill |
88kb |
按需导入@babel/polyfill |
28kb |
@babel/plugin-transform-runtime与@babel/runtime双剑合璧 |
35kb |
可以看到全量导入的方式远远大于其他方式,这也是我们意料之中的,但意料之外的是第二种方式构建的产物体积最小。虽然第二种方式构建产物的体积最小,但是与第三种相差并不大,并且还存在污染全局环境的隐患,所以最优解还是第三种🧐
至此,关于polyfill的讲解就结束了。由于上文都是以Babel7进行讲解的,所以接下来想聊聊Babel7与Babel6的差异,进而拓展我们对Babel的认知维度🤪
Babel7与Babel6的区别
其核心机制方面没有差异,插件、preset、解析转译生成这些都没有变化。主要变化有以下几点
- preset的变更:淘汰es201x,删除stage-x,强推env(重点)
- package名称的变化,把所有 babel-* 重命名为 @babel/*
- 内置解析器由原来的 babylon 变为 @babel/parser
- 支持的 node 版本需要 >=6
至此,所有关于Babel的讲解就结束了,相信此时的你已经收获满满了吧🤪~
结语
Babel无疑是伟大的,它彻底释放了我们的生产力,从而告别了曾经刀耕火种的时代。现在前端领域的蓬勃发展,Babel所贡献的力量是不容忽视的,因此对于Babel的学习是每一个前端工程师绕不开的领域,只有我们磨好了这把利器🪓,才能在未来的开发中披荆斩棘,最终顺利达到我们想要去的彼岸,所以,一起加油吧,骚年😂!