Babel
是一个 JavaScript
编译器。
Babel
是一个工具链,主要用于将ECMAScript 2015+
版本的代码转换为向后兼容的JavaScript
语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
通俗地讲,Babel
只是转义新标准引入的语法,如ES6
中的箭头函数、解构等。而新标准中新增的方法、函数等就需要通过Polyfill
在目标环境中添加这些缺失的特性来解决。
Babel
的功能:
- 语法转换
- 通过
Polyfill
方式在目标环境中添加缺失的特性,对应模块@babel/polyfill
- 源码转换
Babel` 编译过程:
- 解析:将代码字符串解析成抽象语法树。
- 转换:对抽象语法树进行转换操作。(本文重点)
- 输出:根据变换后的抽象语法树再生成代码字符串。
常见使用方式:
-
.babelrc/babel.config.json
(Babel
配置文件) -
babel-loader
(webpack/rollup
等)
初始化项目
- 创建一个空目录,执行
npm init -y
- 新建
src/index.js
const fn = () => { };
- 安装必要依赖:
@babel/core、@babel/cli
-
@babel/core
核心库,包含了所有的核心API
-
@babel/cli
提供的CLI
命令行工具,主要是babel
命令,适合安装在项目中 -
@babel/node
提供了babel-node
命令,更适合全局安装
npm i -D @babel/core @babel/cli
-
- 在
package.json
中配置scripts
,添加babel
的编译命令
执行编译:"scripts": { "compiler": "babel src --out-dir lib --watch" }
npm run compiler
,生成lib/index.js
。当前没有配置任何插件,所以编译前后的代码完全一样。
插件
虽然Babel
是开箱即用的,但如果没有为其添加任何插件,那么它什么也不会做。
Babel
构建在插件之上,使用现有的或自己编写的插件可以组成一个转换通道,Babel
的插件分为两种:语法插件、转换插件
-
语法插件
只允许Babel
解析(不是转换)特定类型的语法,可以在AST
转换时使用,以支持解析新语法// for Example import * as babel from "@babel/core"; const code = babel.transformFromAstSync(ast, { //支持可选链 plugins: ["@babel/plugin-proposal-optional-chaining"], babelrc: false }).code;
-
转换插件
会启用相应的语法插件(因此不需要同时指定这两种插件),先解析,后转换!
在项目根目录下创建配置文件.babelrc
,安装并配置插件
// 一个编译箭头函数的babel插件
npm i @babel/plugin-transform-arrow-functions -D
//.babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
# 也可以指定插件的绝对/相对路径
"plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"]
重新编译的结果:
// lib/index.js
"use strict";
const fn = function () { };
OK! 现在可以转换箭头函数了,如果继续转化其他的JS新特性,还需要继续添加插件,一个个配置的话必然很繁琐!
为此,Babel
提供了预设。
预设
通过使用或创建一个 Preset
即可轻松使用一组插件。
// 官方 preset
@babel/preset-env @babel/preset-flow @babel/preset-react @babel/preset-typescript
注:从 Babel v7
开始,所有针对标准提案阶段的功能所编写的预设(stage preset)
都已被废弃,官方移除了@babel/preset-stage-x
@babel/preset-env
官方:
@babel/preset-env
是一个灵活的预设,你不需要管理目标环境需要的语法转换或浏览器polyfills
,就可以使用最新的JavaScript
,同时也会让JavaScript
打包后的文件更小。
总之,主要作用是:转换目标浏览器中缺失的JavaScript
新语法,加载Polyfills
在不做任何配置的情况下,@babel/preset-env
所包含的插件支持所有最新的JS特性(ES2015、ES2016
等,不包含 stage
阶段),并将其转换为ES5
代码。
但如果代码中包含了可选链(目前仍在stage
阶段),还需要安装相应的插件。
安装与配置.babelrc
npm i @babel/preset-env -D
// .babelrc
{
"presets": ["@babel/preset-env"]
}
Browserslist 集成
@babel/preset-env
会根据配置的目标环境,生成插件列表来编译。对基于浏览器或Electron
的项目,官方推荐使用.browserslistrc
文件来指定目标环境。默认情况下,如果没有在.babelrc
中设置targets
或ignoreBrowserslistConfig
,@babel/preset-env
会使用browserslist
配置源。
比如,仅包括浏览器市场份额超过0.25%
的用户所需的 polyfill
和代码转换(忽略没有安全更新的浏览器,如IE10
和BlackBerry
)
三种配置方式:
-
.babelrc:@babel/preset-env
的targets
可以设置支持哪些平台、哪些版本等等目标信息{ "presets": [ ["@babel/preset-env", { "targets": { "browsers": "> 0.25%, not dead", "node": "xxx", ... } }] ] }
- 在项目跟目下创建
browserslist
配置文件.browserslistrc
> 0.25% not dead
-
package.json
:官方建议这种方式"browserslist": [ "> 0.25%", "not dead" ]
如果不是要兼容所有的浏览器和环境,推荐指定目标环境,这样在编译代码能保持最小!
再比如,.browserslistrc
配置为:
last 2 Chrome versions
编译后发现代码并没有转换,这是因为Chrome
浏览器最新的两个版本都支持箭头函数。
更多 browserslist
配置:browserslist 配置
Polyfill
修改一下src/index.js
const isHas = [1,2,3].includes(2);
const p = new Promise((resolve, reject) => {
resolve(100);
});
编译后发现代码并没有转换!--
因为@babel/preset-env
转换的是语法,但新的内置函数、方法无法转换。
这时,就需要 polyfill
登场了!
polyfill
的译文是垫片,顾名思义,就是垫平不同浏览器或不同环境下的差异,让新的内置函数、方法在低版本浏览器中也可以使用。
core-js/stable
(Polyfill ECMAScript
特性)和 regenerator-runtime/runtime
(需要使用转换后的generator
函数)模块,可以模拟完整的ES2015+
环境(不包含第4阶段前的提议)。
这就意味着可以使用:新的内置函数如
Promise、WeakMap
,新的静态方法如Array.from、Object.assign
,新的实例方法如Array.prototype.includes
,以及generator
函数(前提是使用@babel/plugin-transform-regenerator
插件)。
但为了添加这些功能,polyfill
将添加到全局范围和类似String
这样的内置原型中(如你所想,会污染全局环境,后面会讲避免全局污染的方法)。
注意:Babel v7.4.0
之前,@babel/polyfill
包含core-js(默认v2)
和regenerator-runtime
两个模块;但从Babel v7.4.0
开始,@babel/polyfill
被废弃了,支持直接安装并导入core-js(默认v3)
和regenerator-runtime
core-js
是能够使用新API
的最重要的包,与Babel
高度集成,core-js@3
废弃了@babel/polyfill
,实现了完全无污染的API
转译。
// Babel v7.4.0 之前
npm i -S @babel/polyfill // 不使用 -D 是因为这是一个需要在源码之前运行的垫片
// Babel v7.4.0
npm i core-js regenerator-runtime -S
# core-js: ^3.6.4:提供 es 新的特性
# regenerator-runtime: ^0.14.4:代码中用到generator、async函数的话,提供对 generator 支持
两种使用方式:
-
src/index.js
的首部// Babel v7.4.0 之前 require("@babel/polyfill"); or import "@babel/polyfill"; // Babel v7.4.0 import "core-js/stable"; import "regenerator-runtime/runtime";
- 在
webpack
里配置入口:entry: ["@babel/polyfill", "./src/index.js"],
现在的代码不管在低版本还是高版本浏览器,甚至Node
环境中都能正常运行了!
按需引入
然而,很多时候都不需要完整的引入
Polyfill
,这样会增大构建包的体积。
Babel
在@babel/preset-env
中提供了参数useBuiltIns
,设置为usage
时就只会包含代码中需要的polyfill
。另外还必须同时配置corejs
。@babel/polyfill
默认安装core-js v2
,而core-js v2
分支中已经不再添加新特性,为了可以使用更多新特性,请安装core-js v3
useBuiltIns
的可选值
-
false
不对Polyfill
做操作,引入所有的Polyfill
; -
usage
根据配置的浏览器兼容性,以及代码中使用到的API
来进行Polyfill
,实现按需加载; -
entry
根据browserslist
的配置,引入目标环境不兼容的polyfill
,还需要在入口文件中手动添加import "@babel/polyfill"
还有一个常用的参数 modules
,可以取值 amd, umd, systemjs, commonjs, false
,这可以让Babel
以特定的模块化格式来输出代码。false
表示不进行模块化处理。
// Babel v7.4.0 之前还需要额外安装core-js v3
npm install -S core-js@3
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3 // 默认使用的时corejs@2,因此必须设置corejs@3
}]
]
}
编译时,Babel
会检查所有代码,查找在目标环境中缺失的功能,然后仅仅把需要的polyfill
包含进来。
// lib/index
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var isHas = [1, 2, 3].includes(2);
var p = new Promise(function (resolve, reject) {
resolve(100);
});
此时用webpack
的production
模式构建,包体积要比引入整个@babel/polyfill
小很多了。
提取辅助函数
Polyfill
虽然解决了Babel
不转换非语法的新API
问题,但会使用很小的辅助函数来实现类似_classCallCheck、_createClass
等公共方法。默认情况下,这些辅助函数将被inject(添加)
到需要它的每个文件中。
修改src/index.js
class Point { }
编译之后:
// lib/index
"use strict";
function _classCallCheck ...
var Point = function Point() ...
试想一下,100个文件都是用了class
,那就意味着诸如_classCallCheck
之类的辅助函数被inject
了100次,导致编译后的bundle(包)
体积变大!
为此,Babel
提供了单独的模块@babel/runtime
,用于提供编译模块的辅助函数。启用@babel/plugin-transform-runtime
插件后,Babel
就会使用@babel/runtime
中的工具函数,以 闭包 的形式注入。
npm i -D @babel/plugin-transform-runtime #仅编译时使用
npm i -S @babel/runtime #编译和运动时都需要
在 .babelrc
中配置 @babel/plugin-transform-runtime
插件
{
// "presets": [
// ["@babel/preset-env", {
// "useBuiltIns": "usage",
// "corejs": 3
// }]
// ],
"plugins": [
["@babel/plugin-transform-runtime", { corejs: 3 }]
]
}
编译之后:
// lib/index
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var Point = function Point() {
(0, _classCallCheck2.default)(this, Point);
};
可以看出,帮助函数已经不是直接被 inject
到代码中,而是从@babel/runtime
中引入的。
全局污染问题
@babel/plugin-transform-runtime
:构建过程的代码转换,是一个可以重复使用Babel
注入的帮助程序,以节省代码大小的插件。
除此之外,该转换器还有另外一个作用:为我们的代码创建一个沙盒环境。
如果直接使用
core-js
或@babel/polyfill
,它们所提供的诸如内置程序Promise、Set、Map
等,将会污染全局环境。虽然这对于应用程序或命令行工具可能是可以的,但如果代码是要发布供他人使用的库,或者无法完全控制代码运行环境,那么这种污染将成为一个问题。
@babel/plugin-transform-runtime
会将这些内置别名作为core-js
的别名,因此可以无缝使用它们,无需使用polyfill
(官网)
- 安装依赖:
npm i @babel/runtime-corejs3 -S
- 配置
.babelrc
{ "presets": ["@babel/preset-env"], "plugins": [ ["@babel/plugin-transform-runtime", { "corejs": 3 }] ] }
- 修改
src/index.js
,加入include()、Promise
let isHas = [1,2,3].includes(2); Array.from([1, 2, 3], x => x + x); // [2, 4, 6] let promsie = new Promise(); async function fn() { return 1 }
重新编译可知,@babel/plugin-transform-runtime
通过导入模块的方式引入所需功能,直接去修改Array.prototype
或 新增Promise
函数,避免了全局环境的污染。
搭配
webpack
时,如果使用commonJs
编写模块,且使用了@babel/plugin-transform-runtime
,则应该安装@babel/plugin-transform-modules-commonjs
,webpack
需要利用它处理ES6Module
。
更多插件/预设的补充知识
-
插件的排列顺序很重要!!!
如果两个转换插件都将处理程序的某个代码片段,则将根据转换插件或preset
的排列顺序依次执行。-
Plugin
在Presets
前运行 - 插件顺序从前往后排列
-
Presets
顺序是从后往前的
先执行{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
@babel/preset-react
, 后执行@babel/preset-env
-
- 插件参数
插件和preset
都可以接受参数,参数由插件名和参数对象组成一个数组"plugins": [ ["@babel/plugin-proposal-class-properties", { "loose": true }] ]
- 插件的短名称
- 如果插件名称为
@babel/plugin-XXX
,可以使用短名称@babel/XXX
"plugins": [ "@babel/transform-arrow-functions" // @babel/plugin-transform-arrow-functions ]
- 如果插件名称为
babel-plugin-XXX
,可以使用短名称XXX
,该规则同样适用于带有scope
的插件"plugins": [ "newPlugin", // babel-plugin-newPlugin "@scp/myPlugin" // @scp/babel-plugin-myPlugin ]
- 如果插件名称为
- 创建自己的
Preset
- 可以简单的返回一个插件数组
module.exports = function() { return { plugins: ["A", "B", "C"] } }
-
preset
中也可以包含其他的preset
,以及带有参数的插件module.exports = function() { return { presets: [require("@babel/preset-env")], plugins: [ [require("@babel/plugin-proposal-class-properties"), { loose: true }], require("@babel/plugin-proposal-object-rest-spread") ] } }
- 可以简单的返回一个插件数组
配置文件
Babel
支持多种格式的配置文件,根据使用场景可以选择不同的配置文件。
所有的Babel API
参数都可以配置,但如果该参数需要使用JS
代码,那可能需要使用JS
代码版的配置文件。
- 如果希望以编程的方式创建配置文件或编译
node_modules
目录下的模块,那么babel.config.js
可以满足需求; - 如果只需要一个简单的且只用于单个软件包的配置,那可以选择
.babelrc
-
babel.config.js
在项目根目录下创建babel.config.js
文件,配置文档:babel.config.jsmodule.exports = function(api) { api.cache(true); const presets = [...]; const plugins = [...]; return { presets, plugins }; }
-
.babelrc
在项目根目录下创建.babelrc
文件,配置文档:.babelrc{ "presets": [], "plugins": [] }
-
package.json
可以将.babelrc
中的配置信息作为babel 键(key)
添加到package.json
文件中:{ "name": "my-package", "babel": { "presets": [], "plugins": [] } }
-
.babelrc.js
与.babelrc
配置相同,但是支持使用JS
编写。//可以在其中调用 Node.js 的API const presets = []; const plugins = []; module.exports = { presets, plugins };
使用Webpack构建
- 安装依赖:
npm install -D webpack-cli webpack babel-loader clean-webpack-plugin
- 在根目录下创建配置文件
webpack.config.js
const path = require('path') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: 'production', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[hash].js' }, module: { rules: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ //排除 node_modules 目录 } ] }, plugins: [ new CleanWebpackPlugin() //清理 output 指定的目录 ] }
-
package.json
"scripts": { "build": "webpack }
- 构建:
npm run build