携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
简介
我们都知道,babel
是用来编译js
的,就是把高版本的js
编译成低版本的js
,以便浏览器识别。但是对于babel
更深入点可能就不是很清楚了,所以笔者今天再来简单总结下
看完本文你将学到:
- 知道babel的核心包
- 怎么配置和使用babel
- babel polyfill概念以及使用
- babel结合webpack的使用
Babel 是什么?
Babel
是一个 JavaScript
编译器。
Babel
是一个工具链,主要用于将采用 ECMAScript 2015+
语法编写的代码转换为向后兼容的 JavaScript
语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
下面列出的是 Babel
能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性
babel 的编译流程
babel 是 source to source 的转换,整体编译流程分为三步:
- parse:通过 parser 把源码转成抽象语法树(AST)
- transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
- generate:把转换后的 AST 打印成目标代码,并生成 sourcemap
简单总结一下就是:为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。
核心库 @babel/core
@babel/core
是babel
最核心的一个编译库,他可以将我们的代码进行词法分析--语法分析--语义分析过程从而生成AST
抽象语法树,从而对于“这棵树”的操作之后再通过编译成为新的代码。
CLI命令行工具 @babel/cli
@babel/cli
是babel
提供的命令行工具,它主要是提供 babel
这个命令。
安装了@babel/cli
后我们就可以使用babel
命令来编译js
文件了。将src
目录下的js
编译到lib
目录下。
./node_modules/.bin/babel src --out-dir lib
普通编译
为了更方便的操作我们创建一个项目
mkdir babeltest
然后创建package.json
cd babeltest
npm init
然后来安装下babel
的两个包
npm install --save-dev @babel/core @babel/cli
然后创建需要编译的源文件,在src
目录下创建index1.js
// src/index1.js
const fn = () => 1; // ES6箭头函数, 返回值为1
console.log(fn());
在package.json
定义编译脚本
"scripts": {
"index1": "babel src/index1.js --out-dir dist",
},
运行脚本进行编译
我们运行npm run index1
,就会执行babel
的编译,会把index1.js
进行编译。我们来看看编译后的效果。
啊,啥都没变,编译前后的代码是完全一样的,这是咋回事?
因为 Babel
虽然开箱即用,但是什么动作也不做,如果想要 Babel
做一些实际的工作,就需要为其添加插件(plugin
)或者预设(preset
)。
好吧,我们先来说说插件
使用插件进行编译
还是上面的例子,我们来使用帮助我们进行编译。
因为我们的源代码使用了es6
的箭头函数,所以我们安装一个转换箭头函数的插件@babel/plugin-transform-arrow-functions
npm install --save-dev @babel/plugin-transform-arrow-functions
插件虽然安装好了,但是要怎么使用呢?这就需要用到babel
的配置文件啦!我们创建一个babel.config.json
文件(需要 v7.8.0
或更高版本),并在plugins
里面配置好我们安装的插件就可以啦。
// babel.config.json
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
箭头函数被转换成普通函数啦,达到我们预期的效果啦。
我们再来添加一个es6
的新特性,解构赋值
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
可以发现,由于我们只安装了转换箭头函数的插件,所以它只转换了箭头函数,对于解构这个新特性并没有进行编译。
天啊,ES
的新语法这么多,不会要我们一个一个去安装插件吧,那何时才能配置完呀?
关于插件,我们可以在插件列表查看所有的babel
插件。
其实babel
早就为我们考虑到了,预设(preset
)能完美解决这个问题。
那预设又是什么呢?
使用预设进行编译
简单理解,预设就是一组插件,相当于你只要安装了我这么一个预设,就能享受到我这个预设里面所有的插件。
官方 Preset
有如下几个
- @babel/preset-env,将高版本js编译成低版本js
- @babel/preset-flow,对使用了flow的js代码编译成js文件
- @babel/preset-react,编译react的jsx文件
- @babel/preset-typescript,将ts文件编译成js文件
下面我们使用@babel/preset-env
这个预设来进行编译。
@babel/preset-env
主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill
,在不进行任何配置的情况下,@babel/preset-env
所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段),将其转换成ES5
代码。
首先我们安装@babel/preset-env
这个预设
npm install --save-dev @babel/preset-env
然后在babel.config.json
进行配置
// babel.config.json
{
"presets": ["@babel/preset-env"]
}
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
可以看到,我们的解构语法也被转换好了。
需要说明的是,@babel/preset-env
会根据你配置的目标环境,生成插件列表来编译。对于基于浏览器或 Electron
的项目,官方推荐使用 .browserslistrc
文件来指定目标环境。默认情况下,如果你没有在 Babel
配置文件中(如babel.config.json
)设置 targets
或 ignoreBrowserslistConfig
,@babel/preset-env
会使用 browserslist
配置源。
.browserslistrc
默认值是 > 0.5%, last 2 versions, Firefox ESR, not dead。
如果你不是要兼容所有的浏览器和环境,推荐你指定目标环境,这样你的编译代码能够保持最小。
所以我们配置下目标环境,只需要兼容最近的两个Chrome
版本。
// babel.config.json
{
"targets": "last 2 Chrome versions"
}
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
可以发现,源码和编译后的代码居然是一样的。为什么呢?因为最近的两个Chrome
版本它是原生支持箭头函数和解构赋值的所以根本就不需要进行编译成低版本的js
代码。
所以对于目标环境的配置是非常重要的。配置的好能大大减小我们代码的体积。
插件和预设的执行顺序
-
插件在预设前运行。
-
插件顺序从前往后排列。
-
预设顺序是从后往前(颠倒的)。
例如:
{
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
先执行 transform-decorators-legacy
,在执行 transform-class-properties
。
重要的时,preset
的顺序是 颠倒的。如下设置:
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
将按如下顺序执行: 首先是 @babel/preset-react
,然后是 @babel/preset-env
。
插件和预设的参数
插件和预设都可以接受参数,参数由插件名和参数对象组成一个数组,可以在配置文件中设置。
如果不指定参数,下面这几种形式都是一样的:
{
"plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]
}
要指定参数,请传递一个以参数名作为键(key)的对象。
{
"plugins": [
[
"transform-async-to-module-method",
{
"module": "bluebird",
"method": "coroutine"
}
]
]
}
预设的设置参数的方式和插件完全相同:
{
"presets": [
[
"env",
{
"loose": true,
"modules": false
}
]
]
}
babel的配置文件
babel
的配置文件支持很多种格式。
babel.config.json
官方建议使用 babel.config.json
格式的配置文件。
{ "presets": [...], "plugins": [...] }
babel.config.js
module.exports = function (api) {
api.cache(true);
const presets = [ ... ];
const plugins = [ ... ];
return {
presets,
plugins
};
}
.babelrc.json
{ "presets": [...], "plugins": [...] }
.babelrc.js
const presets = [];
const plugins = [];
module.exports = { presets, plugins };
.babelrc
{
"presets": [],
"plugins": []
}
还可以放到package.json
{
"name": "my-package",
"version": "1.0.0",
"babel": { "presets": [ ... ], "plugins": [ ... ], }
}
@babel/polyfill
@babel/polyfill 模块包含 core-js 和一个自定义的 regenerator runtime 来模拟完整的 ES2015+ 环境。(不包含第4阶段前的提议)。
这里的第4阶段前的提议不包括是什么意思呢?
这就需要了解一个新语法的诞生过程了。我们知道,ES
每年都会更新,那这些新特性是怎么推出来的呢?
其实新语法的诞生包含五个过程。它不是一蹴而就而是一步一步诞生出来的。
- Stage 0 - 设想(Strawman):只是一个想法,可能有 Babel插件。
- Stage 1 - 建议(Proposal):这是值得跟进的。
- Stage 2 - 草案(Draft):初始规范。
- Stage 3 - 候选(Candidate):完成规范并在浏览器上初步实现。
- Stage 4 - 完成(Finished):将添加到下一个年度版本发布中。
所以,只有当到了Stage 4
才是确定要新增的新特性,所以@babel/polyfill
才会支持。
说了这么多@babel/polyfill
到底是个啥?我还是不太明白。
其实,说直白点,@babel/polyfill
就是一个垫片。因为语法转换只是将高版本的语法转换成低版本的,但是新的内置函数、实例方法无法转换。这时,就需要使用 polyfill
上场了,顾名思义,polyfill
的中文意思是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。
比如说我们需要支持String.prototype.include
,在引入babelPolyfill
这个包之后,它会在全局String
的原型对象上添加include
方法从而支持我们的Js Api
。
我们说到这种方式本质上是往全局对象/内置对象上挂载属性,所以这种方式难免会造成全局污染。
下面笔者演示下@babel/polyfill
的使用。
笔者创建了一个index2.js
文件,里面使用了新的includes
方法
这里我们只使用了@babel/preset-env
预设来进行代码的编译
// babel.config.json
{
"presets": ["@babel/preset-env"]
}
我们编译看看编译后的代码
发现居然编译后的代码和源代码基本上一样,这在低版本浏览器显然是运行不了的,因为低版本浏览器肯定是不支持新特性includes
方法。
所以就需要使用到@babel/polyfill
啦
首先,安装 @babel/polyfill
依赖:
npm install --save @babel/polyfill
我们需要将完整的 polyfill
在代码之前加载,修改我们的 src/index2.js
,在最开始引入@babel/polyfill
然后我们再次编译
可以看到,编译后的代码就是把@babel/polyfill
全部引入了。这样固然是不会再报错了,不过,很多时候,我们未必需要完整的 @babel/polyfill
,这会导致我们最终构建出的包的体积增大,@babel/polyfill
的包大小为99K
。
我们更期望的是,如果我使用了某个新特性,再引入对应的 polyfill
,避免引入无用的代码。
配置@babel/preset-env实现按需引入
@babel/preset-env
是支持配置polyfill
的,并且支持按需和全量引入。
在babel-preset-env
中存在一个useBuiltIns
参数,这个参数决定了如何在preset-env
中使用@babel/polyfill
。
false
当我们使用preset-env
传入useBuiltIns
参数时候,默认为false
。它表示仅仅会转化最新的ES
语法,并不会转化任何Api
和方法。
entry
当传入entry
时,需要我们在项目入口文件中手动引入一次core-js
,它会根据我们配置的浏览器兼容性列表(browserList
)然后全量引入不兼容的polyfill
。
如果是Babel7.4.0
之前,我们需要在入门文件引入@babel/polyfill
// core-js 2.0中是使用"@babel/polyfill"
import "@babel/polyfill";
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8];
const result1 = arr1.includes(8);
console.log(result1);
如果是Babel7.4.0
之后,我们需要在入门文件引入core-js/stable
和regenerator-runtime/runtime
// core-js3.0版本中变化成为了下面两个包
import "core-js/stable";
import "regenerator-runtime/runtime";
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8];
const result1 = arr1.includes(8);
console.log(result1);
这种方式就跟我们前面说的使用@babel/polyfill
差不多了,不管用没用到都引入,肯定会加大构建后包的体积。
usage
当我们配置useBuintIns:usage
时,会根据配置的浏览器兼容,以及代码中 使用到的Api
进行引入polyfill
按需添加。
当使用usage
时,我们不需要额外在项目入口中引入polyfill
了,它会根据我们项目中使用到的自动进行按需引入。
所以,如果我们想实现按需引入,我们肯定要配置成usage
。
// babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
]
}
我们来看看编译后的效果
只引入了我们需要的core-js/modules/es.array.includes.js
,这样就达到了按需引入的目的。
配置@babel/runtime和@babel/plugin-transform-runtime实现按需引入
除了前面说的配置@babel/preset-env
来实现按需引入,我们还可以使用@babel/runtime
和@babel/plugin-transform-runtime
来实现按需引入。
@babel/runtime
简单来讲,@babel/runtime
更像是一种按需加载的解决方案,但是babel-runtime
会将引入方式由智能完全交由我们自己,我们需要什么自己引入什么。比如哪里需要使用到Promise
,就需要手动在文件顶部添加import promise from 'babel-runtime/core-js/promise'
。
它的用法很简单,只要我们去安装npm install --save @babel/runtime
后,在需要使用对应的polyfill
的地方去单独引入就可以了。比如:
// 如果需要使用Promise 我们需要手动引入对应的运行时polyfill
import Promise from 'babel-runtime/core-js/promise'
const promsies = new Promise()
到这里能看出来@babel/runtime
的问题了吧,虽然能实现按需引入,但是全部得手动处理,这谁顶得住。
@babel/plugin-transform-runtime
所以就有了@babel/plugin-transform-runtime
插件,这个插件能帮助我们自动按需引入,而不再手动了,是不是很爽,我们来使用下。
首先安装
npm i @babel/plugin-transform-runtime -D
因为@babel/plugin-transform-runtime
会使用到@babel/runtime
所以请确保系统中也安装了@babel/runtime
。
然后配置,在这里我们没有配置@babel/preset-env
的useBuiltIns
参数而是配置的@babel/plugin-transform-runtime
插件
// babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": [["@babel/plugin-transform-runtime", { "corejs": "3" }]]
}
我们再来变一下,看下效果
可以发现,它自动引入了我们需要的东西,并且重命名了。为什么重命名呢?好什么好处呢?
重命名后的好处就是 polyfill
不污染全局。plugin-transform-runtime
提供的runtime
形式的polyfill
都是这种形式。
并且,@babel/plugin-transform-runtime
还有个功能,就是能实现代码的重用。
比如我们有这样一个源码
class People {}
直接使用@babel/preset-env
编译后的效果如下
使用添加@babel/plugin-transform-runtime
插件并配置后的编译效果如下
可以发现,使用@babel/preset-env
它会给我们的代码中定义一个 _classCallCheck()
工具函数,这些工具函数的代码会包含在编译后的每个文件中。如果我们项目中存在多个文件使用了class
,那么无疑在每个文件中注入这样一段冗余重复的工具函数将是一种灾难。
但是使用@babel/plugin-transform-runtime
插件,他是辅助函数是从runtime
包中引入的,所以能减小构建后包的体积。
所以总结@babel/plugin-transform-runtime
插件优势就是
- 抽离重复注入的
helper
代码,减少产物体积 polyfill
不污染全局
然后我就想,既然这种形式不会污染变量,那当然能用就用这种了,答案是否定的,具体使用还是得根据实际情况来。
runtime 不污染全局变量,但是会导致多个文件出现重复代码。
写类库的时候用runtime,系统项目还是用polyfill。
写库使用 runtime 最安全,如果我们使用了 includes,但是我们的依赖库 B 也定义了这个函数,这时我们全局引入 polyfill 就会出问题:覆盖掉了依赖库 B 的 includes。如果用 runtime 就安全了,会默认创建一个沙盒,这种情况 Promise 尤其明显,很多库会依赖于 bluebird 或者其他的 Promise 实现,一般写库的时候不应该提供任何的 polyfill 方案,而是在使用手册中说明用到了哪些新特性,让使用者自己去 polyfill。
话说的已经很明白了,该用哪种形式是看项目类型了,不过通常对于一般业务项目来说,还是plugin-transform-runtime
处理工具函数,babel-polyfill
处理兼容。
也就是说使用@babel/preset-env
配置usage
来按需引入polyfill
,并配置plugin-transform-runtime
来抽取公共方法减少代码整体体积。
在webpack中的应用
前面讲的是使用babel-cli
来编译js
,但在实际项目开发过程中都不会直接使用babel-cli
来编译js
,一般会结合webpack
等一些构建工具来使用。下面笔者来说说使用webpack
编译js
的流程。
创建项目
首先我们创建一个文件夹,然后初始化package.json
文件。
// 创建webpacktest文件夹
mkdir webpacktest
// 进入webpacktest文件夹
cd webpacktest
// 创建package.json
npm init
创建源文件
在根目录下创建src
目录,并创建index.js
文件。
const say = () => {
console.log("hello world");
};
say();
安装webpack 和 webpack-cli
我们本地安装webpack 和 webpack-cli
npm i webpack webpack-cli -D
安装babel相关包
使用webpack
构建的话我们就不需要再安装@babel/cli
了,我们另外单独安装babel-loader
就可以了。
npm i @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader -D
配置webpack.config.js
然后我们在根目录下创建webpack.config.js
文件,并做如下配置
module.exports = {
mode: "development",
module: {
rules: [
// js和jsx 配置
{
test: /\.jsx?$/,
use: ["babel-loader"],
},
],
},
};
用babel-loader
来处理js
和jsx
文件。
配置babel.config.json
使用@babel/preset-env
来编译js
,并添加polyfill
。使用@babel/plugin-transform-runtime
来抽离公共的辅助编译方法,减少构建后包的体积。
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
],
"plugins": [["@babel/plugin-transform-runtime"]]
}
按需配置 browserslistrc
.browserslistrc
默认值是 > 0.5%, last 2 versions, Firefox ESR, not dead。
babel
的编译如果没配置 targets
或 ignoreBrowserslistConfig
,@babel/preset-env
会使用 browserslist
配置源。也就是会用上面的默认配置。
如果需要兼容特定浏览器,只需要按需修改.browserslistrc
就可以了。
我们这里创建一个.browserslistrc
文件,并配置
> 0.5%
last 2 versions
Firefox ESR
编译
在package.josn
里面配置webpack
编译脚本。
"scripts": {
"webpack1": "webpack"
}
在命令行运行 npm run webpack
就可以看到编译后的js
文件了。箭头函数被转换成普通的函数了。
这里我们并没有直接使用babel
命令来编译我们的js
,而是使用了webpack
,并配置了babel-loader
。
参考文档
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!