Babel初识
1.什么是Babel
简单讲Babel 是 Javascript 编译器 ,将 ES6,ES7 ,ES8 转换成 浏览器都支持的ES5 语法,并提供一些插件来兼容浏览器API的工具。
babel是怎么实现的呢:Babel 会将源码转换 AST(抽象语法树)之后,通过遍历AST树,对树做一些修改,然后再将AST转成code,即成源码。通俗讲就是整了个容,浏览器觉得挺漂亮的,让代码可以在不支持高版本es语法的浏览器上运行。如下:
// 转码前
input.map(item => item + 1);
// 转码后
input.map(function (item) {
return item + 1;
});
2.Babel的设计和组成
2.1 设计理念
从图上可以看到,Babel 会将我们写的代码转换AST 之后,通过遍历AST 树,对树做一些修改,然后再将AST转成 code,这样我们用的 箭头函数,let,const,解构赋值才能用的舒坦。
2.2 组成
Babel 的核心是在 @babel/core 这个npm 包,围绕在它周围的分别是
@babel/core
AST转换的核心@babel/cli
打包工具@babel/plugin
Babel 插件机制,Babel基础功能不满足的时候,可以用此@babel/preset-env
把许多 @babel/plugin 综合了下,减少配置@babel/runtime
把你使用到的浏览器某些不支持API,按需导入,代码少@babel/polyfill
把浏览器某些不支持API,兼容性代码全部导入到项目,不管你是不是用到,缺点是代码体积特别大.babelrc/babel.config.json
上面这些配置需要放在一个地方,.babelrc/babel.config.json就是放这些配置地方。
3.babel.config.js配置文件
3.1 简单示例
下面是.babelrc/babel.config.json
示例(具体配置方法可以有很多种,可以看官网):
{
"presets": [
["@babel/preset-env"]
],
"plugins": ["@babel/plugin-syntax-dynamic-import"],
]
}
项目根目录下.babelrc
或者babel.config.json
是 Babel的配置文件,Babel 会自动查找并读取内容。目录结构一般如下:
.
├──src
└──main.js
├──package.json
└──babel.config.json
下面来看一个例子,先安装依赖:
npm install --save-dev @babel/preset-env @babel/cli
配置babel:
{
"presets": ["@babel/preset-env"],
"plugins": []
}
@babel/preset-env
的参数项数量很多,但大部分我们都用不到。我们只需要重点掌握四个参数项即可:targets
、useBuiltIns
、modules
和corejs
。如果要使用配置参数,就需要对babel.config.js
做一些简单的修改:
{
"presets": [
[
// 将原来的 @babel/preset-env 字符串改成数组
"@babel/preset-env",
{ // @babel/preset-env 的配置对象
...
}
]
],
"plugins": []
}
3.2 browserslist(目标环境配置表)
在开始讲具体配置项的作用之前,我们先说说 browserslist(目标环境配置表) ,具体 demo 如下:
"browserslist": [
"> 1%",
"not ie <= 8"
]
上面配置的含义是:目标环境是市场份额大于 1% 的浏览器并且不考虑 IE8 以下的浏览器。
browserslist 可以写在 package.json 中,也可以单独写在工程目录下的 .browserslistrc 文件里。我们可以用 browserslist 来指定代码最重要运行在那些浏览器或者 Node.js 环境。如下是配置在package.json
里面的browserslist
示例:
browserslist 比较常见的一个作用就是帮助 Autoprefixer、postcss 判断是否要增加 CSS 前缀(例如 -webkit-)。我们的 Babel 也会用到 browserslist,当我们使用了 @babel/preset-env 这个预设,那么在 Babel 进行代码转换的过程中,就会读取 browserslist 的配置,从而按目标环境进行相应的转换。
如果我们的 @babel/preset-env 不设置任何参数,Babel 就会完全根据 browserslist 的配置来做语法转换。如果没有 browserslist ,那么 Babel 就会把所有 ES6 的语法转换成 ES5 版本。如下是一个例子:
@babel/preset-env 无配置且不配置 browserslist:如图所示,我们定义了一个箭头函数,babel 最后会将其转换为 ES5 语法的普通函数。
假如我们配置 browserslist:
当我们制定了 chrome 60 版本以后,babel 并没有转换箭头函数,因为这个版本的 chrome 本身就支持箭头函数:
3.3 @babel/preset-env中的配置参数
3.3.1 targets
该参数项可以取值为字符串、字符串数组或对象,不设置的时候取默认值空对象{}。
该参数项的写法与 browserslist 是一样的,如果我们对 @babel/preset-env 的 targets 参数项进行了设置,那么就不使用 browserslist 的配置,而是使用 targets 的配置。如不设置 targets ,那么就使用 browserslist 的配置。
3.3.2 useBuiltIns
useBuiltIns 项取值可以是usage、 entry 或 false。如果该项不进行设置,则取默认值 false。
- false
啥也不干 - entry
必须在项目中主动引入@babel/polyfill,打包后会把所有的 polyfill 都引入 - usage
main.js 主动引入 @babel/polyfill;打包后只会把用到的 polyfill 引入
3.3.3 corejs
这一块可以看看官网。
该参数项的取值可以是 2 或 3,没有设置的时候取默认值为 2。这个参数只有 useBuiltIns 参数为 usage 或者 entry 时才会生效。
因为某些新的 API 只有 core-js@3 里才有,例如数组的 flat 方法,我们需要使用 core-js@3 的 API 模块进行补齐,这个时候我们就把该项设置为 3。
当我们将 corejs 配置为 2 或者使用默认配置时,需要安装 core-js@2 并引入,也可以安装并引入 @babel/polyfill(内部引入了 core-js2,又集成了 regenerator-runtime)。 如果我们的代码中使用了,生成器函数(Generator Functions)、async、await ,我们就需要引入 regenerator-runtime 模块。
从 Babel 7.4.0 开始,@babel/polyfill 软件包已被弃用, 所以还是建议使用 core-js@3。使用 core-js@3 时必须预先安装 core-js@3, 如果 useBuiltIns 是 entry, 则在入口文件引入 core-js@3 ,如果 useBuiltIns 是 usage,则不需要在入口文件引入。
3.3.4 modules
这个参数项的取值可以是 amd、umd、systemjs、commonjs、cjs、auto、false。在不设置的时候,取默认值 auto 。
在该参数项值是 auto 或不设置的时候,会发现我们转码前的代码里 import 都被转码成 require 了。
如果我们将参数项改成 false,那么就不会对 ES6 模块化进行更改,还是使用 import 引入模块。使用 ES6 模块化语法有什么好处呢?在使用 Webpack 一类的打包工具,可以进行静态分析,从而可以做 tree shaking 等优化措施。
4.presets VS plugin
这篇聊下Babel的@babel/babel-preset-*
预设插件, @babel/babel-plugin-*
我下面的例子都是以 Babel7讲的。
4.1 presets是什么
presets 是 plugins 的集合,把很多需要转换的ES6的语法插件集合在一起,避免大家各种配置,比如:
最常用的 presets 是 @babel/preset-env, @babel/preset-env 包含了我们大部分能想到的 ES6 语法,没有的,它会在打包报错,自己加 plugins, 用过 Babel6的可能会看到 stage-0,stage-1 这样的配置,但在 Babel已经全部舍弃,具体 Babel6 和 Babel7差别,可以看看官网,有空我会再写一篇文章。
4.2 presets和plugins的加载顺序
presets 加载顺序和一般理解不一样 ,是倒序的,比方上方代码中,先加载 @babel/preset-react,再加载 @babel/preset-env。
而plugins 按照数组的 index 增序(从数组第一个到最后一个)进行编译
另外plugins 优先于 presets进行编译。
5.@babel/polyfill VS @babel/plugin-transform-runtime
5.1 @babel/polyfill
这篇主要讲polyfill和runtime。 总结下, Babel只是转换syntax
层语法,所以需要@babel/polyfill
来处理API兼容,又因为polyfill
体积太大,所以通过preset
的useBuiltIns
来实现按需加载,再接着为了满足npm 组件开发的需要出现了@babel/runtime
来做隔离。
下面来看一段代码:
let array = [1, 2, 3, 4, 5, 6];
array.includes(item => item > 2);
new Promise()
Babel转换后代码:
var array = [1, 2, 3, 4, 5, 6];
array.includes(function (item) {
return item > 2;
});
new Promise()
Babel 默认只是转换了 箭头函数 let ,Promise 和 includes 都没有转换 ,这是为什么?
Babel把Javascript语法分为 syntax和 api。
- api
api 指那些我们可以通过 函数重新覆盖的语法 ,类似 includes,map,includes,Promise,凡是我们能想到重写的都可以归属到 api - syntax
像 箭头函数,let,const,class, 依赖注入 Decorators,等等这些,我们在 Javascript 在运行是无法重写的,想象下,在不支持的浏览器里不管怎么样,你都用不了 let 这个关键字。
Babel只负责转换 syntax , 而includes,map,includes这些 API层面的则交给polyfill 这个模块单独处理。
polyfill直译是垫片的意思,处理类似 assign,includes,map,includes。某些浏览器如果没有这些方法,最直接的办法的是根据一份浏览器不兼容的表格(这个browserslist已经完成了),把对应浏览器不支持的语法全部重新写一遍,类似下面这样:
//
if (typeof Object.assign != 'function') {
Object.defineProperty(Object, "assign",
·····
}
if (!Array.prototype.includes){
Object.defineProperty(Array.prototype, 'includes',
·····
}
if (!Array.prototype.every){
Object.defineProperty(Array.prototype, 'every',
·····
}
.....好多好多
这种方式可以简单粗暴的解决兼容性问题, 那问题也来了,这样会导致 polyfill.js 这个包非常大。
于是大佬们想到了按需加载。用到了includes,Babel就只引入 includes 的对应的处理代码,按需加载。
这个就需要用到上面3.3.2
说到的@babel/preset-env的useBuiltIns 属性了,不了解 @babel/preset-env 看下上篇 useBuiltIns 有 false,entry,usage 三个属性。
,使用entry配置时候,需要手动在src/main.js 中手动@babel/polyfill:
在package.json中的script配置打包命令:
执行npm run build
后在dist文件夹下新生成一个bundle.js打包文件。该文件把@babel/polyfill里面所有内容都引入了:
这样子体积太大了。
把@babel/preset-env
中useBuiltIns
改为usage
,并且删除src/main.js
引入@babel/polyfill
的的代码:
执行npm run build
后在dist文件夹下新生成一个bundle.js打包文件。由于usage
是按需加载,所以bundle.js的文件中只用导入几个文件:
5.2 @babel/plugin-transform-runtime
polyfill问题是已经解决了,现在又出现了一个场景, 假设你是开发一个 npm组件的选手,你刚好用到了 Promise 方法, 按照上面的方法,写完发布到npm仓库,现在隔壁印度小哥刚好搜到你这包,下载下来了,但是他的项目里面也用到了 Promise ,但是他的Promise是自定义 一套,类似:
window.Promise = function (){
this.reject = ..
this.resolve = ..
}
这个时候就傻眼了,小哥项目跑不起来了。,这个场景其实很常见,那这么办呢?
假设在开发组件的时候能报把所有的Promise 拷贝(copy)到 _Promise对象上,然后组件里都用_Promise ,就和外界做了层隔离,互不影响。这个思路我们在开发设计中也是可以学习套用的,Babel 里面针对这种场景出现了@babel/runtime
, @babel/plugin-transform-runtime
,首先 执行下,这2个一个都不能少,都是必须的:
npm install --save @babel/runtime
npm install --save-dev @babel/plugin-transform-runtime
配置babel.config.json:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2
}
]
]
}
执行npm run build
后在dist文件夹下新生成一个bundle.js打包文件。打包文件内容如下:
5.3 总结
Babel
只是转换syntax
层的语法- 所以需要
@babel/polyfill
来处理API
兼容;又因为polyfill
体积太大,所以通过preset
的useBuiltIns
来实现按需加载 - 再接着为了满足
npm
组件开发的需要 出现了@babel/runtime
来做隔离
Polyfill和Runtime的区别:
- Polyfill:
Polyfill主要用于解决浏览器不支持的ES6新特性问题。它通过向全局对象和内置对象的prototype上添加方法来实现兼容性。例如,如果运行环境中不支持Array.prototype.find方法,引入polyfill后就可以使用ES6方法来编写代码,但缺点是会造成全局空间污染。 - Runtime:
Runtime(如babel-runtime)则是将ES6代码编译成ES5代码来执行。它不会污染全局对象和内置对象的原型。