@babel/preset-env

在Babel6时代,这个预设名字是 babel-preset-env,在Babel7之后,改成@babel/preset-env。本节单独讲解@babel/preset-env,不涉及transform-runtime的内容,二者结合使用的内容会在讲解了transform-runtime之后进行。

@babel/preset-env是整个Babel大家族最重要的一个preset。不夸张地说,所有配置项仅需要它自己就可以完成现代JS工程所需要的所有转码要求。

在使用它之前,需要先安装

  npm install --save-dev @babel/preset-env

@babel/preset-env是Babel6时代babel-preset-latest的增强版。该预设除了包含所有稳定的转码插件,还可以根据我们设定的目标环境进行针对性转码。

Babel快速入门一节,我们简单使用过@babel/preset-env的语法转换功能。除了进行语法转换,该预设还可以通过设置参数项进行针对性语法转换以及polyfill的部分引入。

@babel/preset-env的参数项,数量有10多个,但大部分我们要么用不到,要么已经或将要弃用。这里建议大家掌握重点的几个参数项,有的放矢。重点要学习的参数项有targets、useBuiltIns、modules和corejs这四个,能掌握这几个参数项的真正含义,就已经超过绝大部分开发者了。

对于preset,当我们不需要对其设置参数的时候,其写法是只需要把该preset的名字放入presets对于的数组里即可,例如

  module.exports = {
    presets: ["@babel/env"],
    plugins: []
  }

注意,@babel/env是@babel/preset-env的简写。

如果需要对某个preset设置参数,该preset就不能以字符串形式直接放在presets的数组项了。而是应该再包裹一层数组,数组第一项是该preset字符串,数组第二项是该preset的参数对象。如果该preset没有参数需要设置,则数组第二项可以是空对象或者不写第二项。以下几种写法是等价的:

  module.exports = {
    presets: ["@babel/env"],
    plugins: []
  }
  module.exports = {
    presets: [["@babel/env", {}]],
    plugins: []
  }
  module.exports = {
    presets: [["@babel/env"]],
    plugins: []
  }

如果你使用过vue或react的官方脚手架cli工具,你一定会在其package.json里看到browserslist项,下面该项配置的一个例子:

  "browserslist": [
    "> 1%",
    "not ie <= 8"
  ]

上面的配置含义是,目标环境是市场份额大于1%的浏览器并且不考虑IE8及以下的IE浏览器。Browserslist叫做目标环境配置表,除了写在package.json里,也可以单独写在工程目录下.browserslistrc文件里。我们用browserslist来指定代码最终要运行在哪些浏览器或node.js环境。Autoprefixer、postcss等就可以根据我们的browserslist,来自动判断是否要增加CSS前缀(例如'-webkit-')。我们的Babel也可以使用browserslist,如果你使用了@babel/preset-env这个预设,此时Babel就会读取browserslist的配置。

如果我们的@babel/preset-env不设置任何参数,Babel就会完全根据browserslist的配置来做语法转换。如果没有browserslist,那么Babel就会把所有ES6的语法转换成ES5版本。

在本教程最初的例子里,我们没有browserslist,并且@babel/preset-env的参数项是空的,ES6箭头函数语法被转换成了ES5的函数定义语法。

转换前

  var fn = (num) => num + 2;

转换后

  "use strict";
  var fn = function fn(num) {
    return num + 2;
  };

如果我们在browserslist里指定目标环境是Chrome60,我们再来看一下转换结果,github配套代码babel09。

  "browserslist": [
    "chrome 60"
  ]
@babel/preset-env Babel教程

转换后

  "use strict";

  var fn = num => num + 2;

我们发现转换后的代码仍然是箭头函数,因为Chrome60已经实现了箭头函数语法,所以不会转换成ES5的函数定义语法。

现在我们把Chrome60改成Chrome38,再看看转换后的结果,github配套代码babel10。

  "browserslist": [
    "chrome 38"
  ]

转换后

  "use strict";

  var fn = function fn(num) {
    return num + 2;
  };

我们发现转换后的代码是ES5的函数定义语法,因为Chrome38不支持箭头函数语法。

注意,Babel使用browserslist的配置功能依赖于@babel/preset-env,如果Babel没有配置任何预设或插件,那么Babel对转换的代码会不做任何处理,原封不动生成和转换前一样代码。

既然@babel/preset-env可以通过browserslist针对目标环境不支持的语法进行语法转换,那么是否也可以对目标环境不支持的特性API进行部分引用呢?这样我们就不用把完整的polyfill全部引入到最终的文件里,可以大大减少体积。

答案是可以的,但需要对@babel/preset-env的参数项进行设置才可以,这个我们接下来讲。

参数项

targets

该参数项可以取值为字符串、字符串数组或对象,不设置的时候取默认值空对象{}。

该参数项的写法与browserslist是一样的,下面是一个例子

  module.exports = {
    presets: [["@babel/env", {
      targets: {
        "chrome": "58",
        "ie": "11"
      }
    }]],
    plugins: []
  }

如果我们对@babel/preset-env的targets参数项进行了设置,那么就不使用browserslist的配置,而是使用targets的配置。如不设置targets,那么就使用browserslist的配置。如果targets不配置,browserslist也没有配置,那么@babel/preset-env就对所有ES6语法转换成ES5的。

正常情况下,我们推荐使用browserslist的配置而很少单独配置@babel/preset-env的targets。

useBuiltIns

useBuiltIns项取值可以是"usage" 、 "entry" 或 false。如果该项不进行设置,则取默认值false。

useBuiltIns这个参数项主要和polyfill的行为有关。在我们没有配置该参数项或是取值为false的时候,polyfill就是我们上节课讲的那样,会全部引入到最终的代码里。

