babel@7.x 基础核心详解

1,649 阅读29分钟

babel核心概念

babel已经推出7.x版本,与babel紧密关联的core-js也推出了3.x版本,与babel7.xcore-js3.x对应的是babel6.xcore-js2.xbabel7.xcore-js3.x有很多新变化,值得学习,并且目前业务中的很多还使用的是babel6.x,考虑在现有项目中对babel6.x进行升级处理,或者新的项目,肯定直接上babel7.x新的配置了;

从本篇开始,我将尝试从细节方面来总结最新版本babel(7.x)的作用及用法。经过这几天了解babel和core-js的最新文档,我认为熟练地掌握babel7.x有以下一些要点:

  • babel是什么
  • babel的plugins有什么作用
  • babel的presets有什么作用
  • babel/polyfill做什么用,现在有什么变化
  • 什么是transform-runtime
  • core-js是什么,它是怎么与babel集成使用的,core-js@3与core-js@2有什么变化
  • 什么是babel/register
  • 如何选择babel的配置方式?各个配置方式的应用场景是怎样的
  • babel的配置options中各个选项的含义和作用
  • 常见的babel使用方式都有哪些
  • 如何升级babel7.x,目前其它与babel结合使用的工具,如webpack,对babel7.x的支持情况咋样?
  • babel的api能做什么

以上要点涉及内容非常多,如果上面这些问题都会了,那碰到babel的一些错误或者需要用babel来解决一些工程上的问题,基本没啥问题;

本篇围绕babel等一些基本概念做一些记录和总结。

1.babel是什么

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

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

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

  • 因为babel可以对代码进行处理,有些大厂团队,利用babel在做源码方面的处理(codemods),因为大厂人多,每个人开发习惯都有差异,为了规范代码风格,提高代码质量,借助babel,将每个人的源码做一定的标准化转换处理,能够提升团队整体的开发质量;

  • vue框架推荐使用单文件.vue格式的方式来编写组件,它也是借助babel,将.vue文件,转换为标准的ES代码;

  • react框架普遍使用JSX语法来编写模板,当然vue框架也可以使用jsx,借助babel,jsx也能被正确地转换为ES代码;

  • js本身是弱类型语言,但是在大厂里面,人多特别容易因为语言本身一些小错误导致出现严重bug,所以如果在代码上线之前,借助强类型的语言静态分析能力,对js代码做一些检查的话,就能规避不少问题,flow和typescript目前是两种主流的强类型js的编程方式,babel能够将flow和typescript的代码,转换为标准的ES代码;

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

  • babel尽可能地在遵守ES的规格,当在学习babel的presets和plugins的时候,可能会看到有spec这个option,只要这个option启用了,相应presets和plugins就会按照更加符合规格的方式,对ES代码进行转换,只不过这个option一般是false,表示不启用,这样babel的转换速度会快一些;

  • node运行环境,目前对es6的modules支持不是很好,babel提供了另外解决方案,可以把es6编写的modules在被require的时候,自动进行代码转换,虽然没法评定这个方案的优劣,但是也能感受到babel为了让开发人员能够使用最新ES语法这方面确实很努力;

开箱即用的babel什么也不做。这句话的含义就是如果不对babel不做任何配置,一段ES6的代码经过babel处理,还是原来那段代码。babel的功能都是通过plugin来完成的。每个plugin都有特定的代码处理的功能,只有将它配置到babel里面运行,才能对代码进行转换。babel们目前有很多的plugin,既有官方的,也有第三方的。

因为插件太多,每次配置起来都特别麻烦,每个项目需要的plugin,通常都是相似的一组plugin,所以babel推出了presets,每一个presets都包含若干个plugin,只要配置了presets,意味着这个preset包含的所有插件,都可能会参与到代码的转换当中。常见的preset有:preset-env, preset-state-0 ,preset-stage-2等等。

plugins和presets都是要经过配置才能生效的,除了这两个,babel还有很多其它的options可以配置,只有在挨个了解了每个option的作用之后,才能灵活得在项目中使用它们,所以babel的options也是学习babel的目标之一。

2. babel 有哪些使用场景

babel的options有很多种配置方式,可以在package.json中配置,也可以在babel.config.js中配置,可以在json格式的.babelrc文件中配置,也可以在js格式的.babelrc.js文件中配置。因为babel有命令行工具,所以options也可以直接在babel的命令行上使用时配置;因为babel有提供api,让开发者可以手工调用babel对代码进行转换,所以options也可以在api调用时配置。

调用babel有很多种方式,最底层的应该是直接利用api调用babel;后面要说的其它方式,都脱离不了这层形式。babel提供了命令行工具babel-cli,可以直接通过命令行对文件或文件夹进行转换,这个方式对于测试babel的一些特性,还比较方便。

babel可以跟webpackgulp等构建工具结合起来使用,当跟他们结合时,babel将由构建工具自动调用;这也是目前前端开发中,babel最常见的使用入口。babel还可以跟lint工具如eslint这些结合使用。在node环境中,babel提供了babel-register,实现ES6的文件被node加载时自动转换为ES5的代码。

ES6从语法上转换到ES5,babel是可以做到的,但是一些新的api,本身不是语法上不支持,而是ES5的运行环境没有对应的实现; 所以babel的一项重要职责就是代码的polyfill。在babel7之前,babel专门提供了一个库叫babel/polyfill来做这件事情,在babel7之后,这个库被废弃了,因为polyfill有了新的使用方式。 这也是babel7.x学习的重要内容之一。

因为babel在转换过程中,会利用很多babel自己的工具函数:helpers。在不经过优化的时候,每个文件都会单独包含这些helpers代码,如果文件很多,就会导致大量的重复代码,所以babel专门推出了transform-runtime来对这些helpers进行自动提取和其它优化。

babel对代码的polyfill,是利用另外两个库来做的:core-jsregenerator-runtime。core-js目前升级到了3.x版本,跟2.x区别也很多;regenerator-runtime没有什么变化。core-js@3.x的版本,也值得学习,将来很有可能会直接使用这个库里面的东西,所以需要掌握它是如何组织ES的各个模块实现的。

以上就是跟babel使用密切相关一些概念和要点。babel始终在和最新的ES规范打交道,所以ESMAScript相关概念应该要比较熟悉。

3. ESMAScript阶段

一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由TC39委员会批准。

  • Stage 0 - Strawman(展示阶段)
  • Stage 1 - Proposal(征求意见阶段)
  • Stage 2 - Draft(草案阶段)
  • Stage 3 - Candidate(候选人阶段)
  • Stage 4 - Finished(定案阶段)

tc39当前的所有提案都可以在github上查看:github.com/tc39/ecma26…

也就是说只有stage 4阶段的提案,才可能在每年7月份的时候,加入到当前的新标准发布中。 我们现在通常所说的ES6,其实是泛指ES2015发布以后,所有的ES版本,包括ES6、8、9、10。当别人说ES8、9、10的时候,说的其实就是ES2017、ES2018、ES2019。了解以上这些之后,对于一个前端开发者来说,关注TC39每年都有哪些新的提案,每年新的ESMA-262发布,都添加了哪些特性进入标准,应该是一个保持关注技术新特性的方式。

4. babel7.x中@babel符号是什么含义

从babel7.0开始,babel一系列的包都以@babel开头,这个跟babel没关系,是npm包的一种形式。简而言之,@符号开始的包,如@babel/preset-env,代表的是一类有scope限定的npm包。scope通常的含义代表的就是一个公司或者一个机构、甚至个人,它的作用就是将同一个主体的相关包都集中在一个范围内来组织和管理,这个范围就是scope。这类有scope的包,最大的好处就是只有scope的主体公司、机构和个人,才能往这个scope里面添加新的包,别人都不行;也就是说以@开头的npm包,一定是官方自己推出或者官方认可推出的包,比较有权威性质。

Babel Pugins

开箱即用的babel,什么也不做。如果要让它对代码进行转换,就得配置plugins才能有效。本篇说明babel的plugins的用法以及babel7中plugin的一些变化。

1.babel-cli

为了学习plugin的用法,我们需要先掌握一种babel的使用方法,babel-cli是一个不错的选择,因为它足够简单。现在,随便新建一个文件夹,然后在这个文件夹下运行:

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

就把babel的核心库以及babel的命令行管理工具babel-cli安装到项目的依赖里面了。

接着,在这个文件夹下,新建一个.babelrc.js文件,这是一个js文件,它是对babel进行配置的方式之一;本篇学习plugin的时候,会把plugin写到.babelrc.js里面,这样babel运行的时候,这个文件内配置的plugin就会启用。

最后为了有一个编写测试代码的地方以及有一个能够看到babel进行代码转换后的地方,在这个文件夹内,再添加两个子文件夹,分别是src和dist。最终建好的项目文件夹结构示意如下:

babel-plugin/
    dist/
    node_modules/
    src
    .babelrc.js
    package.json

为了演示babel-cli的用法,往src内添加一个js文件test01.js,并编写如下ES代码:

let foo = () => {
};

接着在终端切换到babel-plugin/文件夹,运行以下命令(本篇后续执行npx babel命令,都是切换到babel-plugin/文件夹下执行):

npx babel src --out-dir dist

这个命令会把src文件夹下的所有js,全部输入到babel,转换完成后,输出存储到dist文件夹。由于目前babel并没有做任何配置,所以上面的命令运行后,应该只能看到一个有一定的格式变化,但是代码内容没变的dist/test01.js

接下来的内容,都将使用上面准备的这个小环境来测试。

2.plugins的使用

当使用.babelrc.js文件来配置babel时,该文件配置结构通常是:

const presets = [  ];
const plugins = [ ];

module.exports = { presets, plugins };

其中presets数组用来配置babel的presets,plugins数组用来配置babel的plugins。presets和plugins都可以配置多个。

3.plugin的名称

在开始使用前,先说下plugin的名称。从babel7开始,babel所有的包,不仅是plugin的包,还有preset的包,都全部变为了@babel这个形式的scope包。如:

@babel/plugin-transform-arrow-functions 用于箭头函数转码
@babel/plugin-transform-block-scoping 块级作用域转码
@babel/plugin-transform-for-of for-of循环转码
etc

