解剖Babel —— 向前端架构师迈出一小步
source link: https://zhuanlan.zhihu.com/p/352878760
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
解剖Babel —— 向前端架构师迈出一小步
当聊到 Babel
的作用,很多人第一反应是:用来实现 API polyfill
。
事实上, Babel
作为前端工程化的基石,作用远不止这些。
作为一个庞大的家族, Babel
生态中有很多概念,比如: preset
、 plugin
、 runtime
等。
这些概念使初学者对 Babel
望而生畏,对其理解也止步于 webpack
的 babel-loader
配置。
本文会从 Babel
的核心功能出发,一步步揭开 Babel
大家族的神秘面纱,向前端架构师迈出一小步。
Babel是什么
Babel 是一个 JavaScript 编译器。
作为 JS
编译器, Babel
接收输入的 JS
代码,经过内部处理流程,最终输出修改后的 JS
代码。
在 Babel
内部,会执行如下步骤:
- 将
Input Code
解析为AST
(抽象语法树),这一步称为parsing
- 编辑
AST
,这一步称为transforming
- 将编辑后的
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" ]
代表:非 IE
11的版本以及所有 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#?browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYGwhgzhAECCAO9oG8C-Q&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=script&lineWrap=true&presets=env&prettier=false&targets=&version=7.13.7&externalPlugins=babel-plugin-transform-regenerator%406.26.0
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK