解剖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 11maintained 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#