(查看全部:babeljs.io/docs/en/plu…

如果要使用babel7,在确定需要使用某一个plugin的时候,一定要先确定它是不是babel自己的包,如果是,就要通过@babel这个scope来安装,否则很有可能会安装到babel6的相关包;如果它不是babel自己的包,那肯定不能用@babel这个scope来安装,用它确定的名称即可。比如transform-arrow-functions这个plugin,如果是babel6,它的npm包名称则为:babel-plugin-transform-es2015-arrow-functions,如果是babel7,它的npm包名称则为:@babel/plugin-transform-arrow-functions

babel7与babel6的plugin名称区别,基本就是把babel6的babel-前缀,替换为了@babel/作为前缀。 但也不绝对,比如前面babel-plugin-transform-es2015-arrow-functions这个包,在babel7里面,还去掉了es2015的字符。这也是babel7中对于plugin做出另外一个变化之一:

babel7去掉了plugin包名称里面跟es版本有关的部分,比如es3、es2015。关于plugin名称,另外一个比较大的变化是:

如果某个plugin要转换的不是ECMA-262每年正式发布的特性(ES2015, ES2016, etc),这个plugin的名称就会被重名为-proposal修饰的名称。它这么做的目的,显然是为了让plugin的使用场景更加清晰;而且,一旦某个plugin要转换的特性,已经进入TC39工作流程的Stage 4这个状态,还会对plugin包的名称再做重命名(Stage 4意味着这个特性即将在下一年的ECMA-262中发布)。

综上所述,在以后使用babel7的plugin时,对包的名称得稍微谨慎一点,最好到它的npm主页或github主页上查看下readme说明,防止低级错误出现。

4.plugin的分类

babel的plugin分为三类:

  • syntax 语法类
  • transform 转换类
  • proposal 也是转换类,指代那些对ES Proposal进行转换的plugin。

通过查看babel的github代码结构,可以很清晰地看到以上三类插件的源码文件夹名称: github.com/babel/babel…

syntax类plugin用于ES新语法的转换,其实也是使用的时候必须的,但是当使用某一个transform类或proposal类的插件时,如果需要做某个语法转换,则相应的syntax类插件,会自动启用,所以在使用babel的时候,syntax类plugin,不需要单独配置。比如说下面这个transform类plugin,是用来转换typescript的,从它源码的package.json可以看到,它依赖了@babel/plugin-syntax-typescript这个syntax类的plugin:

{
  "name": "@babel/plugin-transform-typescript",
  "version": "7.5.5",
  "description": "Transform TypeScript into ES.next",
  "dependencies": {
    "@babel/helper-create-class-features-plugin": "^7.5.5",
    "@babel/helper-plugin-utils": "^7.0.0",
    "@babel/plugin-syntax-typescript": "^7.2.0"
  },
  "peerDependencies": {
    "@babel/core": "^7.0.0-0"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/helper-plugin-test-runner": "^7.0.0"
  }
}

5. 使用示例

打开小项目的.babelrc.js文件,把以下内容放进去:

const presets = [];
const plugins = [];

module.exports = {presets, plugins}

我们准备往plugins里面添加点东西,来试试babel。

plugins是一个数组,它的每个元素代表一个单独的plugin:

["pluginA", ["pluginA", {}]]

元素有两种类型:

  • 纯字符串,用来标识一个plugin
  • 另外一个数组,这个数组的第一个元素是字符串,用来标识一个plugin,第二个元素,是一个对象字面量,可以往plugin传递options配置

纯字符串形式的plugins元素,是数组形式的简化使用。因为plugin是可以配置option的,所以纯字符串的plugin元素,相当于全部使用options的默认值,不单独配置。

举例如下:

const plugins = [
    '@babel/plugin-transform-arrow-functions',
    [
      "@babel/plugin-transform-async-to-generator",
      {
         "module": "bluebird",
         "method": "coroutine"
       }
    ]
];

如何用字符串标识一个plugin?

如果plugin是一个npm包,则可以直接使用这个npm包的名称 如果plugin是本地的一个文件,则可以相对路径或绝对路径引用这个文件,来作为plugin的标识

plugin标识的缩写?出于历史原因,babel为plugin的标识配置提供了缩写规则,只要一个plugin是以babel-plugin-开头的,就可以省略babel-plugin-,如以下两种方式都是等价的:

const plugins = [
    [
        "myPlugin",
        "babel-plugin-myPlugin" // equivalent
    ]
];

如果一个plugin是一个scope包,以上缩写规则同样成立:

const plugins = [
    "@org/babel-plugin-name",
    "@org/name" // equivalent
]

注意:babel-plugin-myPlugin不一定是babel自己的包;@org也不指@babel,别的机构也可以把自己开发的babel包作为scope包形式发布。

babel7因为包都变为scope包了,所以有了新的缩写规则:

const plugins = [
    '@babel/transform-arrow-functions',// 等价于@babel/plugin-transform-arrow-functions
    [
      "@babel/transform-async-to-generator",// 等价于@babel/plugin-transform-async-to-generator
      {
         "module": "bluebird",
         "method": "coroutine"
       }
    ]
];

不过似乎babel的作者,也并不觉得plugin缩写是很有必要的事情,说不定将来缩写规则就会取消了,所以实际使用中的话,要引用一个plugin,还是直接用完整的npm包名称比较稳妥,反正也没多几个字。

为了演示plugin的作用,我们可以选用几个有代表性的ES6特性,来应用相应的plugin:

@babel/plugin-transform-classes 这个plugin可以转换ES6class
@babel/plugin-transform-arrow-functions 这个plugin可以转换ES6的箭头函数
@babel/plugin-transform-computed-properties 这个plugin可以转换ES6的属性名表达式

安装成功后,把它们配置到.babelrc.js文件中:

const presets = [];
const plugins = [
    '@babel/plugin-transform-arrow-functions',
    ['@babel/plugin-transform-classes'],
    '@babel/plugin-transform-computed-properties'
];

module.exports = {presets, plugins}

接下来在src目录下,继续编辑test01.js,并将其替换为:

// 箭头函数
let foo = () => {

};

// ES6 class
class List {
    constructor(pi = 1, ps = 10) {
        this.pi = 1;
        this.ps = 10;
    }

    loadData() {

    }

    static genId(){
        return ++this.id;
    }
}

let name = 'lyzg';

let obj = {
    baseName: name,
    [name + '_id']: 'baseName'
};

这是一段包含箭头函数、class和属性名表达式等ES6特性的代码。接着运行npx babel src --out-dir dist,执行完之后,打开dist/01.js,应该能查看到如下转换后的代码:

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

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; }