useBuiltIns取值为"entry"或"usage"的时候,会根据配置的目标环境找出需要的polyfill进行部分引入。让我们看看这两个参数值使用上的不同。

useBuiltIns:"entry"

配套的代码github是babel11例子。

我们在入口文件用import语法引入polyfill(也可以在webpack的entry入口项)。此时的Babel配置文件如下:

  module.exports = {
    presets: [["@babel/env", {
      useBuiltIns: "entry"
    }]],
    plugins: []
  }

需要安装的npm包如下:

  npm install --save-dev @babel/cli @babel/core  @babel/preset-env
  npm install --save @babel/polyfill

我们指定目标环境是火狐58,package.json里的browserslist设置如下:

    "browserslist": [
      "firefox 58"
    ]

转换前端的代码如下:

  import '@babel/polyfill';

  var promise = Promise.resolve('ok');
  console.log(promise);

使用npx babel a.js -o b.js命令进行转码。

转码后:

  "use strict";

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

  require("core-js/modules/es7.string.trim-left");

  require("core-js/modules/es7.string.trim-right");

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

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

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

  var promise = Promise.resolve('ok');
  console.log(promise);

我们可以看到Babel针对火狐58不支持的API特性进行引用,一共引入了6个core-js的API补齐模块。同时也可以看到,因为火狐58已经支持Promise特性,所以没有引入promise相关的API补齐模块。你可以试着修改browserslist里火狐的版本,修改成版本26后,会引入API模块大大增多,有几十个。

useBuiltIns:"usage"

配套的代码github是babel12例子。

"usage"在Babel7.4之前一直是试验性的,7.4之后的版本稳定。

这种方式不需要我们在入口文件(以及webpack的entry入口项)引入polyfill,Babel发现useBuiltIns的值是"usage"后,会自动进行polyfill的引入。

我们的Babel配置文件如下:

  module.exports = {
    presets: [["@babel/env", {
      useBuiltIns: "usage"
    }]],
    plugins: []
  }

需要安装的npm包如下:

  npm install --save-dev @babel/cli @babel/core  @babel/preset-env
  npm install --save @babel/polyfill

我们指定目标环境是火狐27,package.json里的browserslist设置如下:

    "browserslist": [
      "firefox 27"
    ]

转换前端的代码如下:

  var promise = Promise.resolve('ok');
  console.log(promise);

使用npx babel a.js -o b.js命令进行转码。

下面是转换后的代码:

  "use strict";

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

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

  var promise = Promise.resolve('ok');
  console.log(promise);

观察转换的代码,我们发现引入的core-js的API补齐模块非常少,只有2个。为什么呢?

因为我们的代码里只使用了Promise这一火狐27不支持特性API,使用useBuiltIns:"usage"后,Babel除了会考虑目标环境缺失的API模块,同时考虑我们项目代码里使用到的ES6特性。只有我们使用到的ES6特性API在目标环境缺失的时候,Babel才会引入core-js的API补齐模块。

这个时候我们就看出了'entry'与'usage'这两个参数值的区别:'entry'这种方式不会根据我们实际用到的API进行针对性引入polyfill,而'usage'可以做到。另外,在使用的时候,'entry'需要我们在项目入口处手动引入polyfill,而'usage'不需要。

需要注意的是,使用'entry'这种方式的时候,只能import polyfill一次,一般都是在入口文件。如果进行多次import,会发生错误。

corejs

该参数项的取值可以是2或3,没有设置的时候取默认值为2(还有一种对象proposals取值方法,我们实际用不到,忽略掉即可)

这个参数项只有useBuiltIns设置为'usage'或'entry'时,才会生效。

取默认值或2的时候,Babel转码的时候使用的是core-js@2版本(即core-js2.x.x)。因为某些新API只有core-js@3里才有,例如数组的flat方法,我们需要使用core-js@3的API模块进行补齐,这个时候我们就把该项设置为3。

需要注意的是,corejs取值为2的时候,需要安装并引入core-js@2版本,或者直接安装并引入polyfill也可以。如果corejs取值为3,必须安装并引入core-js@3版本才可以,否则Babel会转换失败并提示:

  `@babel/polyfill` is deprecated. Please, use required parts of `core-js` and `regenerator-runtime/runtime` separately

modules

这个参数项的取值可以是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不设置的时候,取默认值"auto"。

该项用来设置是否把ES6的模块化语法改成其它模块化语法。

我们常见的模块化语法有两种:(1)ES6的模块法语法用的是import与export;(2)commonjs模块化语法是require与module.exports。

在该参数项值是'auto'或不设置的时候,会发现我们转码前的代码里import都被转码成require了。

如果我们将参数项改成false,那么就不会对ES6模块化进行更改,还是使用import引入模块。

使用ES6模块化语法有什么好处呢。在使用Webpack一类的打包工具,可以进行静态分析,从而可以做tree shaking 等优化措施。

笔记与思考

  1. 如何我们对@babel/preset-env的targets参数项进行了设置 =>
    如果我们对@babel/preset-env的targets参数项进行了设置

  2. 目标环境是市场份额大于1%的浏览器并且不考虑IE8及一下的IE浏览器
    一下-->以下

  3. 文章最后说,modules字段默认是'auto',会把所有的ESModule(import语法)转为commonjs(require语法),可是webpack我记得是可以做Tree shaking的啊,而且Tree shaking的前提就是必须使用ESModule,babel都把ESModule给转没了,最后竟然还能做Tree shaing,这块感觉想不通,我感觉我的逻辑无法自洽了,可否请大佬解答一下呢?

发表评论

邮箱地址不会被公开。 必填项已用*标注