前言
Babel
是一个转译器,感觉相对于编译器compiler,叫转译器transpiler更准确,因为它只是把同种语言的高版本规则翻译成低版本规则,而不像编译器那样,输出的是另一种更低级的语言代码。
babel 的微内核架构
Babel
和 Webpack
为了适应复杂的定制需求和频繁的功能变化,都使用了微内核 的架构风格。也就是说它们的核心非常小,大部分功能都是通过插件扩展实现的。
@babel/* 原理
babel 是一个 MonoRepo
项目, 不过组织非常清晰,下面就源码上我们能看到的模块进行一下分类, 配合上面的架构图让你对babel有个大概的认识:
babel 核心 一一 @babel/core
工具 | 功能 | 描述 |
---|---|---|
@babel/parser | 解析 | 接受源码,进行词法分析、语法分析,生成AST。内置支持很多语法,JSX、Typescript、Flow、以及最新的ECMAScript规范,但不支持扩展 |
@babel/traverse | 转换 | 接受源码,进行词法分析、语法分析,生成AST |
@babel/generator | 生成 | 接受最终生成的AST,并将其转换为代码字符串,同时此过程也可以创建source map |
转译处理流程
相关逻辑
- 1、加载和处理配置(config)
- 2、加载插件
@babel/babel-plugin-*
- 3、调用
@babel/parser
进行语法解析,生成 AST - 4、调用
@babel/traverser
遍历AST,并使用访问者模式应用插件@babel/babel-plugin-*
对 AST 进行转换 - 5、调用
@babel/generator
生成代码,包括SourceMap转换和源代码生成
着重讲解和编译器类似Babel的转译过程分为三个阶段是: 解析(parse),转换(transform),生成(generate).
解析 解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:
词法分析(Lexical Analysis)
:词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流语法分析(Syntactic Analysis)
:语法分析阶段会把一个令牌流转换成 AST 的形式
转换 转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作,这个过程需要各种插件进行配合操作。
生成 代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。.
代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串
以ES6代码转译为ES5代码为例,babel转译的具体过程如下:
ES6代码输入 ==》 babel进行解析 ==》 得到AST
==》 通过@babel/traverse
调用对AST树进行遍历转译 ==》 得到新的AST树 ==》 用@babel/generator通过AST树生成ES5代码
babel 插件
语法插件
@babel/plugin-syntax-*
:由于@babel/parser内置支持了很多 JavaScript 语法特性,Parser也不支持扩展.因此通过 因此plugin-syntax-*
开启或配置某个功能特性
转换插件
@babel/plugin-transform-*
:普通的转换插件@babel/plugin-proposal-*
: 还在'提议阶段'(非正式)的语言特性
预定义集合
- @babel/presets-*
babel 工具
工具 | 描述 | 备注 |
---|---|---|
@babel/cli | @babel/cli依赖@babel/core,允许使用CLI命令行方式编译文件 | npx babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-classes |
@babel/node | 依赖@babel/core & @babel/preset-env ,替代 CLI 中的 node 命令,可以直接运行采用 ES6 语法编写的代码(实际上是先编译再执行node脚本) | npx babel-node ./src/index.js --presets @babel/preset-env |
@babel/register | 实际上为require加了一个钩子(hook),之后所有被 node 引用的 .es6、.es、.jsx 以及 .js 文件都会先被 Babel 转码 | |
@babel/helper | 辅助代码,单纯的语法转换可能无法让代码运行起来,比如低版本浏览器无法识别class关键字,这时候需要添加辅助代码,对class进行模拟 |
@babel/register
和 @babel/node
都非常好用,但是由于二者都是实时转码,因而性能上会有一定影响。官方建议将二者仅置于开发环境下使用。而在正式生产环境中部署时,预先编译代码是值得推荐的做法.
babel 使用
安装babel 命令行工具@babel/cli
, 就可以直接在终端执行编译
npm install -D @babel/cli @babel/core
复制代码
三种编译方式
// 方式一: 通过命令行编译解析
./node_modules/.bin/babel src --out-dir lib
// 方式二: 通过package.json 中 scripts 配置
{
scripts: {
build:"babel src --out-dir lib"
}
}
// 方式三:用`npx babel`来代替`./node_modules/.bin/babel` npx 其实是`npm`的包运行器
npx babel src --out-dir lib
复制代码
// 源代码
const fn = () => 1; // 箭头函数, 返回值为1
console.log(fn());
// 输出代码 (原样输出)
const fn = () => 1; // 箭头函数, 返回值为1
console.log(fn());
复制代码
使用指定babel 插件 一一 @babel/plugin-transform-*
// 1、安装插件
npm i --save-dev @babel/plugin-transform-arrow-functions
// 2、 源代码
const fn = () => 1; // 箭头函数, 返回值为1
console.log(fn());
// 3、编译
npx babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
// 4、输出结果
const fn = function () {
return 1;
};
console.log(fn());
复制代码
如果是一两个插件,简单使用这个命令行工具就解决了。但是ES6、ES7新增了很多功能,所以如果有一些预设配置就好了。于是有了
使用预定义的集合 一一 @babel/preset-env
// 1、安装
npm i --save-dev @babel/preset-env
// 2、 源代码
const fn = () => 1; // ES6 箭头函数, 返回值为1
let num = 3 ** 2; // ES7 求幂运算符
let foo = function(a, b, c, ) { // ES7 参数支持尾部逗号
console.log('a:', a)
console.log('b:', b)
console.log('c:', c)
}
foo(1, 3, 4)
console.log(fn());
console.log(num);
// 3、编译
npx babel src --out-dir lib --presets=@babel/preset-env
// 4、输出结果
"use strict";
var fn = function fn() {
return 1;
};
var num = Math.pow(3, 2);
var foo = function foo(a, b, c) {
console.log('a:', a);
console.log('b:', b);
console.log('c:', c);
};
foo(1, 3, 4);
console.log(fn());
console.log(num);
复制代码
相信到此你对Babel的功能有一定了解了吧, 但是真正使用起来我们不可能都是靠命令行+参数的形式吧, 没错, 接下来我要将这些功能做成配置项.
使用babel 配置文件 一一 babel.config.js
执行命令时,@babel/core
默认会去寻找根目录下的一个名为babel.config.js
或者babelrc.js
的文件或者packages的babel属性读取配置信息
// babel.config.js
const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
chrome: "64",
firefox: "60",
safari: "11.1"
}
}
]
]
module.exports = { presets };
复制代码
// package.json
{
"scripts": {
"build": "babel src -d lib"
}
}
复制代码
实战
想要转译 ECMAScript的新语法 + 新API,可以有以下两套方案:
方案一:@babel/preset-env + @babel/polyfill
方案二:@babel/preset-env + @babel/plugin-transform-runtime + @babel/runtime-corejs2
在解释方案之前,我们得先理解 babel 在转译的时候,会将源代码分成 syntax 和 api 两部分来处理:
- syntax:类似于展开对象、optional chain、let、const 等语法
- api:类似于 [1,2,3].includes 等函数、方法
const 这种语法为 syntax,includes 这种方法为 api。可以看到,syntax 很轻松就转好了,但是 api 默认并没有做任何处理。babel 转译后的代码如果在不支持 includes 这个方法的浏览器里运行,就会报错。
@babel/preset-env
提供转译 ES 新语法,剩下的事情(即 ES 的新 API,例如:Proxy 转译)才是 @babel/polifill
或 @babel/plugin-transform-runtime
需要去解决的事情
使用@babel/polyfill
转换
根据useBuiltIns 参数配置的不同,其处理结果也不一样:
useBuiltIns | 功能 | 描述 | 优缺点 |
---|---|---|---|
false | 对api 不处理 | 即代码转译后不会处理类似includes这些新标准的API ,原样输出。在js代码第一行引入 import '@babel/polyfill' | 最安全,但打包体积会大一些,一般不选用 |
entry | 入口处引入@babel/polyfill | 需要在源代码的最上方手动引入@babel/polyfill 这个库(该库一共分为两部分,第一部分是 core-js,第二部分是 regenerator-runtime | 基本覆盖,足够安全且代码体积不是特别大 |
usage | 按需加载 | 不需要手动引入 @babel/polyfill | 代码书写规范,且信任第三方包的时候,可以使用! |
entry
全局引入实现api处理
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry", // 入口处引入@babel/polyfill
"debug": true
}
]
]
}
复制代码
转换前后对比:
这种模式下,babel 会将所有的 polyfill 全部引入,这样会导致结果的包大小非常大,而我们这里仅仅需要 includes 一个方法而已。
usage
实现按需加载
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"debug": true
}
]
]
}
复制代码
总结:babel 在转译的过程中,对 syntax 的处理可能会使用到 helper 函数,对 api 的处理会引入对应的垫片(polyfill)。
再来看下一个更复杂情况,使用到class、async generator这些语法时:
转译过程中像 class
、async
这类语法中,babel 自定义了 _classCallCheck
这个helper函数来辅助,类似的helper函数在每个被转译后的文件里都会被定义了一遍,这样显然不合理.
使用@babel/plugin-transform-runtime
转换
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"debug": true
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3 // 指定 runtime-corejs 的版本,目前有 2 3 两个版本
}
]
]
}
复制代码
以下图结果来看,通过@babel/plugin-transform-runtime
& @babel/runtime-corejs3
就可以解决上面两个问题:
- api 从之前的直接修改原型改为了从一个统一的模块中引入,避免了对全局变量及其原型的污染,解决了第一个问题
- helpers 从之前的原地定义改为了从一个统一的模块中引入,使得打包的结果中每个 helper 只会存在一个,解决了第二个问题
根据corejs配置的不同
corejs option | Install command | 描述 |
---|---|---|
false | npm install --save @babel/runtime | 提供转换之后,运行时需要的的helper函数 和regenerator-runtime |
2 | npm install --save @babel/runtime-corejs2 | 依赖core-js@2 和regenerator-runtime |
3 | npm install --save @babel/runtime-corejs3 | 依赖core-js@3 和regenerator-runtime |
结论
在实际生产开发中验证,二者的配置、原理总结如下:
方案 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
@babel/polyfill | 依赖core-js@2 和regenerator-runtime ,完整模拟 ES2015+ 环境 | 打包体积过大、且污染全局对象和内置的对象原型。开发非第三方包项目可以一劳永逸 | 开发第三方包时不建议使用 |
@babel/plugin-transform-runtime | 不污染原型链,且可以实现按需引入, 打包体积小。 | 不能兼容实例方法 | 建议开发第三方包时使用 |
@babel/polyfill
vs @babel/runtime-corejs2
vs @babel/runtime-corejs3
@babel/polyfill 和@babel/runtime-corejs2 都使用了 core-js(v2)这个库来进行 api 的处理。core-js(v2)这个库有两个核心的文件夹,分别是 library 和 modules。@babel/runtime-corejs2 使用 library 这个文件夹,@babel/polyfill 使用 modules 这个文件夹。
- library 使用 helper 的方式,局部实现某个 api,不会污染全局变量;
- modules 以污染全局变量的方法来实现 api;
- library 和 modules 包含的文件基本相同,最大的不同是_export.js 这个文件:
复制代码
关于
@babel/runtime-corejs2
vs@babel/runtime-corejs3
区别类似core-js@2
vscore-js@3
详细分析过程 可参考这里
core-js@2
vs core-js@3
core-js@2
一个最常见的问题就是包的体积太大(~2M),并且有很多重复的文件被引用。基于这个原因,core-js@3使用Monorepo
对包进行拆分管理,三个核心的包分别是
- core-js:定义全局的polyfill(~500k, 40k minified and gzipped)
- core-js-pure:提供不污染全局环境的polyfill,等价于core-js@2/library(~440k)
- core-js-compat:包含了core-js模块和API必要的数据,通过browserslist来生成所需要的core-js模块的列表
写在最后??
作业:手动实现一个babel插件