Babel 101 - 01 - Babel的基础使用说明

Sunday, Nov 15, 2020

What is Babel

Babel is a JavaScript compiler. Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.

Babel可以帮我们处理以下事情:

  • 语法转换,将ES6+ 语法转换成ES5的代码
  • 对新版本ES的feature提供polyfill
  • TS / flow支持(类型)
  • 生成source map
  • ...

Usage Guide

Babel 有以下几种使用方式:

  • 通过Cli使用
  • 代码中使用
  • 配合打包工具(gulp、webpack、rollup使用)

plugins

Babel不进行任何配置开箱即用不会任何效果,也就是说一段ES6的代码经过刚开箱的 babel 处理,输出的还是之前的代码。Babel的所有转换效果都是通过Plugins完成的,每个Plugin都有自己的能力,能处理对应的ES代码。 例(开箱即用) 安装babel cli和core

npm install @babel/cli @babel/core --save-dev

写一段简单的代码在src/index.js中:

// src/index.js
const app = () => {};

运行babel,将处理完的文件输出到dist目录

npx babel src --out-dir dist

处理后的 dist/index.js没有变化:

// dist/index.js
const app = () => {};

从babel7开始,babel提供的npm库都变为在@babel这个scope下。

Babel plugins分为两类:

  • 语法类(Syntax)
  • 转换类(Transform)

Syntax plugins在官网上是这么说的:

These plugins only allow Babel to parse specific types of syntax (not transform).

所以Syntax plugin是用来 识别 新语法的,不进行转换,在启用transform plugin的时候对应的syntax会自动引入,不需要单独配置。

plugins usage

给index.js补充一些代码:

const app = () => {};
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  getName() {
    return this.name;
  }
  getAge() {
    return this.age;
  }
}

安装plugin, 创建一个 babel的配置文件(.babelrc.js)

npm install @babel/plugin-transform-arrow-functions @babel/plugin-transform-classes --save-dev

const plugins = ['@babel/transform-arrow-functions', '@babel/transform-classes'];
module.exports = { plugins };

再次使用babel处理,可以看到class和箭头函数都被转换了。

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
const app = function () {};
let Person = /*#__PURE__*/function () {
  function Person(name, age) {
    _classCallCheck(this, Person);
    this.name = name;
    this.age = age;
  }
  _createClass(Person, [{
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }, {
    key: "getAge",
    value: function getAge() {
      return this.age;
    }
  }]);
  return Person;
}();

plugins 的启用顺序

如果两个plugins都要对某一种类型的节点进行处理,处理顺序是怎样的? babel按照以下顺序对代码进行处理:

  • plugins先于presets
  • plugins数组内部处理顺序从左到右
  • presets数组内部处理顺序从右到左

transform-runtime

上面的plugins例子中可以看到在转换的时候,babel引入了很多自定义的函数(helper),比如_createClass,这样存在的问题是,每个使用class的文件里都会有这样一段一毛一样的代码,这种重复肯定是不可以接受的,所以babel提供了transform-runtime这个plugin。

// shell
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

// babel config
{
  "plugins": ["@babel/transform-runtime"]
}

转换完的代码中就可以看到:

import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";

var app = function () {};
var Person = /*#__PURE__*/function () {
  function Person(name, age) {
    _classCallCheck(this, Person);
    this.name = name;
    this.age = age;
  }
  _createClass(Person, [{
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }, {
    key: "getAge",
    value: function getAge() {
      return this.age;
    }
  }]);
  return Person;
}();

这样就都是引用同一个地方的代码了就不存在重复问题了。

transform runtime plugin提供了一些配置:

corejs

默认为false,可以设置为:false、2、3,不同的值需要安装不同的babel/runtime。

  • 为false,需要安装 babel/runtime
  • 为2,需要安装 babel/runtime-corejs2
  • 为3,需要安装 babel/runtime-corejs3

上面三个值的区别是什么?corejs: false的情况下,transform-plugin 只会对语法进行处理

const arr = [1, 2, 3]
arr.find(item => item > 2)

babel处理完如下:

var arr = [1, 2, 3];
arr.find(function (item) {
  return item > 2;
});