// 箭头函数
let foo = function () {}; // ES6 class


let List =
/*#__PURE__*/
function () {
  function List(pi = 1, ps = 10) {
    _classCallCheck(this, List);

    this.pi = 1;
    this.ps = 10;
  }

  _createClass(List, [{
    key: "loadData",
    value: function loadData() {}
  }], [{
    key: "genId",
    value: function genId() {
      return ++this.id;
    }
  }]);

  return List;
}();

let name = 'lyzg';

let obj = _defineProperty({
  baseName: name
}, name + '_id', 'baseName');

以上就是babel插件的核心用法。

细心观察的话,可以看到dist/test01.js中还包含有let关键字,说明我用的三个插件并不具备let声明转码的功能,所以当我们选择自己配置插件,来进行代码转换的时候,就得对自己所需的插件非常清楚才行。以ES6的class来说,相关的转码插件不止@babel/plugin-transform-classes一个,因为ES6class的特性,不全都是ES2015发布的,现在也还有它的新特性还处于proposal阶段,所以想使用全面的ES6class,就得了解更多相关插件才行。正是因为自己组合plugin,会比较麻烦,所以babel推出了presets,来简化plugins的使用

6.transform plugins

babel的transform plugins主要分为以下一些类别:

  • ES3 对ES3的一些特性做转换
  • ES5 对ES5的一些特性做转换
  • ES2015 对ES6的特性做转换,大部分plugin都是这个类别的
  • ES2016 对ES7的特性做转换
  • ES2017 对ES8的特性做转换
  • ES2018 对ES9的特性做转换
  • Modules 自动转换代码的模块组织方式
  • Experimental 提案中的特性转换
  • Minification 压缩代码体积,这个类别下的插件,没有部署在@babel里面,而是作为一个独立的库来管理的:babel/minify,而且这个库还是一个实验性的项目,没有发布正式版,所以babel没有推荐在生产环境中使用
  • React 用于react代码转换
  • Other 其它

点击了解以上分类的明细

7.syntax plugins

babel的syntax plugins不是用来转换代码的,而是用来对ES6新的语法特性进行解析的,如果直接使用syntax plugin,代码不会有任何转换。要对新语法进行转换,就必须使用对应的transform plugins。syntax plugin会被transform plugin依赖,用于语法解析。

8.plugin的启用顺序

前面了解到babel是基于plugin来使用的,plugin可以配置多个;同时babel还提供了preset,preset基本上可以看作是一组plugin。如果有这么多个plugin,对源代码进行解析,肯定要有一个处理的先后顺序,前一个plugin的处理结果,将作为下一个plugin的输入。所以babel规定了plugin的启用顺序:

  • 配置中plugins内直接配置的plugin,先于presets中的plugin
  • 配置中plugins数组内的plugin,按照数组索引顺序启用
  • 配置中presets数组内的presets,按照数组索引顺序逆序启用,也就是先应用后面的presets,再应用前面的preset。

如:

{
  "plugins": ["transform-decorators-legacy", "transform-class-properties"]
}

先启用transform-decorators-legacy,然后才是transform-class-properties。

{
  "presets": ["es2015", "react", "stage-2"]
}

preset的启用顺序:stage-2 react es2015

9.plugin的options

前面介绍plugins如何配置时,简化了这个部分的说明,babel官方文档对plugin和preset的配置,有明确的声明,而且plugin和preset的配置方式是一致的:但每个plugin的options其实都不一样,本文记录几个在babel官方文档中经常出现的option:

  • loose 启用松散式的代码转换,假如某个插件支持这个option,转换后的代码,会更加简单,代码量更少,但是不会严格遵循ES的规格,通常默认是false
  • spec 启用更加符合ES规格的代码转换,默认也是false,转换后的代码,会增加很多helper函数,代码量更大,但是代码质量更好
  • legacy 启用旧的实现来对代码做转换。详见后面举例
  • useBuiltIns 如果为true,则在转换过程中,会尽可能地使用运行环境已经支持的实现,而不是引入polyfill

举例来说:@babel/plugin-proposal-object-rest-spread这个插件,在babel7里面,默认转换行为等同于spec: true,所以它不再提供spec这个option,它下面这段代码:

