前端开发的必修课——babel

364 阅读5分钟

前言

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/): image.png

transform阶段

这个阶段主要就是我们babel插件工作的阶段。@babel/traverse这个阶段对我们的AST树进行遍历,当执行到不同的节点时会执行相应的visitor函数,visitor 函数里可以对 AST 节点进行增删改的操作,并返回一个新的AST树。

generate阶段

在generate阶段,可以利用@babel/generatortransform阶段生成的新AST转换为字符串,并生成sourcemap

 babel配置文件详解

我们知道babel其中的一个作用就是将ES6+的代码转化为目前浏览器所支持的js代码。通常在项目根目录下都会出现.babelrc或者babel.config.json的配置文件,这是因为babel在执行编译时会去项目的根目录读取配置文件,其中配置文件中肯定少不了presetsplugins的配置。

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包括ArrayStringMath等原生对象上的JS高版本方法,这是我们例子中ES6中Arrayfind方法的截图:

image.png

这样我们就能通过垫片的方式在低版本的浏览器运行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包含了corejsregenerator-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插件。