【转载】解剖Babel —— 向前端架构师迈出一小步

夏婆子
• 阅读 1625

解剖Babel —— 向前端架构师迈出一小步

当聊到Babel的作用,很多人第一反应是:用来实现API polyfill。

事实上,Babel作为前端工程化的基石,作用远不止这些。

作为一个庞大的家族,Babel生态中有很多概念,比如:preset、plugin、runtime等。

这些概念使初学者对Babel望而生畏,对其理解也止步于webpack的babel-loader配置。

本文会从Babel的核心功能出发,一步步揭开Babel大家族的神秘面纱,向前端架构师迈出一小步。

Babel是什么

Babel 是一个 JavaScript 编译器。

作为JS编译器,Babel接收输入的JS代码,经过内部处理流程,最终输出修改后的JS代码。
【转载】解剖Babel —— 向前端架构师迈出一小步

在Babel内部,会执行如下步骤:

1、将Input Code解析为AST(抽象语法树),这一步称为parsing
2、编辑AST,这一步称为transforming
3、将编辑后的AST输出为Output Code,这一步称为printing
【转载】解剖Babel —— 向前端架构师迈出一小步

从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 —— 向前端架构师迈出一小步

在babel-core内部:

  • babel-traverse可以通过「深度优先」的方式遍历AST树
  • 对于遍历到的每条路径,babel-types提供用于修改AST节点的节点类型数据
    所以,整个Babel底层编译能力由如下部分构成:
    【转载】解剖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/babe...

[2] AST explorer: https://astexplorer.net/

[3] core-js仓库: https://github.com/zloirock/c...

[4] Browserslist: https://github.com/browsersli...

[5] Babel v7.4.0: https://babeljs.io/docs/en/ba...

[6] babel-plugin-syntax-decorators: https://github.com/babel/babe...

[7]babel playground: https://babeljs.io/repl#

点赞
收藏
评论区
推荐文章
20pzqm 20pzqm
3年前
遇到几个前端小问题
babel问题babel是负责转换ES新版本语法到老版本js上的。比如解构语法...程序有如下报错SyntaxError:Unexpectedtoken8|letdataModulebuildfailed:Error:NoPostCSSConfigfoundin:E:\CODE\xxxxxx新建.pos
Jacquelyn38 Jacquelyn38
4年前
React.js中JSX的原理与关键实现
在开始开发之前,我们需要创建一个空项目文件夹。安装1.初始化npm init y2.安装webpack相关依赖npm install webpack webpackcli D3.安装babelloader相关依赖npm install babelloader @babel/core @babel/presetenv D4.
Stella981 Stella981
3年前
Babel配置中的presets、plugins、各个阶段stage的含义
什么是BabelBabel官方文档:https://babeljs.io/Babel 中文文档:https://www.babeljs.cn/我们知道各个浏览器对JavaScript版本的支持各不相同,很多新的语法无法直接在浏览器中运行,为了解决这个“沟通不畅”的问题,所以就有了Babel,Babel主要用于将ECMAScrip
Stella981 Stella981
3年前
ES6 系列之 Babel 是如何编译 Class 的(下)
_摘要:_ 前言在上一篇\《ES6系列Babel是如何编译Class的(上)》\(https://github.com/mqyqingfeng/Blog/issues/105),我们知道了Babel是如何编译Class的,这篇我们学习Babel是如何用ES5实现Class的继承。ES5寄生组合式继承\
Stella981 Stella981
3年前
Babel 和 PostCss 的一些基本配置
Babel是一个javascript编译器,PostCSS是一个样式转换工具。两者都可以看作是一个转化平台,我们可以在上面使用一些插件,来达到想要的代码转化。几乎每个前端项目都要使用它们。Babel配置.babelrc文件{"presets"://babe
Stella981 Stella981
3年前
Babel
Babel是JavaScript编译器,主要用于将ECMAScript2015版本的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器或其他环境中.中文官方地址https://www.babeljs.cn/(https://www.oschina.net/action/GoToLink?urlhttps%
Stella981 Stella981
3年前
Babel 学习笔记
在Github上Fork了别人的代码,在package.json中看到了很多与Babel有关的包,很是不解他们之间的关系。本文为Babel学习过程中记录的笔记,(建议直接去Babel官网(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fbabeljs.cn%2F
Stella981 Stella981
3年前
Babel总结
什么是babel?babel是一个JavaScript编译器。Babel是一个工具链,主要用于将ECMAScript2015代码转换为向后兼容的旧浏览器或环境中JavaScript版本。注解:传统的编译是指转化成可执行的代码,也就是二进制代码。但是对于前端来说,因为JS是解释性语言,对于浏览器或者Node来说就是可执行的代码。
Stella981 Stella981
3年前
Babel是如何读懂JS代码的
本文转载自:安秦 https://zhuanlan.zhihu.com/p/27289600概述稍微了解行业现状的开发者都知道,现在前端“ES6即正义”,然而浏览器的支持还是进行时。所以我们会用一个神奇的工具将ES6都给转换成目前支持比较广泛的ES5语法。对,说的就是Babel。本文不再介绍Babel是什么也不讲怎么用,这类
Stella981 Stella981
3年前
Babel:plugin、preset的区别与使用
关注“重度前端”助力前端深度学习━━━━━前言在现代大前端的生态环境下,相信大部分前端同行都用过babel,也知道babel的作用以及相关的配置和使用,本文不说如何配置和使用,老张想从一个宏观的角度去理解babel,理解它的机制,希望可以在后续的使用中不至于存在那么多的迷惑。本文概览简单
Easter79 Easter79
3年前
Tool系列—Babel
1、简单介绍     这些转换器(更准确地说是源代码到源代码的编译器)可以把你写的符合ECMAScript6标准的代码完美地转换为ECMAScript5标准的代码,并且可以确保良好地运行在所有主流JavaScript引擎中。    Babel对ES6的支持程度比其它同类更高,而且Babel拥有完善的文档和一个