let bar = {...obj};

转换为:

function ownKeys(object, enumerableOnly) { ...; }

function _objectSpread(target) { ...; }

function _defineProperty() { ...; }

let bar = _objectSpread({}, obj);

这个插件支持loose和useBuiltIns这两个option。如果启用loose则代码会转换为:

function _extends() { ...; }

let bar = _extends({}, obj);

如果同时启用loose和useBuiltIns,则代码会转换为:

let bar = Object.assign({}, obj);

看!loose和useBuiltIns会让转换后的代码越来越简单,但是也跟ES规格表达的需求偏离地越来越远。

  • legacy

由于ES6的decorators语法有了新的编写方式,所以babel7把@babel/plugin-proposal-decorators插件默认对ES6 decorators语法的转换,启用了新写法的转码,如果在编码时,还在使用旧的ES6的decorators语法,则在使用这个插件的时候,应该启用legacy option,以便这个插件,仍能对旧语法进行转码。

Babel preset

babel的presets是用来简化plugins的使用的。本篇介绍babel presets相关的内容。

官方推荐的preset

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

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

其它的preset,如state-2, state-3, es2015这些从babel7开始已经不推荐使用了。

1.自定义一个preset

要创建一个自己的preset,很简单,创建一个下面形式的模块:

module.exports = function() {
  return {
    plugins: [
      "pluginA",
      "pluginB",
      "pluginC",
    ]
  };
}

上面这个简单的文件,就可以作为一个babel的preset。当babel的配置中,引用上面这个preset时,这个preset内定义的plugins就会生效。另外preset除了可以包含plugins,还可以包含其它的presets,只要类似下面的形式定义就可以了:

仔细看一下,preset对外export的这个函数返回值,是不是跟.babelrc.js里面的配置很像呢!其实就一样的,我们在.babelrc.js里面怎么配置preset和plugin,这个地方就怎么配置preset和plugin。

所以要定义一个仅仅是plugin集合作用的preset还是很简单的。

2.preset配置与使用

preset的配置方式与plugin完全一致。查看官方对plugin/preset配置项的说明

继续利用上一篇用到的那个小项目,并在项目根目录新建一个文件my-preset.js:

module.exports = () => ({
  plugins: [
    ['@babel/plugin-transform-arrow-functions'],
    ['@babel/plugin-transform-classes', {spec: false}],
    ['@babel/plugin-transform-computed-properties'],
    ['@babel/plugin-proposal-object-rest-spread', {loose: true, useBuiltIns: true}]
  ]
});

把上一篇文章里面,在.babelrc.js里配置的所有plugins,全部都移动到my-preset.js,我想把这个文件等会作为一个preset,配置到.babelrc.js里面,试试babel转码的结果。.babelrc.js最终修改为:

const presets = [
    './my-preset.js'
];
const plugins = [
];

module.exports = {presets, plugins}

然后运行npx babel src --out-dir dist,即可查看这个自定义preset配置后的转码结果。

3. preset名称

上一篇介绍plugin名称的时候,我尝试从自己的角度来说明这个东西的规律。但实际上babel官方文档对plugin/preset的名称,有一个部分很详细的说明了这里面的规则,而且这个部分有一个专门的名称叫做Name Normalization,包含各种名称类型,如相对路径、绝对路径、npm包、scope npm包以及名称的简写规则的。详情请参考

4. preset的顺序

上一篇也介绍过,presets是可以配置多个的,但是preset的启用顺序,与它在presets配置数组中的索引顺序是相反的。

5. preset options

不同的preset支持不同的options,options的配置方式跟plugin是一样的,例如:

{
  "presets": [
    ["@babel/preset-env", {
      "loose": true,
      "modules": false
    }]
  ]
}

6. @babel/preset-env

这是当前babel最重要的一个preset,而且功能比上面自定义的preset要复杂的多,所以有必要详细的学习。这个preset,可以根据我们对browserslist的配置,在转码时自动根据我们对转码后代码的目标运行环境的最低版本要求,采用更加“聪明”的转码,如果我们设置的最低版本的环境,已经原生实现了要转码的ES特性,则会直接采用ES标准写法;如果最低版本环境,还不支持要转码的特性,则会自动注入对应的polyfill。

browserslist应该已经不陌生了, 前端构建工具里面,很多都跟它有关系。除了browserslist,@babel/preset-env,还依赖了另外两个库来完成它的实现:compat-table, and electron-to-chromium。后面两个帮助preset-env,知道ES6的特性,在不同的平台、不同的运行环境中,都是从哪个版本开始原生支持的。

@babel/preset-env不支持所有stage-x的plugins。通过查看preset-env的package.json文件,就能知道它需要哪些plugins:

