前言
babel对于前端研发来说就像是一个熟悉的陌生人
,也许你总能听到或者看到babel,但却不知道在什么时候发挥了什么作用。 其实在以下的功能中都有babel的身影:
转译 esnext、typescript、flow 等到目标环境支持的 js
特定情况下的代码转换
(例如利用babel-plugin-import对antd组件的引入方式进行转换)代码静态分析
(如代码混淆,linter检查等)
后面我们从以下几个方面来学习下babel:
- babel的编译流程
- 详解项目中babelrc的配置
- 实现一个babel的插件
babel的编译流程
babel的编译分为三个阶段:parse阶段 => transform阶段 => generate阶段
parse阶段
在parse阶段,利用@babel/parse
主要做的就是将源码的字符串转化为AST,其中的过程有词法分析和语法分析。词法分析
就是将字符串分割成一个个的tokens(令牌流),语法分析
就是按照不同的语法规则,组成一个AST对象。
下面这个字符串就被变化为了一个抽象语法树(创建于astexplorer.net/):
transform阶段
这个阶段主要就是我们babel插件工作的阶段。@babel/traverse
这个阶段对我们的AST树进行遍历,当执行到不同的节点时会执行相应的visitor函数,visitor 函数里可以对 AST 节点进行增删改的操作,并返回一个新的AST树。
generate阶段
在generate阶段,可以利用@babel/generator
将transform阶段
生成的新AST转换为字符串,并生成sourcemap
。
babel配置文件详解
我们知道babel其中的一个作用就是将ES6+
的代码转化为目前浏览器所支持的js代码。通常在项目根目录下都会出现.babelrc
或者babel.config.json
的配置文件,这是因为babel在执行编译时会去项目的根目录读取配置文件,其中配置文件中肯定少不了presets
和plugins
的配置。
plugins(插件)
plugins顾名思义就是babel的插件
,那么插件是做什么的呢?我们接下来用一个例子来呈现效果:
// .babelrc
{
"plugins": [
"@babel/plugin-transform-arrow-functions"
]
}
复制代码
// index.js
// 编译前
const a = () => {
const arr = ['a', 'b', 'c']
const newArr = [...arr, 'd']
console.log(newArr)
}
// 编译后
const a = function () {
const arr = ['a', 'b', 'c'];
const newArr = [...arr, 'd'];
console.log(newArr);
};
复制代码
很明显我们的箭头函数
被转化为了ES5的普通函数
,那么我们现在希望ES6的spread的语法也编译成ES5的语法呢?同理我们继续去新增转换spread的插件。
// .babelrc
{
"plugins": [
"@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-spread"
]
}
复制代码
// index.js
const a = function () {
const arr = ['a', 'b', 'c'];
const newArr = [].concat(arr, ['d']);
console.log(newArr);
};
复制代码
这样我们就实现了对spread的语法的转换。但是毕竟有这么多的ES6+的新特性,这样一一的引入插件效率实在太低了,但是好在多个插件会被批量封装到presets(预设)
,我们上述用到的插件就被封装到了@babel/preset-env
中。
presets(预设)
presets
实际上就是插件集合的配置。例如上面的例子:
// .babelrc
{
"presets": ["@babel/preset-env"]
}
复制代码
// index.js
// 编译后
var a = function a() {
var arr = ['a', 'b', 'c'];
var newArr = [].concat(arr, ['d']);
console.log(newArr);
};
复制代码
我们能看到箭头函数
和spread
语法都进行了转换,甚至将const
也转成了var
的写法,所以能看出来@babel/preset-env
内部内置了多个插件,但是当涉及到某些对象的方法的时候,并不是语法的转换,还需要引入polyfill
垫片来在注入中相关方法的api。
polyfill(垫片)
通过语法的转换和方法的垫片(polyfill),就能在目标环境使用ES6+的语法和方法。 接下来我们通过一个例子来看看怎么配置babel的polyfill:
// 编译前
import 'babel-polyfill'
const a = () => {
const arr = ['a', 'b', 'c']
console.log(arr.find('a'))
}
复制代码
// 编译后
require("babel-polyfill");
var a = function a() {
var arr = ['a', 'b', 'c'];
console.log(arr.find('a'));
};
复制代码
babel-polyfill
包括了core-js
和 regenerator-runtime
模块。
其中core-js
包括Array
、String
、Math
等原生对象上的JS高版本方法,这是我们例子中ES6中Array
的find
方法的截图:
这样我们就能通过垫片的方式在低版本的浏览器运行ES6+的一些api方法。
regenerator-runtime
模块的话,主要为了实现对async await
的支持。
但是我们观察编译后的代码,可以发现babel-polyfill
被整体都引入了进来,这会导致我们打包的产物变大,为此我们需要实现对polyfill
按需引入。
按需加载polyfill
我们可以把babelrc
文件改成下面的配置:
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
复制代码
编译后,效果如下图:
require("core-js/modules/es.array.find.js");
var a = function a() {
var arr = ['a', 'b', 'c'];
console.log(arr.find('a'));
};
复制代码
这样我们就实现了按需引入polyfill。
@babel/runtime
@babel/runtime
可以帮助我们复用插件的一些共同的逻辑,以达到减小编译后代码体积的效果。
// 编译前
class Circle {}
复制代码
// 编译后
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Circle = function Circle() {
_classCallCheck(this, Circle);
};
复制代码
这意味着每个包含类的文件每次都会重复生成_classCallCheck
。
@babel/plugin-transform-runtime
通常仅在开发时使用,但是运行时最终代码需要依赖 @babel/runtime
,所以我们可以在项目中分别在开发依赖和生产依赖引入@babel/plugin-transform-runtime
和@babel/runtime
。
由于babel-runtime包含了corejs
和regenerator-runtime
两个模块,所以可以不用再单独引入polyfill 。
接下来把我们再babelrc
文件中引入@babel/plugin-transform-runtime
插件:
{
"presets": [
["@babel/preset-env"]
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
复制代码
接着我们再编译一下我们之前的例子:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _find = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/find"));
var a = function a() {
var arr = ['a', 'b', 'c'];
console.log((0, _find["default"])(arr).call(arr, 'a'));
};
复制代码
最后我们总结下这种配置方式的优缺点:
优点:
- 可以实现
polyfill
的按需引入 - 不会污染原型对象
缺点:
- 无法为第三方库的api做polyfill
结语
没想到单纯一个基础的babelrc文件的配置就能写这么多,作为一个H5研发来说,为了兼容低版本的手机浏览器,了解babel的项目配置还是很重要的。后面我们单独来谈谈怎么自己写一个babel插件。