前言
babel ------ 简单来说就是把JavaScript 中 es2015/2016/2017/2046 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。严格来说,babel 也可以转化为更低的规范。但以目前情况来说,es5 规范已经足以覆盖绝大部分浏览器,因此常规来说转到 es5 是一个安全且流行的做法。babel 作为一种常用的前端译器,他的工作可以被分解为三个主要阶段:
解析(Parsing)
------ 将源代码转换为一个更抽象的形式:先通过词法分析(tokenizer)将源代码 分解成一个个词素;再通过语法分析(parser)接收词素并将它们组合成一个描述了源代码各部分之间关系的中间表达形式:抽象语法树
。转化(Transformation)
------ 接受解析产生的抽象形式并且操纵这些抽象形式做任何编译器想让它们做的事: traverser函数接收抽象语法树以及一个访问者对象,然后创造一个新的抽象语法树代码生成(Code Generation)
------ 基于转换后的代码表现形式(code representation)生成目标代码。
整体来说,babel 的工作流程就是source to source
的转换,编译流程如下:
- parse:通过 parser 把源码转成抽象语法树(AST)
- transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
- generate:把转换后的 AST 打印成目标代码,并生成 sourcemap
1. 创建本地node项目演示babel编译效果
我们先通过本地的一个demo项目效果直观感受一下babel的编译能力:当前的demo并没有通过前端框架建一个工程项目,只是简单通过本地node环境安装 @babel/cli
构建一个演示demo。这个方法可以通过终端命令行执行方式运行,便于编译本地目录或者文件,在使用babel
时候我们一般需要安装:@babel/core
@babel/cli
@babel/preset-env
等核心依赖。除此之外我们还需要安装一些扩展依赖包:core-js
@babel/plugin-transform-runtime
@babel/runtime-corejs3
等。 增加本地的js代码如下:
js
// src/index.js
function test01 () {
const a = [1,2,3,4,5];
return a.reduce((pre, cur) => {
pre.push(cur + 20);
return pre
}, [])
}
console.log(test01())
function test02() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
}
test02().then(res => {
console.log(res)
})
本地通过babel.config.json 配置文件如下:
js
{
"presets": [
[
// babel预设
"@babel/env",
{
// 使用corejs 3的版本
"corejs": 3,
// 按需加载
"useBuiltIns": "usage",
// 不使用模块化 交给其它打包工具处理
"modules": false
}
]
],
"plugins": [
[
// 只引入用到的模块
"@babel/plugin-transform-runtime",
{
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
执行如下脚本,可以将 src/index.js
文件通过babel 编译到指定的文件目录dist/*
下:
js
npx babel src/* --out-file dist/source.js
编译之后的代码如下:
js
import _reduceInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/reduce";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _setTimeout from "@babel/runtime-corejs3/core-js-stable/set-timeout";
function test01() {
var a = [1, 2, 3, 4, 5];
return _reduceInstanceProperty(a).call(a, function (pre, cur) {
pre.push(cur + 20);
return pre;
}, []);
}
console.log(test01());
function test02() {
return new _Promise(function (resolve, reject) {
_setTimeout(function () {
resolve('success');
}, 1000);
});
}
test02().then(function (res) {
console.log(res);
});
以上的demo演示了一个简单的babel 编译流程,现代化的工程开发过程中涉及到的配置更加复杂繁冗,我们按照配置流程逐步学习和实践。
2. babel的安装使用及基础配置
从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下,从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题。在使用babel
时候我们一般需要安装@babel/core
@babel/cli
@babel/preset-env
等核心依赖。
@babel/core
: babel实现转换的核心,需要依赖更底层的@babel/parser
、@babel/generator
、@babel/traverse
、等:
@babel/parser
对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法, 例如指定是ts
,jsx
语法,词法语法ast都在这个阶段@babel/traverse
通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶 段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据,简单 的说操作Ast 语法树 改变语法树结构自然达到了转换效果@babel/generator
打印 AST 成目标代码字符串,支持 comments、minified、 sourceMaps 等选项。将语法树转换成对应新的编码
@babel/cli
: 是 Babel 提供的命令行,它可以在终端中通过命令行方式运行,编译文件 或目录@babel/preset-env
: Babel 只是一个'编译器' 你需要告诉他转换规则,需要在transformer 利用我们配置好的 plugins/presets把 Parser生成的 AST转变为新的 AST,即'@babel/preset-env' 就是一套转换规则集合
babel 编译的核心 ------ transform
API
babel/core
中的 transform
方法转换输入的代码,转换完成后执行callback,返回参数有转换的代码
、source map
和 AST
。这里的 transform
API包含了阶段解析(Parsing)、转化(Transformation)和代码生成(Code Generation)完整的三个阶段。
js
// transform的简单使用 ------ 输出转换结果没有变化,babel只是一个编译器并不会做过多操作
const core = require("@babel/core");
const code = `
let a = 1;
if (a > 0) {
console.log("hello world");
}`;
core.transform(code, {}, (err, result) => {
console.log(result, err); // => { code, map, ast }
console.log(result.code); // => var a = 1;if (a > 0) {console.log("hello world");}
});
js
// 引入 @babel/preset-env 配置转换规则
const core = require("@babel/core");
const env = require("@babel/preset-env");
const code = "const a = 'hello world';";
// 配置转换规则后 es6 =》 es5
core.transform(code, { presets: [env] }, (err, result) => {
console.log(result, err); // => { code, map, ast }
console.log(result.code); // '"use strict";\n\nvar a = 'hello world';'
});
babel 的相关配置
在使用@babel/core
中的transform
api 时候如果没有去配置规则,字符串在内部就是一日游
: 即code=> 转换'ast' => 没规则保持转换'ast' => 没改变的'ast'再次变回之前的'code'
。整个配置中比较常用的两个字段:presets
预设h和plugins
插件。preset可以被看作是一组 Babel 'plugins'插件,更通俗的理解'plugins' 是一条条转换的规则,presets将这种有统一规律的"plugins" 插件装进一个预设中
js
{
"presets": [], // 预设
"plugins": [] // 插件
}
- plugins -- 插件(主要有三类)
syntax plugin
:只是在parse阶段使用,可以让 parser 能够正确的解析对应的语法成 AST;transform plugin
:是对 AST 的转换,针对es20xx 中的语言特性、typescript、jsx 等的 转换都是在这部分实现的;proposal plugin
:未加入语言标准的特性的 AST 转换插件
- presets -- 预设(对于插件的一层封装,是一组插件的集合)
- 专门根据es标准处理语言特性的预设 --
babel-preset-es20xx
- 对其react、ts兼容的预设 --
preset-react
preset-typescript
- 目前最常使用的便是
@babel/preset-env
这个预设
- 配置文件规则 根据官方文档的说法,目前有两类配置文件:全局配置(
babel.config.json
) 和 局部配置(.babelrc.json
、package.json
)
3. 配置 @babel/preset-env
@babel/preset-env
预设是一系列插件的集合,主要用来帮助我们对语法进行转换。包含了我们在babel6中常用es2015, es2016, es2017 等最新的语法转化插件,允许我们使用最新的js语法,比如 let,const,箭头函数等等,但不包含低于 Stage 3 的 JavaScript 语法提案。通过安装一个babel/preset-env
能解决大部分问题。常用配置属性targets
、useBuiltIns
、corejs
等。
target -- 指定语法最低版本浏览器兼容
可以通过配置文件指定语法最低版本浏览器兼容。这里其实配合的是Browserslist
,Browserslist
的数据都是来自以下配置,其优先级如下:
@babel/preset-env
里的 targetspackage.json
里的 browserslist 字段.browserslistrc
配置文件
@babel/preset-env
配置支持 browserslist 查询写法
js
{ "targets": "> 0.25%, not dead" } // 全球使用人数大于0.25%且还没有废弃的版本
@babel/preset-env
配置支持最小环境版本构成的对象
js
{ "targets": { "chrome": "58", "ie": "11" } }
如果没配置targets,Babel会假设你的目标是最老的浏览器 @babel/preset-env将转换所有 ES2015 - ES2020代码为ES5兼容
corejs -- js语法垫片
- JavaScript的模块化标准库,其中包括各种ECMAScript特性的
polyfill
。转换后代码中引入api的polyfill都是来源于corejs。它现有2和3两个版本,目前2版本已经进入功能冻结阶段了,新的功能会添加到3版本中。 - 这个选项只有在和
useBuiltIns: "usage"
或useBuiltIns:"entry"
一起使用时才有效果,该属性默认为"2.0"。其作用是进一步约束引入的polyfill的数量。
useBuiltIns -- 控制 api 导入
AST
语法树解决是语法层面,可以简单的理解为字符进行转换。但是新增加api
层面具有语义的方法,例如使用Promise
时候是其内部定义的逻辑让Prmoise
具备了实现效果。社区提供了这种赋予语义的包corejs
,参数可以设置三个值'entry','usage', 'false'来控制这类垫片的是否按需自动引入。
useBuiltIns: false
: 目前社区给的垫片库推荐是core-js
你需要在项目入口引入这个'api'垫片,但不会根据你target配置按需引入
。例如在主文件入口引入import "core-js/stable"
和import "regenerator-runtime/runtime"
,则无视配置的target规则,在所有的浏览器中都会引入polyfill。useBuiltIns: entry
: 根据配置的浏览器兼容,引入当前浏览器不兼容的polyfill。入口文件引入import "core-js/stable"
和import "regenerator-runtime/runtime"
,会自动根据browserslist替换成当前浏览器无法兼容的所有 polyfilluseBuiltIns: usage
: 根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加
useBuiltIns 推荐使用 "useBuiltIns: usage" 配置,他会自动帮我们引入corejs垫片
,不用在手动全局引入,并且会对指定浏览器版本进行配合
简单的配置
@babel/preset-env
+ useBuiltins(entry)
+ targets
这样 @babel/preset-env
定义了 Babel所需插件预设,同时由 Babel 根据targets 配置的支持环境,自动按需加载 polyfills, 当 useBuiltins 配置为 usage,它可以真正根据代码情况,分析 AST(抽象语法树)进行更细粒度的按需引用
js
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry",
"targets": { chrome: 44 }
"corejs": 3,
}
]
],
"plugins": []
}
4. 配置 @babel/plugin-transform-runtime
@babel/plugin-transform-runtime
的使用是为了解决两个问题
:
- @babel/plugin-transform-runtime 可以解决全局污染问题
- 移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime、@babel/helpers里的辅助函数来替代
案例解释上述问题一
- 没有使用
@babel/plugin-transform-runtime
的配置如下:
js
// 没有使用'@babel/plugin-transform-runtime' 的babel.config.json 配置如下:
{
"presets": [
[
"@babel/env",{
"modules": false,
"useBuiltIns": "usage",
"targets": { "ie":8 },
"corejs": 3
}
]
]
}
// 转换前的代码:
const promise = new Promise();
// 转换后的代码:
import "core-js/modules/es.object.to-string.js";
import "core-js/modules/es.promise.js";
var promise = new Promise(() => {});
console.log(promise);
问题 ------ 虽然转换了api,但是实际上也造成了全局污染帮我们导入了一个Promise,把原本浏览器环境的 Promise 将变成corejs 引入的 Promise
- 使用
@babel/plugin-transform-runtime
的配置如下:
javascript
// 安装 @babel/plugin-transform-runtime ,并使用配置之后:
{
"presets": [
[
"@babel/env",
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModules": false,
}
]
]
}
// 转换前的代码:
const promise = new Promise();
// 转换后的代码:
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
var promise = new _Promise();
解决 ------ 通过重新定义一个变量名 _Promise
,再引用,解决了和全局 Promise 的冲突问题。
案例解释上述问题二
如下代码是我们的测试代码:
js
function test01 () {
const a = [1,2,3,4,5];
return a.reduce((pre, cur) => {
pre.push(cur + 20);
return pre
}, [])
}
console.log(test01())
function test02() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
}
test02().then(res => {
console.log(res)
})
class Person {
constructor(name) {
this.name = name
}
getName() {
return this.name;
}
}
console.log(new Person('llz'))
babel 编译转换之后的代码:
js
import _classCallCheck from "@babel/runtime-corejs3/helpers/classCallCheck";
import _createClass from "@babel/runtime-corejs3/helpers/createClass";
import "core-js/modules/es.function.name.js";
import _reduceInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/reduce";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _setTimeout from "@babel/runtime-corejs3/core-js-stable/set-timeout";
function test01() {
var a = [1, 2, 3, 4, 5];
return _reduceInstanceProperty(a).call(a, function (pre, cur) {
pre.push(cur + 20);
return pre;
}, []);
}
console.log(test01());
function test02() {
return new _Promise(function (resolve, reject) {
_setTimeout(function () {
resolve('success');
}, 1000);
});
}
test02().then(function (res) {
console.log(res);
});
var Person = /*#__PURE__*/function () {
function Person(name) {
_classCallCheck(this, Person);
this.name = name;
}
_createClass(Person, [{
key: "getName",
value: function getName() {
return this.name;
}
}]);
return Person;
}();
console.log(new Person('llz'));
babel.config.json 配置如下:
json
{
"presets": [
[
"@babel/env",
{
"corejs": 3,
"useBuiltIns": "usage",
"modules": false
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
在理解上述编译过程之前需要知道@babel/helpers
和@babel/runtime
:像Promise这类api都是通过引入corejs中的api处理的,但是像 class 这类语法糖需要引入帮助函数@babel/helpers
。@babel/runtime
具有和@babel/helpers
一样的帮助函数,但是需要@babel/plugin-transform-runtime
配合使用,并且和@babel/helpers
的效果也是不同的。
@babel/plugin-transform-runtime
实际上和@babel/env
配置中的target是冲突的,因为babel中插件的执行顺序是:先plugin再preset。@babel/plugin-transform-runtime 转完了之后,再交给 preset-env 这时候已经做了无用的转换了。而 @babel/plugin-transform-runtime 并不支持 targets 的配置,就会做一些多余的转换 和 polyfill。其实从上面的例子在使用@babel/plugin-transform-runtime
配置的core.js 导入变成了@babel/runtime-corejs3/core-js-stable
这是因为之前corejs
版本的导入是污染全局的,你想使用独立的包需要安装runtime-corejs3
。
总结:
- 需要让babel配合插件帮助进行语法转换,这里选择的是
@babel/preset-env
- 对api转换需要配合
core-js
和regenerator-runtime/runtime
,但是利用@babel/preset-env
中提供useBuiltins
可以不用手动在全局文件引入core-js
和regenerator-runtime/runtime
- 在语法转换的时候
@babel/helpers
会提供一些用来帮助转换的helper函数
,但是希望进一步优化需要使用@babel/plugin-transform-runtime
配合@babel/runtime
,可以帮助解决移除语法转换后内联的辅助函数
使用runtime-corejs3
解决全局垫片的污染问题 - 但二者都存在彼此优缺点,
@babel/env
可以用 target 但是不能使用帮助函数和解决全局污染,@babel/plugin-transform-runtime
不能用target 但是能使用帮助函数和解决全局污染。可以通过同时配置两者让他们互相解决自己短板 可以参考总结篇章