babel7配置

582 阅读8分钟

1.分享目的

通过实操,了解babel7.x的基本使用和配置,能够看懂项目中babel相关配置。在碰到相关babel问题时不是一头雾水,找不到解决问题的地方。

2.babel是什么

简单来说,babel就是一个js代码的转换器。它主要的使用场景有:

  1. 把ES6的代码转换为ES5代码,这样即使代码最终的运行环境(常见:浏览器)不支持ES6,在开发期间也能使用最新语法提升开发效率和质量;

  2. 有些ES6最新api,目标运行环境(常见:浏览器)还没有普遍提供实现,babel借助core-js对可以自动给js代码添加polyfill,以便最终在浏览器运行的代码能够正常使用那些api;babel始终能提供对最新es提案的支持;

  3. .jsx文件和ts文件等转化为js代码

  4. babel因为会对代码进行转换,所以可选地能够生成代码的sourcemap,便于排查特殊问题;

  5. 相关模块介绍

    包名功能说明
    @babel/corebabel编译核心包必装开发依赖 ,babel7版本的基础包
    @babel/cli命令行执行babel命令工具非必装开发依赖,packages.json的script中使用了babel命令则需安装
    babel-loaderwebpack中使用babel加载文件非必装开发依赖,webpack项目中使用
    @babel/plugin-*babel编译功能实现插件开发依赖,按照需要的功能安装
    @babel/preset-*功能实现插件预设开发依赖,插件集合。按照需要的功能安装,js语言新特性转换推荐使用preset-env
    @babel/plugin-transform-runtime复用工具函数非必装开发依赖,和@babel/runtime同时存在
    @babel/runtime工具函数库非必装生产依赖,和@babel/plugin-transform-runtime同时存在提供 helpers 函数,并会去安装 regenerator-runtime 包,只做语法转换(helpers 和 regenerator), 没有新 api 的实现。
    @babel/polyfill低版本浏览器兼容库非必装生产依赖,已不推荐使用,推荐通过preset-env的useBuiltIns属性按需引入
    core-js@*低版本浏览器兼容库非必装生产依赖,通过preset-env引入polyfill需安装此包,并通过corejs指定版本 给低版本浏览器提供es6+新特性接口的库。分为 core-js@2和@core-js@3
    regenerator-runtimeregenerator-runtime模块来自facebook的regenerator模块,主要作用是生成器函数、async、await函数经babel编译后,regenerator-runtime模块用于提供功能实现
    @babel/runtime-corejs*不污染变量的低版本浏览器兼容库非必装生产依赖,plugin-transform-runtime设置开启后,可以不污染变量的引入polyfill
    @babel/runtime-corejs3@babel/runtime-corejs3 包含 @babel/runtime 的全部并额外安装 core-js-pure@3
    @babel/preset-envjs语言新特性转换这是一个智能预设,只要安装这一个preset,就会根据你设置的目标浏览器,自动将代码中的新特性转换成目标浏览器支持的代码。 即使不设置targets,也会有一个默认值,规则为 > 0.5%, last 2 versions, Firefox ESR, not dead
  6. 为什么放在@babel下,没什么特别意义。表示一定是官方推出的包,比较权威,可信赖。

3.plugin,prests

npm init 然后一路enter
npm i @babel/core 安装babel7核心模块
npm i @babel/cli 安装babel可执行文件
创建 src/index.js 放我们的源文件
packagejson中增加 build: rm -rf ./lib/index.js && babel src -d lib 使用babel编译src下所有文件
src的index.js下增加如下代码用于检验babel的编译结果。
  let fn = async () => {}
  const res = [1,2,3].includes(3)
  class Test {}
  const A = new Map()

此时我们执行 npm run build。然后查看新生成的lib下index文件。发现新的文件和源文件一模一样。why?

因为我们没有通过配置规则告诉babel如何转化

3.1 如何配置babel的规则

  1. package.json加入babel的配置信息就行。
  2. babelrc和.babelrc.js。一种是json,一种是js
  3. babel.config.json,babel.config.js

3.2 plugin:转化规则的配置