{
  "name": "@babel/preset-env",
  "version": "7.5.5",
  "dependencies": {
    "@babel/helper-module-imports": "^7.0.0",
    "@babel/helper-plugin-utils": "^7.0.0",
    "@babel/plugin-proposal-async-generator-functions": "^7.2.0",
    "@babel/plugin-proposal-dynamic-import": "^7.5.0",
    "@babel/plugin-proposal-json-strings": "^7.2.0",
    "@babel/plugin-proposal-object-rest-spread": "^7.5.5",
    "@babel/plugin-proposal-optional-catch-binding": "^7.2.0",
    "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
    "@babel/plugin-syntax-async-generators": "^7.2.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/plugin-syntax-json-strings": "^7.2.0",
    "@babel/plugin-syntax-object-rest-spread": "^7.2.0",
    "@babel/plugin-syntax-optional-catch-binding": "^7.2.0",
    "@babel/plugin-transform-arrow-functions": "^7.2.0",
    "@babel/plugin-transform-async-to-generator": "^7.5.0",
    "@babel/plugin-transform-block-scoped-functions": "^7.2.0",
    "@babel/plugin-transform-block-scoping": "^7.5.5",
    "@babel/plugin-transform-classes": "^7.5.5",
    "@babel/plugin-transform-computed-properties": "^7.2.0",
    "@babel/plugin-transform-destructuring": "^7.5.0",
    "@babel/plugin-transform-dotall-regex": "^7.4.4",
    "@babel/plugin-transform-duplicate-keys": "^7.5.0",
    "@babel/plugin-transform-exponentiation-operator": "^7.2.0",
    "@babel/plugin-transform-for-of": "^7.4.4",
    "@babel/plugin-transform-function-name": "^7.4.4",
    "@babel/plugin-transform-literals": "^7.2.0",
    "@babel/plugin-transform-member-expression-literals": "^7.2.0",
    "@babel/plugin-transform-modules-amd": "^7.5.0",
    "@babel/plugin-transform-modules-commonjs": "^7.5.0",
    "@babel/plugin-transform-modules-systemjs": "^7.5.0",
    "@babel/plugin-transform-modules-umd": "^7.2.0",
    "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5",
    "@babel/plugin-transform-new-target": "^7.4.4",
    "@babel/plugin-transform-object-super": "^7.5.5",
    "@babel/plugin-transform-parameters": "^7.4.4",
    "@babel/plugin-transform-property-literals": "^7.2.0",
    "@babel/plugin-transform-regenerator": "^7.4.5",
    "@babel/plugin-transform-reserved-words": "^7.2.0",
    "@babel/plugin-transform-shorthand-properties": "^7.2.0",
    "@babel/plugin-transform-spread": "^7.2.0",
    "@babel/plugin-transform-sticky-regex": "^7.2.0",
    "@babel/plugin-transform-template-literals": "^7.4.4",
    "@babel/plugin-transform-typeof-symbol": "^7.2.0",
    "@babel/plugin-transform-unicode-regex": "^7.4.4",
    "@babel/types": "^7.5.5",
    "browserslist": "^4.6.0",
    "core-js-compat": "^3.1.1",
    "invariant": "^2.2.2",
    "js-levenshtein": "^1.1.3",
    "semver": "^5.5.0"
  }
}

上一篇说过babel的plugin,现在把还在处于proposal阶段的plugin都命名为了-proposal形式的plugin。 非proposal的plugin都变为-transform形式的plugin了。为什么上面的package.json还会包含几个-proposal的plugin呢?这是因为以上几个-proposal的plugin在我写文这个时间已经进展到stage-4了,它变为-trasform的plugin是早晚的事,所以preset-env才会包含它们。

由于proposal会不断地变化,意味着preset-env也会跟着调整,所以保持preset-env的更新,在平常的项目中也是比较重要的一项工作。

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

stage-x的preset已经不再推荐使用了,而且preset-env不支持stage-x的plugin,但是曾经的vue项目,还使用了stage-2这个preset,意味着stage-2里面的一些plugin还是vue项目需要的,如何用plugin单独配置的方式来取代stage-2呢?babel官方提供了一个替代的说明文件:如何替代stage-x的preset

7.browserslist

@babel/preset-env,需要像autoprefixer那样,配置一个browserslist,以便确认目标运行环境。虽然官方推荐我们使用跟autoprefixer一样的配置方式,借助.browserslistrc这个文件配置。我个人认为,preset-env的browserslist还是应该在preset-env里面独立配置,因为又不见得别的工具跟preset-env,对于目标环境的需求是完全相同的。preset-env的options里面有一个target option,就可以用来单独为它配置browserslist。

browserlist的配置方式需阅读它们的官方文档。

写好一个browserlist字符串,可以通过npx browserlist string来检测,如:npx browserslist 'iOS > 8, Android > 4'

> npx browserslist 'iOS > 8, Android > 4'
npx: installed 5 in 4.052s
android 67
android 4.4.3-4.4.4
android 4.4
android 4.2-4.3
android 4.1
ios_saf 12.2-12.3
ios_saf 12.0-12.1
ios_saf 11.3-11.4
ios_saf 11.0-11.2
ios_saf 10.3
ios_saf 10.0-10.2
ios_saf 9.3
ios_saf 9.0-9.2
ios_saf 8.1-8.4

8. options

  • target

用来配置目标运行环境。

string | Array | { [string]: string }, defaults to {}.

target如果是一个string,则应该写成browserlist的query形式(什么是browserlist query),如

{
  "target":"iOS > 8, Android > 4"
}

target如果是一个对象,则可以把browserlist的browsers直接配置到对象上(什么是browserlist browser),如:

{
  "target": {
    "iOs": "8",
    "Android": "4"
  }
}

这种形式配置的browser代表的是它的最低版本。

