解剖Babel —— 向前端架构师迈出一小步
当聊到Babel
的作用,很多人第一反应是:用来实现API polyfill
。
事实上,Babel
作为前端工程化的基石,作用远不止这些。
作为一个庞大的家族,Babel
生态中有很多概念,比如:preset
、plugin
、runtime
等。
这些概念使初学者对Babel
望而生畏,对其理解也止步于webpack
的babel-loader
配置。
本文会从Babel
的核心功能出发,一步步揭开Babel
大家族的神秘面纱,向前端架构师迈出一小步。
Babel是什么
作为JS
编译器,Babel
接收输入的JS
代码,经过内部处理流程,最终输出修改后的JS
代码。
在Babel
内部,会执行如下步骤:
-
将
Input Code
解析为AST
(抽象语法树),这一步称为parsing
-
编辑
AST
,这一步称为transforming
-
将编辑后的
AST
输出为Output Code
,这一步称为printing
从Babel仓库的源代码,可以发现:Babel
是一个由几十个项目组成的Monorepo
。
其中babel-core
提供了以上提到的三个步骤的能力。
在babel-core
内部,更细致的讲:
-
babel-parser
实现第一步 -
babel-generator
实现第三步
要了解第二步,我们需要简单了解下AST
。
AST的结构
进入AST explorer,选择@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仓库看到,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": [
"not IE 11",
"maintained node versions"
]
代表:非IE
11的版本以及所有Node.js
基金会维护的版本。
@babel/polyfill与core-js关系
@babel/polyfill
可以看作是:core-js
加regenerator-runtime
。
单独使用@babel/polyfill
会将core-js
全量导入,造成项目打包体积过大。
为了解决全量引入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提供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,输入:
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/plugin-*
Babel
对外暴露的API
,使开发者可以介入其编译JS
的能力
上层
@babel/preset-*
日常开发会使用的插件集合。
对于立志成为前端架构师的同学,Babel
是前端工程化的基石,学懂、会用他是很有必要的。
能看到这里真不容易,给自己鼓鼓掌吧。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!