babel的plugin分为三类:

  • syntax 语法类 如plugin-syntax-class-properties用于class语法转化
  • transform 转换类 如@babel/plugin-transform-arrow-functions 用于箭头函数转码
  • proposal 也是转换类,指代那些对ES Proposal进行转换的plugin。如@babel/plugin-proposal-decorators装饰器
// 首先npm i @babel/plugin-transform-arrow-functions

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

plugin的元素有两种类型:

  1. 纯字符串,用来标识一个plugin
  2. 另外一个数组,这个数组的第一个元素是字符串,用来标识一个plugin,第二个元素,是一个对象字面量,可以往plugin传递options配置
  3. 如 plugin: ['@babel/plugin-transform-arrow-functions']或者 [["@babel/plugin-transform-async-to-generator",{"module": "bluebird","method": "coroutine"}]]

此时我们npm run build 发现箭头函数被转化了。但是class等还未被转化,因为没有安装相关插件。那么如果我需要转化什么就安装什么插件岂不是很麻烦?

3.3 presets:插件的集合

目前官方推荐的preset,有下面四个:

  • @babel/preset-env 所有项目都会用到的
  • @babel/preset-flow flow需要的
  • @babel/preset-react react框架需要的
  • @babel/preset-typescript typescript需要的

当然也可以自己写presets,本质上就是一个插件数组。

4.@babel/preset-env

babel最重要的一个preset,而且功能比上面自定义的preset要复杂的多,所以有必要详细的学习。

"dependencies": {
    "@babel/compat-data": "^7.16.8",
    "@babel/helper-compilation-targets": "^7.16.7",
    "@babel/helper-plugin-utils": "^7.16.7",
    "@babel/helper-validator-option": "^7.16.7",
    "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7",
    ......,
    "@babel/plugin-transform-typeof-symbol": "^7.16.7",
    "@babel/plugin-transform-unicode-escapes": "^7.16.7",
    "@babel/plugin-transform-unicode-regex": "^7.16.7",
    "@babel/preset-modules": "^0.1.5",
    "@babel/types": "^7.16.8",
}

preset-env是会一直变化的,所以最好能保持其能一直更新。

并且preset-env不是万能的。 如果我们用到某一个新的ES特性,还是proposal阶段,而且preset-env不提供转码支持的话,就得自己单独配置plugins了。

此时我们修改babel.config.js,增加@babel/preset-env

const presets = [ ["@babel/preset-env"]];

module.exports = { presets };

// 转化后代码如下

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); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// require('core-js')var fn = function fn() {};
var res = [1, 2, 3].includes(3);
var Test = /#PURE/_createClass(function Test() { _classCallCheck(this, Test);});
var A = new Map();

我们发现所有的class,map,箭头函数都被转化了。正是我们想要的结果。但是发现includes方法还在,有些浏览器是不支持includes方法的。这需要我们进行额外的配置

在这之前我们了解下@babel/preset-env options的几个重要相关配置

target: 目标运行环境 如 ' > 0.5%, last 2 versions, Firefox ESR, not dead' 这也是其默认规则。也可以是对象,如 { chrome: 97 } 参考:Browserslist配置说明

modules: 这个用于配置是否启用将ES6的模块转换其它规范的模块 默认auto

corejs:指定preset-env进行polyfill时,要使用的corejs版本。默认2,建议修改为3

useBuiltIns:由于@babel/polyfill在babel7.4开始,也不支持使用了。 所以现在要用preset-env,必须是得单独安装core-js v3。主要有两个value: entry和usage。 这两个值,不管是哪一个,都会把core-js的modules注入到转换后的代码里面,充当polyfill。

下面演示一下useBuiltIns为usage和entry的用法。以及两者的优缺点。

当设置为entry时。需要手动在入口文件引入corejs

const presets = [

["@babel/preset-env", {

useBuiltIns: "entry",

corejs: 3

}]

];

入口文件: require('core-js')

打包后代码如下:

"use strict";
require("core-js/modules/es.symbol.js");
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/es.symbol.async-iterator.js");

.......