除了以上两种形式,targets还有其它的一些配置方式,但是我觉得不常用,所以不打算仔细介绍了。详情请查看

  • spec

这个在上一篇讲过的,这个option会传递到preset内部的plugin,如果plugin支持这个option, spec就会传给它。

  • loose

这个在上一篇讲过的,这个option会传递到preset内部的plugin,如果plugin支持这个option, loose就会传给它。

  • modules
“amd” | “umd” | “systemjs” | “commonjs” | “cjs” | “auto” | false, defaults toauto”.

这个用于配置是否启用将ES6的模块转换其它规范的模块。在vue项目里,这个option被显示的配置为了false。

  • debug

这个用于开启转码的调试。我觉得很有用,能够看到很多有用的提示,尤其是polyfill相关的处理结果。

  • corejs
2, 3 or { version: 2 | 3, proposals: boolean }, defaults to 2.

这个option,用来指定preset-env进行polyfill时,要使用的corejs版本。 core-js是第三方写的不支持的浏览器环境,也能支持最新ES特性的库,该作者称其为standard library。 core-js现在有2个版本在被人使用:v2和v3。 所以preset-env的corejs这个option,可以支持配置2或者3。 但是从未来的角度来说,我认为不应该再关注core-js v2,它始终会被v3代替,慢慢地大家都会升级到v3上面来。 所以本篇在学习preset-env对core-js的polyfill行为,不再关注core-js v2了。

如果仅考虑core-js v3的话,preset-env的corejs 这个option,有两种配置:

corejs: 3
corejs: {version: 3, proposals: boolean}

默认情况下,对corejs的polyfill,只会注入那些stabled的ES特性,还处于proposal状态的polyfill则不会注入。 如果需要注入proposals的polyfill,则可以考虑将corejs配置为:corejs: {version: 3, proposals: true}

corejs: {version: 3, proposals: true}往往搭配下面的useBuiltIns: 'usage'一起使用。

  • useBuiltIns
“usage” | “entry” | false, defaults to false.

由于@babel/polyfill在babel7.4开始,也不支持使用了。 所以现在要用preset-env,必须是得单独安装core-js v3

useBuiltIns,主要有两个value: entryusage。 这两个值,不管是哪一个,都会把core-js的modules注入到转换后的代码里面,充当polyfill。 什么是core-js的module? 这个需要专门去学习core-js的文档,后面的博客学习core-js时,我也会专门记录。

  • entry

先来看entry的作用机制。 假如.babelrc.js如下配置:

const presets = [ 
    [
    "@babel/preset-env",
            {
                "targets": {
                    ios: 8,
                    android: 4
                },
                useBuiltIns: "entry",
                corejs: 3,
                debug: true//方便调试
            }
        ]
];
const plugins = [
];

module.exports = { presets, plugins };

准备如下一段代码:

Promise.resolve().finally();

let obj = {...{}};

globalThis.obj = obj;

运行npx babel src --out-dir dist对它进行转码。 因为有debug,控制台会打印出:

Using polyfills with `entry` option:

[D:\babel\src\main.js] Import of core-js was not found.
Successfully compiled 1 file with Babel.

这就提示我们需要在代码里,加入对core-js的import调用,以便注入core-js作为polyfill。

将代码修改如下:

import "core-js";

Promise.resolve().finally();

let obj = {...{}};

globalThis.obj = obj;

运行npx babel src --out-dir dist对它进行转码。 此时不会再有Import of core-js was not found提示。 最后转码结果如下:

"use strict";

require("core-js/modules/es.symbol");

require("core-js/modules/es.symbol.description");

// 中间省略了几百行require

require("core-js/modules/esnext.global-this");

require("core-js/modules/web.immediate");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

Promise.resolve().finally();

var obj = _objectSpread({}, {});

globalThis.obj = obj;

这个结果有做省略,累计有500多行。 在useBuiltIns: 'entry'模式下,代码中对core-js的import:import "core-js",会根据targets的配置,替换为core-js最底层的modules引用,如require("core-js/modules/es.symbol")。 core-js有很多个module,虽然源代码只有一行import "core-js",转换后的代码有500多行require("core-js/modules/...")

如果你想在项目中继续使用以前babel/polyfill的方式,只需要引入下面的代码在入口文件即可:

import "core-js";
import "regenerator-runtime/runtime";

这个方式有问题吗?有的,就是使用的polyfill太多了,有的可能整个项目的逻辑都不需要它,这个方式最后生成代码比较大,对前端性能肯定是有影响的;唯一的优点就是省心,不要去考虑哪个文件需要引入哪些core-js的modules来作为polyfill。

如何改进呢?

在自身对当前文件所用到的ES特性非常熟悉的情况下,可以选择手工地引入core-js的modules,来避免整体的引用。 将代码修改为:

import "core-js/es/promise";
import "core-js/es/array";

Promise.resolve().finally();

let obj = {...{}};

globalThis.obj = obj;

运行npx babel src --out-dir dist对它进行转码。 最终结果为:

"use strict";

require("core-js/modules/es.array.concat");

require("core-js/modules/es.array.copy-within");

// 省去几十行

require("core-js/modules/es.array.unscopables.flat");

require("core-js/modules/es.array.unscopables.flat-map");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("core-js/modules/es.promise.finally");

require("core-js/modules/es.string.iterator");