如果设置corejs为3并且安装了runtime-corejs3,babel处理过的代码如下:

import _findInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/find";
var arr = [1, 2, 3];
_findInstanceProperty(arr).call(arr, function (item) {
  return item > 2;
});

corejs: 2和corejs: 3的区别在于,2只能处理类方法,3能处理类方法和实例方法

presets

plugins有辣么多,挨个配置岂不是要了老命,所以babel提供了presets,来简化babel配置。 官方Presets:

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

下面主要介绍一下preset-env,其他几个preset从名字就可以看出来是非常有针对性的去处理一些指定情况。

@babel/preset-env

env所有presets最复杂的一个了,这个preset,可以根据我们对browserslist的配置,在转码时自动根据我们对转码后代码的目标运行环境的最低版本要求,采用更加“smart”的转码,如果我们设置的最低版本的环境,已经原生实现了要转码的ES特性,则会直接采用ES标准写法;如果最低版本环境,还不支持要转码的特性,则会自动注入对应的polyfill。 @babel/preset-env没有收入所有stage-x的plugins。所以preset-env不是万能的。如果我们用到某一个新的ES特性,还是proposal阶段,而且preset-env不提供转码支持的话,就得自己单独配置plugins。 那preset-env如何去使用呢?这里介绍日常会用到的配置:

target

告诉env要兼容的目标环境,上面说过,preset-env会很聪明的帮助我们进行代码转换,这里就是配置最低的目标环境。 例:

{
  "targets": "> 0.25%, not dead"
}

modules

设置babel转换的时候对于模块的导出方式,可以设置以下几个值:

  • amd
  • umd
  • systemjs
  • commonjs
  • cjs
  • auto
  • false 例: 新建一个src/lib/index.js
export const func = () => {};
export class Apple {
  constructor(price) {
    this.price = price;
  }
}

不设置module,babel处理后:

"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Apple = exports.func = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var func = function func() {};
exports.func = func;
var Apple = function Apple(price) {
  (0, _classCallCheck2["default"])(this, Apple);
  this.price = price;
};
exports.Apple = Apple;

设置module为false:

const presets = [['@babel/env', {modules: false}]];

经过babel处理后,可以看到模块导出方式不发生变化仍然是es module:

import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
export var func = function func() {};
export var Apple = function Apple(price) {
  _classCallCheck(this, Apple);
  this.price = price;
};

useBuildIns

useBuildIns 可以设置为 usage、entry、false,默认为false。该属性告诉env如何去处理polyfill 因为我们要对一些高级语法、类方法、实例方法进行polyfill,所以我们要引入core-js,

import "core-js";

设置 useBuiltIns: “entry”,之后,env会根据当前目标的环境从中取出需要的polyfill,例:

require("core-js/modules/es6.array.copy-within");
require("core-js/modules/es6.array.every");
require("core-js/modules/es6.array.fill");
require("core-js/modules/es6.array.filter");
require("core-js/modules/es6.array.find");
require("core-js/modules/es6.array.find-index");
require("core-js/modules/es7.array.flat-map");
require("core-js/modules/es6.array.for-each");
require("core-js/modules/es6.array.from");
// ...

可以看到babel根据当前环境从corejs里取出了很多的polyfill文件

设置useBuildIns为usage,则会根据当前代码使用到的需要polyfill的方法进行处理,例: 这里只用到了数组的find方法

const arr = [1, 2, 3]
arr.find(item => item > 2)

经过babel处理之后,也只会引入find这一个依赖,不会像上面设置为entry之后引入了很多没用到的依赖。

require("core-js/modules/es6.array.find");
var arr = [1, 2, 3];
arr.find(function (item) {
  return item > 2;
});

corejs

最后一个要介绍的配置是corejs 该属性设置使用的corejs的版本,默认是2

当设置 corejs为3并且useBuildIns为usage的时候,就不需要我们在代码入口处手动import corejs了。

总结

Babel是一个JS的compiler,开箱即用的babel不会做任何处理,Babel的插件可以对语法进行转换,如果需要polyfill,需要使用core-js和regenerator-runtime。通过设置presets-env的useBuildIns可以帮助我们更好的进行polyfill。