共计 5169 个字符,预计需要花费 13 分钟才能阅读完成。
解剖 Babel —— 向前端架构师迈出一小步
当聊到 Babel 的作用,很多人第一反馈是:用来实现 API polyfill。
事实上,Babel 作为前端工程化的基石,作用远不止这些。
作为一个宏大的家族,Babel 生态中有很多概念,比方:preset、plugin、runtime 等。
这些概念使初学者对 Babel 望而却步,对其了解也止步于 webpack 的 babel-loader 配置。
本文会从 Babel 的外围性能登程,一步步揭开 Babel 大家族的神秘面纱,向前端架构师迈出一小步。
Babel 是什么
Babel 是一个 JavaScript 编译器。
作为 JS 编译器,Babel 接管输出的 JS 代码,通过外部解决流程,最终输入批改后的 JS 代码。
在 Babel 外部,会执行如下步骤:
1、将 Input Code 解析为 AST(形象语法树), 这一步称为 parsing
2、编辑 AST,这一步称为 transforming
3、将编辑后的 AST 输入为 Output Code,这一步称为 printing
从 Babel 仓库 [1] 的源代码,能够发现:Babel 是一个由几十个我的项目组成的 Monorepo。
其中 babel-core 提供了以上提到的三个步骤的能力。
在 babel-core 外部,更粗疏的讲:
- babel-parser 实现第一步
- babel-generator 实现第三步
要理解第二步,咱们须要简略理解下 AST。
AST 的构造
进入 AST explorer[2],抉择 @babel/parser 作为解析器,在左侧输出:
const name = ['ka', 'song'];
能够解析出如下构造的 AST,他是 JSON 格局的树状构造:
在 babel-core 外部:
- babel-traverse 能够通过「深度优先」的形式遍历 AST 树
- 对于遍历到的每条门路,babel-types 提供用于批改 AST 节点的节点类型数据
所以,整个 Babel 底层编译能力由如下局部形成:
当咱们理解 Babel 的底层能力后,接下来看看基于这些能力,下层能实现什么性能?
Babel 的下层能力
基于 Babel 对 JS 代码的编译解决能力,Babel 最常见的下层能力为:
- polyfill
- DSL 转换(比方解析 JSX)
- 语法转换(比方将高级语法解析为以后可用的实现)
因为篇幅无限,这里仅介绍 polyfill 与「语法转换」相干性能。
polyfill
作为前端,最常见的 Babel 生态的库想必是 @babel/polyfill 与 @babel/preset-env。
应用 @babel/polyfill 或 @babel/preset-env 能够实现高级语法的降级实现以及 API 的 polyfill。
从上文咱们晓得,Babel 自身只是 JS 的编译器,以上两者的转换性能是谁实现的呢?
答案是:core-js
core-js 简介
core-js 是一套模块化的 JS 规范库,包含:
- 始终到 ES2021 的 polyfill
- promise、symbols、iterators 等一些个性的实现
- ES 提案中的个性实现
- 跨平台的 WHATWG / W3C 个性,比方 URL
从 core-js 仓库 [3] 看到,core-js 也是由多个库组成的 Monorepo,包含:
- core-js-builder
- core-js-bundle
- core-js-compat
- core-js-pure
- core-js
咱们介绍其中几个库:
core-js
core-js 提供了 polyfill 的外围实现。
import 'core-js/features/array/from';
import 'core-js/features/array/flat';
import 'core-js/features/set';
import 'core-js/features/promise';
Array.from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
[1, [2, 3], [4, [5]]].flat(2); // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x)); // => 32
间接应用 core-js 会净化全局命名空间和对象原型。
比方上例中批改了 Array 的原型以反对数组实例的 flat 办法。
core-js-pure
core-js-pure 提供了独立的命名空间:
import from from 'core-js-pure/features/array/from';
import flat from 'core-js-pure/features/array/flat';
import Set from 'core-js-pure/features/set';
import Promise from 'core-js-pure/features/promise';
from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
flat([1, [2, 3], [4, [5]]], 2); // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x)); // => 32
这样应用不会净化全局命名空间与对象原型。
core-js-compat
core-js-compat 依据 Browserslist 保护了不同宿主环境、不同版本下对应须要反对个性的汇合。
Browserslist[4]提供了不同浏览器、node 版本下 ES 个性的反对状况
比方:
"browserslist": [
"not IE 11",
"maintained node versions"
]
代表:非 IE11 的版本以及所有 Node.js 基金会保护的版本。
@babel/polyfill 与 core-js 关系
@babel/polyfill 能够看作是:core-js 加 regenerator-runtime。
regenerator-runtime 是 generator 以及 async/await 的运行时依赖
独自应用 @babel/polyfill 会将 core-js 全量导入,造成我的项目打包体积过大。
从 Babel v7.4.0[5]开始,@babel/polyfill 被废除了,能够间接援用 core-js 与 regenerator-runtime 代替
为了解决全量引入 core-js 造成打包体积过大的问题,咱们须要配合应用 @babel/preset-env。
preset 的含意
在介绍 @babel/preset-env 前,咱们先来理解 preset 的意义。
初始状况下,Babel 没有任何额定能力,其工作流程能够形容为:
const babel = code => code;
其通过 plugin 对外提供染指 babel-core 的能力,相似 webpack 的 plugin 对外提供染指 webpack 编译流程的能力。
plugin 分为几类:
- @babel/plugin-syntax-* 语法相干插件,用于新的语法反对。比方 babel-plugin-syntax-decorators[6]提供 decorators 的语法反对
- @babel/plugin-proposal-* 用于 ES 提案的个性反对,比方 babel-plugin-proposal-optional-chaining 是可选链操作符个性反对
- @babel/plugin-transform-* 用于转换代码,transform 插件外部会应用对应 syntax 插件
多个 plugin 组合在一起造成的汇合,被称为 preset。
@babel/preset-env
应用 @babel/preset-env,能够「按需」将 core-js 中的个性打包,这样能够显著缩小最终打包的体积。
这里的「按需」,分为两个粒度:
宿主环境的粒度。依据不同宿主环境将该环境下所需的所有个性打包
按应用状况的粒度。仅仅将应用了的个性打包
咱们来顺次看下。
宿主环境的粒度
当咱们按如下参数在我的项目目录下配置 browserslist 文件(或在 @babel/preset-env 的 targets 属性内设置,或在 package.json 的 browserslist 属性中设置):
not IE 11
maintained node versions
会将「非 IE11」且「所有 Node.js 基金会保护的 node 版本」下须要的个性打入最终的包。
显然这是利用了方才介绍的 core-js 这个 Monorepo 下的 core-js-compat 的能力。
按应用状况的粒度
更现实的状况是只打包咱们应用过的个性。
这时候能够设置 @babel/preset-env 的 useBuiltIns 属性为 usage。
比方:
a.js:
var a = new Promise();
b.js:
var b = new Map();
当宿主环境不反对 promise 与 Map 时,输入的文件为:
a.js:
import "core-js/modules/es.promise";
var a = new Promise();
b.js:
import "core-js/modules/es.map";
var b = new Map();
当宿主环境反对这两个个性时,输入的文件为:
a.js:
var a = new Promise();
b.js:
var b = new Map();
进一步优化打包体积
关上 babel playground[7],输出:
class App {}
会发现编译出的后果为:
function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function"); } }
var App = function App() {
"use strict";
_classCallCheck(this, App);
};
其中_classCallCheck 为辅助办法。
如果多个文件都应用了 class 个性,那么每个文件打包对应的 module 中都将蕴含_classCallCheck。
为了缩小打包体积,更好的形式是:须要应用「辅助办法」的 module 都从同一个中央援用,而不是本人保护一份。
@babel/runtime 蕴含了 Babel 所有「辅助办法」以及 regenerator-runtime。
单纯引入 @babel/runtime 还不行,因为 Babel 不晓得何时援用 @babel/runtime 中的「辅助办法」。
所以,还须要引入 @babel/plugin-transform-runtime。
这个插件会在编译时将所有应用「辅助办法」的中央从「本人保护一份」改为从 @babel/runtime 中引入。
所以咱们须要将 @babel/plugin-transform-runtime 置为 devDependence,因为他在编译时应用。
将 @babel/runtime 置为 dependence,因为他在运行时应用。
总结
本文从底层向上介绍了前端日常业务开发会接触的 Babel 大家族成员。他们包含:
底层
@babel/core(由 @babel/parser、@babel/traverse、@babel/types、@babel/generator 等组成)
他们提供了 Babel 编译 JS 的能力。
注:这里 @babel/core 为库名,前文中 babel-core 为其在仓库中对应文件名
中层
@babel/plugin-*
Babel 对外裸露的 API,使开发者能够染指其编译 JS 的能力
下层
@babel/preset-*
日常开发会应用的插件汇合。
对于立志成为前端架构师的同学,Babel 是前端工程化的基石,学懂、会用他是很有必要的。
能看到这里真不容易,给本人鼓鼓掌吧。
参考资料
[1] Babel 仓库: https://github.com/babel/babel/tree/main/packages
[2] AST explorer: https://astexplorer.net/
[3] core-js 仓库: https://github.com/zloirock/core-js/tree/master/packages
[4] Browserslist: https://github.com/browserslist/browserslist
[5] Babel v7.4.0: https://babeljs.io/docs/en/babel-polyfill#docsNav
[6] babel-plugin-syntax-decorators: https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators
[7]babel playground: https://babeljs.io/repl#