require("core-js/modules/web.dom-collections.iterator");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

Promise.resolve().finally();

var obj = _objectSpread({}, {});

globalThis.obj = obj;

这个结果虽然还是有很多,但是比直接import core-js要减少好多了。 它的机制也是类似的,就是把对core-js的import,转换为core-js的最小单位:modules。:

import "core-js/es/promise";
import "core-js/es/array";

上面两个引用代表的是core-js的两个命名空间:promise和array。 它们分别可能包含多个modules, 所以转码后,还是有好几十个core-js module的require语句。

虽然单独引用core-js的某一部分,能够减少最终的转码大小,但是要求开发人员对core-js和ES特性特别熟悉,否则你怎么知道当前文件需不需要polyfill,以及要哪些呢? 所以这个做法,真正使用的时候,难度较大

useBuiltIns: "entry"对core-js的import替换,是根据targets配置的环境进行判断的。假如把配置文件调整一下:(ios: 12是非常新的环境了)

const presets = [ 
    [
    "@babel/preset-env",
            {
                "targets": {
                    ios: 12
                },
                useBuiltIns: "entry",
                corejs: 3,
                debug: true
            }
        ]
];
const plugins = [
];

module.exports = { presets, plugins };

然后重新对以下代码:

import "core-js/es/promise";
import "core-js/es/array";

Promise.resolve().finally();

let obj = {...{}};

globalThis.obj = obj;

运行babel,结果为:

"use strict";

require("core-js/modules/es.array.reverse");

require("core-js/modules/es.array.unscopables.flat");

require("core-js/modules/es.array.unscopables.flat-map");

require("core-js/modules/web.dom-collections.iterator");

Promise.resolve().finally();
let obj = { ...{}
};
globalThis.obj = obj;

同样的代码,由于targets设置为了较新版本的环境,所以最终转码的结果减少很多。

  • usage

usage比起entry,最大的好处就是,它会根据每个文件里面,用到了哪些es的新特性,然后根据我们设置的targets判断,是否需要polyfill,如果targets的最低环境不支持某个es特性,则这个es特性的core-js的对应module会被注入。

配置文件修改为:

const presets = [ 
    [
    "@babel/preset-env",
            {
                "targets": {
                    ios: 8,
                    android: 4.1
                },
                useBuiltIns: "usage",
                corejs: 3,
                debug: true
            }
        ]
];
const plugins = [
];

module.exports = { presets, plugins };

代码修改为:

Promise.resolve().finally();

let obj = {...{}};

globalThis.obj = obj;

运行npx babel src --out-dir dist转码,最终结果为:

"use strict";

require("core-js/modules/es.symbol");

require("core-js/modules/es.array.filter");

require("core-js/modules/es.array.for-each");

require("core-js/modules/es.object.define-properties");

require("core-js/modules/es.object.define-property");

require("core-js/modules/es.object.get-own-property-descriptor");

require("core-js/modules/es.object.get-own-property-descriptors");

require("core-js/modules/es.object.keys");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("core-js/modules/es.promise.finally");

require("core-js/modules/web.dom-collections.for-each");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

Promise.resolve().finally();

var obj = _objectSpread({}, {});

globalThis.obj = obj;

最终这个文件转码结果,就只有这么多,比使用entry简化了不少,而且很智能,不需要自己去操心需要哪些polyfill。 这也是为啥usage被推荐在项目中使用的原因

观察上面的源码和转码后的代码,其实有个小地方没有被转换,就是:globalThis,这是es的一个提案,当前是stage-3阶段,这个没有被注入polyfill。这是因为preset-env默认不会对proposals进行polyfill,所以如果需要对proposals进行polyfill,可以把配置文件做一点点修改:

const presets = [ 
    [
    "@babel/preset-env",
            {
                "targets": {
                    ios: 8,
                    android: 4.1
                },
                useBuiltIns: "usage",
                corejs: {version: 3, proposals: true},
                debug: true
            }
        ]
];
const plugins = [
];

module.exports = { presets, plugins };

然后重新运行npx babel src --out-dir dist转码,最终结果为:

"use strict";

require("core-js/modules/es.symbol");

require("core-js/modules/es.array.filter");

require("core-js/modules/es.array.for-each");

require("core-js/modules/es.object.define-properties");

require("core-js/modules/es.object.define-property");

require("core-js/modules/es.object.get-own-property-descriptor");

require("core-js/modules/es.object.get-own-property-descriptors");

require("core-js/modules/es.object.keys");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("core-js/modules/es.promise.finally");

require("core-js/modules/esnext.global-this");

require("core-js/modules/web.dom-collections.for-each");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

Promise.resolve().finally();

var obj = _objectSpread({}, {});

globalThis.obj = obj;

多了一条require("core-js/modules/esnext.global-this");,这就是globalThis proposal的polyfill。

  • 其它options

其它的options,建议有需要在学习。

对presets的介绍就是上面这些内容了,其实从babel官方文档中还能够再总结出一些东西的,但是这样就太费时间了,好多信息散落在blog github以及各个preset的单独介绍页面中,不好整理。 本篇的内容包含了babel@7.x的核心要点与plugins和preset最重要的知识点,对于加强babel基础配置的认识,已经够了。

文章摘自:blog.liuyunzhuge.com/tags/babel/… 仅用于自己学习使用;