require("core-js/modules/web.url-search-params.js");
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); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var fn = function fn() {};
var res = [1, 2, 3].includes(3);
var Test = /#PURE/_createClass(function Test() { _classCallCheck(this, Test);});
var A = new Map();

将所有的垫片全部引入了

这样的优点是全局彻底ployfill,就算 node_modules 中的依赖存在不兼容的代码,也能成功运行,缺点也很明显,引入过多不必要的代码

当设置为usage时

const presets = [

["@babel/preset-env", {

useBuiltIns: "usage",

corejs: 3

}]

];

生成代码如下
require("core-js/modules/es.object.define-property.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.string.iterator.js");
require("core-js/modules/web.dom-collections.iterator.js");
require("core-js/modules/es.promise.js");
require("regenerator-runtime/runtime.js");
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); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = genkey; var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
// require('core-js')var fn = /#PURE/function () { var _ref = _asyncToGenerator( /#PURE/regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } } }, _callee); }));
return function fn() { return _ref.apply(this, arguments); };}();
var res = [1, 2, 3].includes(3);
var Test = /#PURE/_createClass(function Test() { _classCallCheck(this, Test);});
var A = new Map();

我们发现不是所有的都被require了,仅仅部分。真正做到了按需加载。缺点是无法处理node_modules中内容

如果以上配置增加target: {chrome:97}的配置,会发现箭头函数,class等都继续存在。因为97版本的chrome以上都支持。

插件和presets的执行顺序

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)。

5.@babel/plugin-transform-runtime

官方说明:A plugin that enables the re-use of Babel's injected helper code to save on codesize.

helpers: true // 开启引入帮助函数
regenerator: true // 不用全局的regeneratorRuntime,关闭后打包代码会有regeneratorRuntime变量
corejs: false // 可设置为 { version: 2/3, proposals: false/true } 开启后表示开启了沙箱模式,不会污染全局变量。但是实验下来此选项会让useBuiltIns的usage失效。

但是以上的配置有存在两个问题。

一是污染了全局变量。比如regeneratorRuntime这个变量他直接使用了。说明在window下有regeneratorRuntime变量。包括includes方法。

二是增加了像_createClass这样的辅助函数。如果有多个文件,那么就会创建多个辅助函数。

增加runtime配置

["@babel/preset-env", {useBuiltIns: "usage",corejs: 3}]];

const plugins = [["@babel/plugin-transform-runtime",{corejs: 3}]];
module.exports = { presets, plugins };

// 然后执行build打包后代码如下

"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var _context2;
// require('core-js')var fn = /#PURE/function () { var _ref = (0, _asyncToGenerator2["default"])( /#PURE/_regenerator["default"].mark(function _callee() { return _regenerator["default"].wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } } }, _callee); }));
return function fn() { return _ref.apply(this, arguments); };}();
var res = (0, _includes["default"])(_context2 = [1, 2, 3]).call(_context2, 3);var Test = /#PURE/(0, _createClass2["default"])(function Test() { (0, _classCallCheck2["default"])(this, Test);});var A = new _map"default";

发现以上两个问题都解决了,同时所有模块都是引自@babel/runtime-corejs3模块下

如果使用了装饰器语法,该如何支持呢:['@babel/plugin-proposal-decorators', { legacy: true }], legacy:使用历史遗留(stage 1)的装饰器中的语法和行为

6.几种配置结果

结论:

  • 在preset-env中配置useBuiltIns:usage,会按需引入相关的polyfill,但是不会提取公共的help函数,以及会污染全局对象和对象原型
  • 在第一点的基础上,新增@babel/plugin-transform-runtime但是core-js指定为false,结果相对于第一点,不同的是这次提取了公共的help函数。
  • 在第二点的基础上,runtime插件指定core-js版本3,会导致useBuiltIns:usage配置无效。并且也会按需引入相关polyfill,提取公共help函数,但是不会污染全局对象和对象原型。这种配置适合打包npm库。
  • 实际业务需求开发中,采用第二点方案较为合适,但是这种混用的方式,在github上有相关issue(github.com/babel/babel…),并且bable团队成员不推荐这种混用的方式。

参考:

babel详解

babel中文官网