Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

babel源码解析之(@babel/preset-env)_vv_小虫的博客-CSDN博客_@babel #74

Open
eightHundreds opened this issue May 19, 2022 · 0 comments

Comments

@eightHundreds
Copy link
Owner

前言

还记得之前写过一篇文章:babel 源码解析一, 里面把 babel 的整个流程跑了一遍,最后还自定义了一个插件用来转换 “箭头函数”,通过前面的源码解析我们知道,preset 其实就是一些插件的集合,这一节我们来介绍一个 babel 中比较重要的 preset“@babel/preset-env”,可能有些小伙伴已经用过了,没用过的话至少也应该见过,废话不多说,我们直接盘它。

开始

我们从项目创建开始,首先我们创建一个叫 babel-demo 的项目用来测试 babel,然后执行 npm 初始化

npm init 

然后我们创建一个 src 目录用来存放 demo 源码来做 babel 测试,我们直接在 src 中创建一个 demo1.js,然后随便写一些代码,

src/demo1.js:

const fn = () => {};

new Promise(() => {});

class Test {}

const c = [1, 2, 3].includes(1);
var a = 10;

代码很简单,我就不多解释了,然后我们安装 @babel/cli、@babel/core、@babel/preset-env,

在项目根目录执行 npm 或者 yarn:

yarn add -D @babel/cli && yarn add -D @babel/core && yarn add -D @babel/preset-env

npm install -D @babel/cli && npm install -D @babel/core && npm install -D @babel/preset-env

然后我们直接在根目录运行一下 babel 命令测试一下:

npx babel ./src/demo1.js -o ./lib/demo1.js

可以看到,我们执行了 babel 命令,然后打开我们编译过后的文件,

lib/demo1.js:

const fn = () => {};

new Promise(() => {});

class Test {}

const c = [1, 2, 3].includes(1);
var a = 10;

小伙伴可以发现,没有任何变化,是的!因为我们还没有任何 babel 的配置信息。

配置

我们在根目录创建一个 babel.config.js 文件 (当然,babel 的配置不止有 babel.config.js 一种,还有 babel.config.cjs、.babelrc 等等,大家可以看 babel 的官网),作为 babel 的配置文件,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env"
        ]
    ]
};

可以看到,我们在 presets 字段中配置了一个 “preset-env”,然后我们在根目录运行一下 babel 命令:

npx babel ./src/demo1.js -o ./lib/demo1.js

lib/demo1.js:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var fn = function fn() {};

new Promise(function () {});

var Test = function Test() {
  _classCallCheck(this, Test);
};

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,这一次运行过后,es6 的语法都变成了 es5,那么 preset-env 是怎样把我们的 es6 代码变成 es5 的呢? 我们下面结合 demo 一步一步分析一下。

我们按照babel-preset-env官网的顺序来,

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

翻译一下大概就是:

有了@babel/preset-env后,你无需关心你所运行的环境需要的语法,因为会自动的根据当前环境对 js 语法做转换。

我们在没有 @babel/preset-env 之前如果需要转换 es 语法的话,是需要我们自己去配置一些插件或者内置的 stage,所以之前 babel 内置了 state0、state1、state2、state3 供你选择,还是处于手动配置状态,babel7.0 后添加了 preset-env,它会根据你所配置的浏览器的列表,自动的去加载当前浏览器所需要的插件,然后对 es 语法做转换。

targets

描述你项目所支持的环境,因为内置了browserslist,最后会把targets传递给它,所以可以参考browserslist的配置

比如:

字符串:

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

或者对象:

{
  "targets": {
    "chrome": "58",
    "ie": "11"
  }
}

也可以通过配置文件去配置,我们 demo 中选用配置文件去配置,因为browserslist在大部分项目中是会被很多其它框架插件所依赖的,比如:“postcss”、"stylelint" 等等,

我们直接在根目录创建一个.browserslistrc文件,

.browserslistrc:

> 0.25%, not dead

可以看到,我们配置一个 “> 0.25%, not dead” 字符串(browserslist 默认配置),那么这代表了哪些浏览器呢?

browserslist 提供了一个命令供我们查看,我们直接在根目录运行这个命令:

  babel-demo git:(v0.0.1)  npx browserslist "> 0.25%, not dead"
and_chr 81
and_uc 12.12
chrome 83
chrome 81
chrome 80
chrome 79
chrome 49
edge 18
firefox 76
firefox 75
ie 11
ios_saf 13.4-13.5
ios_saf 13.3
ios_saf 13.0-13.1
ios_saf 12.2-12.4
op_mini all
opera 68
safari 13.1
safari 13
safari 12.1
samsung 11.1-11.2
  babel-demo git:(v0.0.1)  

可以看到,browserslist 命令列出了我们字符串所代表的浏览器集合,大家可以根据自己项目需求做配置即可。

我们再次运行 babel 命令:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo1.js -o ./lib/demo1.js

然后看一下编译过后的文件:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var fn = function fn() {};

new Promise(function () {});

var Test = function Test() {
  _classCallCheck(this, Test);
};

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,跟之前的配置没有什么区别,这又是为什么呢? 因为我们用的是默认的 browserslist 配置,我们试着改一下 browserslist 的配置,这次我们把浏览器配置配高一点,

.browserslistrc:

chrome 79

我们再次运行 babel 命令:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo1.js -o ./lib/demo1.js

然后看一下结果

lib/demo1.js:

"use strict";

const fn = () => {};

new Promise(() => {});

class Test {}

const c = [1, 2, 3].includes(1);
var a = 10;

可以看到,这次 babel 没有对代码做任何语法转换,因为我们的 “chrome 79” 浏览器是支持这些语法的,preset-env 认为是不需要转换。

我们还是把浏览器配置列表文件换成默认配置,看一下默认情况下 preset-env 对我们代码做的转换:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var fn = function fn() {};

new Promise(function () {});

var Test = function Test() {
  _classCallCheck(this, Test);
};

var c = [1, 2, 3].includes(1);
var a = 10;

我们看一下babel-preset-env的源码 (可以自己去 clone 一份),demo 用的是“7.7.7” 版本

packages/babel-preset-env/src/index.js:

...
export default declare((api, opts) => {
  ...
  const pluginNames = filterItems(
    shippedProposals ? pluginList : pluginListWithoutProposals,
    include.plugins,
    exclude.plugins,
    transformTargets,
    modulesPluginNames,
    getOptionSpecificExcludesFor({ loose }),
    pluginSyntaxMap,
  );
  ...

  return { plugins };
});

我们先撇开其它的配置信息(后面我们会讲到),我们之前就说过 preset 其实就是一些 plugin 的集合,那么默认 preset-env 获取哪些插件呢?我们看到这么一行代码:

shippedProposals ? pluginList : pluginListWithoutProposals,

shippedProposals 配置稍后再讲,可以看到我们会获取一个 pluginList 对象,pluginList 是一个模块导出来的,

import pluginList from "../data/plugins.json";

packages/babel-preset-env/data/plugins.json:

{
  
  "transform-classes": {
    "chrome": "46",
    "edge": "13",
    "firefox": "45",
    "safari": "10",
    "node": "5",
    "ios": "10",
    "samsung": "5",
    "opera": "33",
    "electron": "0.36"
  },
  
}

代码有点多,我们直接看一个就可以了,比如 “transform-classes”,这个就是给我们的 class 加上_classCallCheck 方法的插件:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

preset-env 会根据我们配置的 browserlist 去 plugins.json 找看有没有浏览器是需要这个插件的,需要就加入,

我们再重新梳理一遍逻辑

首先看一下我们的浏览器列表:

  babel-demo git:(v0.0.1)  npx browserslist "> 0.25%, not dead"
and_chr 81
and_uc 12.12
chrome 83
chrome 81
chrome 80
chrome 79
chrome 49
edge 18
firefox 76
firefox 75
ie 11
ios_saf 13.4-13.5
ios_saf 13.3
ios_saf 13.0-13.1
ios_saf 12.2-12.4
op_mini all
opera 68
safari 13.1
safari 13
safari 12.1
samsung 11.1-11.2

然后 preset-env 会通过 filterItems 筛选出我们需要的插件,

packages/babel-preset-env/src/index.js:

...
export default declare((api, opts) => {
  ...
  const pluginNames = filterItems(
    shippedProposals ? pluginList : pluginListWithoutProposals,
    include.plugins,
    exclude.plugins,
    transformTargets,
    modulesPluginNames,
    getOptionSpecificExcludesFor({ loose }),
    pluginSyntaxMap,
  );
  ...

  return { plugins };
});

packages/babel-preset-env/src/filter-items.js:

export default function(
  list: { [feature: string]: Targets },
  includes: Set<string>,
  excludes: Set<string>,
  targets: Targets,
  defaultIncludes: Array<string> | null,
  defaultExcludes?: Array<string> | null,
  pluginSyntaxMap?: Map<string, string | null>,
) {
  const result = new Set<string>();

  for (const item in list) {
    if (
      !excludes.has(item) &&
      (isPluginRequired(targets, list[item]) || includes.has(item))
    ) {
      result.add(item);
    }
  }
...

  return result;
}

然后 filterItems 方法会调用一个叫 isPluginRequired 的方法,

packages/babel-preset-env/src/filter-items.js:

import semver from "semver";
import { semverify, isUnreleasedVersion } from "./utils";

import type { Targets } from "./types";

export function isPluginRequired(
  supportedEnvironments: Targets,
  plugin: Targets,
) {
  const targetEnvironments = Object.keys(supportedEnvironments);

  if (targetEnvironments.length === 0) {
    return true;
  }

  const isRequiredForEnvironments = targetEnvironments.filter(environment => {
    
    if (!plugin[environment]) {
      return true;
    }

    const lowestImplementedVersion = plugin[environment];
    const lowestTargetedVersion = supportedEnvironments[environment];

    
    if (isUnreleasedVersion(lowestTargetedVersion, environment)) {
      return false;
    }

    
    if (isUnreleasedVersion(lowestImplementedVersion, environment)) {
      return true;
    }

    if (!semver.valid(lowestTargetedVersion.toString())) {
      throw new Error(
        `Invalid version passed for target "${environment}": "${lowestTargetedVersion}". ` +
          "Versions must be in semver format (major.minor.patch)",
      );
    }
		
    return semver.gt(
      semverify(lowestImplementedVersion),
      lowestTargetedVersion.toString(),
    );
  });

  return isRequiredForEnvironments.length > 0;
}

isPluginRequired 方法会去获取 plugins.json 中插件所支持的环境,然后跟当前配置的浏览器环境做比较,如果当前浏览器的环境小于插件所需要的浏览器环境,那么就返回 true,否则返回 false。

在我们 demo 中,是需要 "transform-classes" 插件的,所以 "preset-env" 的 "plugins" 里面会有一个 "transform-classes" 插件,那么 "transform-classes" 插件的源码在哪呢?

packages/babel-plugin-transform-classes/src/index.js:

...
import transformClass from "./transformClass";
...
export default declare((api, options) => {
  ...
  return {
    name: "transform-classes",

    visitor: {
      ExportDefaultDeclaration(path: NodePath) {
        if (!path.get("declaration").isClassDeclaration()) return;
        splitExportDeclaration(path);
      },

      ClassDeclaration(path: NodePath) {
        const { node } = path;

        const ref = node.id || path.scope.generateUidIdentifier("class");

        path.replaceWith(
          t.variableDeclaration("let", [
            t.variableDeclarator(ref, t.toExpression(node)),
          ]),
        );
      },

      ClassExpression(path: NodePath, state: any) {
       ...
        path.replaceWith(
          transformClass(path, state.file, builtinClasses, loose),
        );
			...
      },
    },
  };
});
.....

可以看到,当读到我们的 ClassExpression 节点的时候就开始执行 transformClass 方法,

我们打开我们的 demo1.js,

src/demo1.js:

const fn = () => {};

new Promise(() => {});

class Test {}

const c = [1, 2, 3].includes(1);
var a = 10;

当读到 "class Test {}" 这一句的时候,就会执行 "babel-plugin-transform-classes" 的 "transformClass" 方法,

packages/babel-plugin-transform-classes/src/transformClass.js:

 import type { NodePath } from "@babel/traverse";
import nameFunction from "@babel/helper-function-name";
import ReplaceSupers, {
  environmentVisitor,
} from "@babel/helper-replace-supers";
import optimiseCall from "@babel/helper-optimise-call-expression";
import * as defineMap from "@babel/helper-define-map";
import { traverse, template, types as t } from "@babel/core";

type ReadonlySet<T> = Set<T> | { has(val: T): boolean };

function buildConstructor(classRef, constructorBody, node) {
  const func = t.functionDeclaration(
    t.cloneNode(classRef),
    [],
    constructorBody,
  );
  t.inherits(func, node);
  return func;
}

export default function transformClass(
  path: NodePath,
  file: any,
  builtinClasses: ReadonlySet<string>,
  isLoose: boolean,
) {
  ...

  return classTransformer(path, file, builtinClasses, isLoose);
}

代码太多了,我们直接看重点,我们会发现最后调用了一个叫 "classTransformer" 的方法

function classTransformer(
    path: NodePath,
    file,
    builtinClasses: ReadonlySet<string>,
    isLoose: boolean,
  ) {
   ...
    
    if (!classState.isLoose) {
      constructorBody.body.unshift(
        t.expressionStatement(
          t.callExpression(classState.file.addHelper("classCallCheck"), [
            t.thisExpression(),
            t.cloneNode(classState.classRef),
          ]),
        ),
      );
    }
...
    return t.callExpression(container, closureArgs);
  }

我们可以看到,classTransformer 方法中添加了一个 helper:

t.callExpression(classState.file.addHelper("classCallCheck"),

那么我们这个 helper 到底是什么呢?

packages/babel-helpers/src/helpers.js:

...
helpers.classCallCheck = helper("7.0.0-beta.0")`
  export default function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
      throw new TypeError("Cannot call a class as a function");
    }
  }
`;
...

可以看到,在 babel-helpers 中我们找到了这么一段代码,是不是很熟悉呢?

lib/demo1.js:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var fn = function fn() {};

new Promise(function () {});

var Test = function Test() {
  _classCallCheck(this, Test);
};

var c = [1, 2, 3].includes(1);
var a = 10;

ok! 通过上面的分析我们可以知道,当我们设置了 targets(也可以是. browserslistrc 文件)我们运行浏览器的列表后,preset-env 会根据 targets 获取对应的插件,比如我们 demo 中需要的 “transform-arrow-functions”、“transform-classes” 等等。

targets.esmodules

当我们设置了targets.esmodules属性后,preset-env 就会忽略targets.browsers(所支持的浏览器)属性跟. browserslistrc 文件,然后直接把浏览器的支持改为支持 “es6.module” 语法的浏览器,

比如我们修改一下我们的配置文件

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "esmodules": true
                }
            }
        ]
    ]
};

然后运行 babel 看效果:

➜  babel-demo git:(v0.0.1) ✗ npx babel ./src/demo1.js -o ./lib/demo1.js

lib/demo1.js:

"use strict";

var fn = () => {};

new Promise(() => {});

class Test {}

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,对我们的源代码没做改变,因为支持 “es6.module” 语法的浏览器有这些:

packages/babel-preset-env/data/built-in-modules.json:

{
  "es6.module": {
    "edge": "16",
    "firefox": "60",
    "chrome": "61",
    "safari": "10.1",
    "opera": "48",
    "ios_saf": "10.3",
    "and_chr": "71",
    "and_ff": "64"
  }
}

而这些浏览器是可以支持我们的 demo1.js 的代码的,所以 preset-env 认为不需要做转换。

targets.node

string | "current" | true.

声明当前运行的 node 的版本号,可以为:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "esmodules": true,
                  	"node": "current",
                  	"node": true,
                  	"node": "13.12.0"
                }
            }
        ]
    ]
};

代码在 packages/babel-preset-env/src/targets-parser.js:

 node: (target, value) => {
    const parsed =
      value === true || value === "current"
        ? process.versions.node
        : semverifyTarget(target, value); 
    return [target, parsed];
  },

targets.safari

string | "tp".

指定 safari 浏览器的版本,可以为 “tp” 或者自定义版本号。

targets.browsers

string | Array<string>.

跟直接指定 targets 为字符串或者browserslist配置文件的方式一样。

spec

boolean, 默认 为 false.

主要是给其它插件用的参数,在 preset-env 的源码中可以看到

packages/babel-preset-env/src/index.js:

  const plugins = Array.from(pluginNames)
    .map(pluginName => [
      getPlugin(pluginName),
      { spec, loose, useBuiltIns: pluginUseBuiltIns },
    ])
    .concat(polyfillPlugins);

可以看到,直接给了插件,其中还有‘loose’、‘useBuiltIns’字段。

loose

boolean, 默认: false.

也是给插件用的参数,跟spec一样,比如我们修改一下我们的 demo1.js 的代码,

src/demo1.js:

const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

我们给 Test 类加了一个 say 方法,我们先看一下不加loose参数的结果:

babel.config.js

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                loose: false
            }
        ]
    ]
};

lib/demo1.js:

"use strict";

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

var fn = function fn() {};

new Promise(function () {});

var Test = function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,当 loose:false(默认)的时候:

packages/babel-plugin-transform-classes 插件直接利用 Object.defineProperty 方法在 Test 的原型对象上面添加属性的方式实现,

我们现在修改一下配置文件,把 loose 改为 true

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                loose: true
            }
        ]
    ]
};

然后运行 babel 看结果

lib/demo1.js:

"use strict";

var fn = function fn() {};

new Promise(function () {});

var Test = function () {
  function Test() {}

  var _proto = Test.prototype;

  _proto.say = function say() {};

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,当 loose:true 的时候:

packages/babel-plugin-transform-classes 插件直接在 Test 的原型对象上面添加了一个 say 属性。

modules

"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false, 默认为 "auto".

是否允许把 ES6 module 语法 转换成其它类型

如果是 false 的话就不做转换

cjs 就是 commonjs

比如我们在 src 中创建一个 demo.module.js 文件测试,

demo.module.js:

import demo1 from "./demo1";
function test() {

}
export default test;

可以看到,在我们的测试代码中,我们导入一个 demo1,然后输出一个 test 函数,

我们把 modules 设置成 “auto”(默认)

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                loose: true,
                modules: "auto"
            }
        ]
    ]
};

然后我们直接运行 babel:

  babel-demo git:(v0.0.1)  npx babel ./src/demo.module.js
"use strict";

exports.__esModule = true;
exports.default = void 0;

var _demo = _interopRequireDefault(require("./demo1"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function test() {}

var _default = test;
exports.default = _default;

  babel-demo git:(v0.0.1)  

可以看到,默认以 commonjs 的方式输出 es6 的模块。

我们修改成 false,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                loose: true,
                modules: false
            }
        ]
    ]
};

然后我们运行 babel 看效果:

  babel-demo git:(v0.0.1)  npx babel ./src/demo.module.js
import demo1 from "./demo1";

function test() {}

export default test;

  babel-demo git:(v0.0.1)  

可以看到,es6 的模块代码没有做任何改变。

debug

boolean, 默认 false.

是否打印 prese-env 争对当前配置所加载的所有插件信息、所有浏览器列表,

我们来用用,我们直接加上 debug 为 true 参数,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                loose: true,
                modules: false,
                debug: true,
            }
        ]
    ]
};

然后我们运行 babel:

  babel-demo git:(v0.0.1)  npx babel ./src/demo.module.js
@babel/preset-env: `DEBUG` option

Using targets:
{
  "chrome": "49",
  "edge": "18",
  "firefox": "75",
  "ie": "11",
  "ios": "12.2",
  "opera": "68",
  "safari": "12.1",
  "samsung": "11.1"
}

Using modules transform: false

Using plugins:
  proposal-nullish-coalescing-operator { "chrome":"49", "edge":"18", "ie":"11", "ios":"12.2", "safari":"12.1", "samsung":"11.1" }
  proposal-optional-chaining { "chrome":"49", "edge":"18", "ie":"11", "ios":"12.2", "safari":"12.1", "samsung":"11.1" }
  proposal-json-strings { "chrome":"49", "edge":"18", "ie":"11" }
  proposal-optional-catch-binding { "chrome":"49", "edge":"18", "ie":"11" }
  transform-parameters { "ie":"11" }
  proposal-async-generator-functions { "chrome":"49", "edge":"18", "ie":"11" }
  proposal-object-rest-spread { "chrome":"49", "edge":"18", "ie":"11" }
  transform-dotall-regex { "chrome":"49", "edge":"18", "firefox":"75", "ie":"11" }
  proposal-unicode-property-regex { "chrome":"49", "edge":"18", "firefox":"75", "ie":"11" }
  transform-named-capturing-groups-regex { "chrome":"49", "edge":"18", "firefox":"75", "ie":"11" }
  transform-async-to-generator { "chrome":"49", "ie":"11" }
  transform-exponentiation-operator { "chrome":"49", "ie":"11" }
  transform-template-literals { "ie":"11", "ios":"12.2", "safari":"12.1" }
  transform-literals { "ie":"11" }
  transform-function-name { "chrome":"49", "edge":"18", "ie":"11" }
  transform-arrow-functions { "ie":"11" }
  transform-classes { "ie":"11" }
  transform-object-super { "ie":"11" }
  transform-shorthand-properties { "ie":"11" }
  transform-duplicate-keys { "ie":"11" }
  transform-computed-properties { "ie":"11" }
  transform-for-of { "chrome":"49", "ie":"11" }
  transform-sticky-regex { "ie":"11" }
  transform-unicode-escapes { "ie":"11" }
  transform-unicode-regex { "chrome":"49", "ie":"11" }
  transform-spread { "ie":"11" }
  transform-destructuring { "chrome":"49", "ie":"11" }
  transform-block-scoping { "ie":"11" }
  transform-new-target { "ie":"11" }
  transform-regenerator { "chrome":"49", "ie":"11" }
  syntax-dynamic-import { "chrome":"49", "edge":"18", "firefox":"75", "ie":"11", "ios":"12.2", "opera":"68", "safari":"12.1", "samsung":"11.1" }

Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
import demo1 from "./demo1";

function test() {}

export default test;

  babel-demo git:(v0.0.1)  

可以一目了然的看到 preset-env 用了哪些插件,以及我们所支持的浏览器集合。

include

Array<string|RegExp>, 默认为 [].

数组每个 item 可以为下面两种形式:

  • Babel plugins - 可以为插件的全路径 (@babel/plugin-transform-spread) 或者省略前缀 (plugin-transform-spread).
  • Built-ins (在 core-js@2core-js@3中, 可以为 es.map, es.set, 或者 es.object.assign,这样两个版本都会识别

通过上面的例子我们知道,如果浏览器版本支持当前语法的话,preset-env 是不会加载相应的插件的,但是如果用户强制需要加载某些插件的话,我们可以用 include 参数,比如 “es.promise”、“es.promise.finally” 等等。

我们修改一下配置文件,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                corejs: 3,
                useBuiltIns: 'usage',
              	debug: true,
                include: [
                ]
            }
        ]
    ]
};

我们添加了一个指定了 corejs 的版本(后面会讲到),然后把 polyfill 的注入方式改为‘usage’(后面会讲到),然后设置了 include 为一个空数组,

然后我们修改一下我们的浏览器列表文件,我们把浏览器的版本提高,使它能兼容我们的 demo1.js 的代码:

.browserslistrc

chrome 73

我们运行 babel 看一下效果(为了更直观的看效果,我们开启 debug):

  babel-demo git:(v0.0.1)  npx babel ./src/demo1.js -o ./lib/demo1.js
@babel/preset-env: `DEBUG` option

Using targets:
{
  "chrome": "73"
}

Using modules transform: auto

Using plugins:
  proposal-nullish-coalescing-operator { "chrome":"73" }
  proposal-optional-chaining { "chrome":"73" }
  syntax-json-strings { "chrome":"73" }
  syntax-optional-catch-binding { "chrome":"73" }
  syntax-async-generators { "chrome":"73" }
  syntax-object-rest-spread { "chrome":"73" }
  transform-modules-commonjs { "chrome":"73" }
  proposal-dynamic-import { "chrome":"73" }

Using polyfills with `usage` option:
  babel-demo git:(v0.0.1)  

因为当前浏览器支持我们的 demo1.js 的代码,只有一些默认的不支持的插件。

接下来我们修改一下我们的配置文件,添加一些默认的插件在 include 属性中:

babel.config.js

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                corejs: 3,
                useBuiltIns: 'usage',
                debug: true,
                include: [
                    '@babel/plugin-transform-classes',
                    
                    
                    'es.array.iterator',
                    'es.array.includes',
                    
                    'es.promise',
                    
                    
                    'es.object.assign',
                    
                    'es.promise.finally'
                ]
            }
        ]
    ]
};

可以看到,我们加了一些默认的插件在里面,

我们再次运行 babel 看结果:

 babel-demo git:(v0.0.1)  npx babel ./src/demo1.js -o ./lib/demo1.js
@babel/preset-env: `DEBUG` option

Using targets:
{
  "chrome": "73"
}

Using modules transform: auto

Using plugins:
  proposal-nullish-coalescing-operator { "chrome":"73" }
  proposal-optional-chaining { "chrome":"73" }
  syntax-json-strings { "chrome":"73" }
  syntax-optional-catch-binding { "chrome":"73" }
  syntax-async-generators { "chrome":"73" }
  syntax-object-rest-spread { "chrome":"73" }
  transform-classes {}
  transform-modules-commonjs { "chrome":"73" }
  proposal-dynamic-import { "chrome":"73" }

lib/demo1.js:

"use strict";

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

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

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 fn = () => {};

new Promise(() => {});

let Test = function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

const c = [1, 2, 3].includes(1);
var a = 10;

可以看到,比之前编译的多了很多插件信息,也就是我们的 include 里面默认的插件。

exclude

Array<string|RegExp>, 默认 [].

不需要依赖的插件集合,跟 include 用法一样,但是作用跟 include 相反。

useBuiltIns

@babel/preset-env 使用 polyfill 的配置,那么什么是 polyfill 呢?我们都知道,preset-env 会根据浏览器的配置做 es 的语法转换,但是在实际运行环境中,除了 es 的语法外,还有一些其它的 api,比如我们需要用到 Promise 对象,比如我们需要用到 Array.prototype.includes 方法等等,所以需要我们在当前运行环境中添加 Promise 和 Array.prototype.includes 方法,让当前的 js 代码能够运行在当前环境。

useBuiltIns的值为 entry 或者 usage 的时候,@babel-preset-env 会直接依赖 core-js 当作 polyfill 使用。

自从 babel 在 7.4.0 版本后弃用了 @babel/polyfill 后,babel 建议我们直接使用 core-js 并且设置 corejs 的版本来替换 polyfill。

useBuiltIns: 'entry'

当我们使用 entry 的时候,会根据当前浏览器的配置信息来加载 core-js 的插件,最后动态替换掉源码中两种形式的代码块。

  • import "core-js";
    
    
  • import "@babel/polyfill";
    
    

比如我们 demo 中,我们修改一下配置文件:

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                corejs: 3,
                useBuiltIns: 'entry',

            }
        ]
    ]
};

然后我们修改一下浏览器配置信息为默认:

.browserslistrc

> 0.25%, not dead

然后我们在 src 中创建一个叫 demo-entry.js 的文件测试:

src/demo-entry.js:

const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

我们直接运行 babel 命令:

npx babel ./src/demo-entry.js -o lib/demo-entry.js

lib/demo-entry.js:

"use strict";

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

var fn = function fn() {};

new Promise(function () {});

var Test = function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,除了转换了一些语法类的东西外并没有添加 polyfill(Promise、includes 等等)

那么我们设置了 usage 后,怎样才会起作用呢?我们改一下 demo-entry.js 文件的代码,

demo-entry.js:

import "core-js";
const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

然后我们再次运行 babel 看结果:

"use strict";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

require("core-js/modules/es.array.last-index-of");

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

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

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

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

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

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

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

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

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

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

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

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

require("core-js/modules/es.date.to-primitive");

require("core-js/modules/es.function.has-instance");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

require("core-js/modules/es.number.is-finite");

require("core-js/modules/es.number.is-integer");

require("core-js/modules/es.number.is-nan");

require("core-js/modules/es.number.is-safe-integer");

require("core-js/modules/es.number.max-safe-integer");

require("core-js/modules/es.number.min-safe-integer");

require("core-js/modules/es.number.parse-float");

require("core-js/modules/es.number.parse-int");

require("core-js/modules/es.number.to-fixed");

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

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

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

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

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

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

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.get-own-property-names");

require("core-js/modules/es.object.get-prototype-of");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

require("core-js/modules/es.reflect.delete-property");

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

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

require("core-js/modules/es.reflect.get-prototype-of");

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

require("core-js/modules/es.reflect.is-extensible");

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

require("core-js/modules/es.reflect.prevent-extensions");

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

require("core-js/modules/es.reflect.set-prototype-of");

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

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

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

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

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

require("core-js/modules/es.string.code-point-at");

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

require("core-js/modules/es.string.from-code-point");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

require("core-js/modules/es.typed-array.uint8-clamped-array");

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

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

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

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

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

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

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

require("core-js/modules/es.typed-array.find-index");

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

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

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

require("core-js/modules/es.typed-array.index-of");

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

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

require("core-js/modules/es.typed-array.last-index-of");

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

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

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

require("core-js/modules/es.typed-array.reduce-right");

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

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

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

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

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

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

require("core-js/modules/es.typed-array.to-locale-string");

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

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

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

require("core-js/modules/esnext.aggregate-error");

require("core-js/modules/esnext.array.last-index");

require("core-js/modules/esnext.array.last-item");

require("core-js/modules/esnext.composite-key");

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

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

require("core-js/modules/esnext.map.delete-all");

require("core-js/modules/esnext.map.every");

require("core-js/modules/esnext.map.filter");

require("core-js/modules/esnext.map.find");

require("core-js/modules/esnext.map.find-key");

require("core-js/modules/esnext.map.from");

require("core-js/modules/esnext.map.group-by");

require("core-js/modules/esnext.map.includes");

require("core-js/modules/esnext.map.key-by");

require("core-js/modules/esnext.map.key-of");

require("core-js/modules/esnext.map.map-keys");

require("core-js/modules/esnext.map.map-values");

require("core-js/modules/esnext.map.merge");

require("core-js/modules/esnext.map.of");

require("core-js/modules/esnext.map.reduce");

require("core-js/modules/esnext.map.some");

require("core-js/modules/esnext.map.update");

require("core-js/modules/esnext.math.clamp");

require("core-js/modules/esnext.math.deg-per-rad");

require("core-js/modules/esnext.math.degrees");

require("core-js/modules/esnext.math.fscale");

require("core-js/modules/esnext.math.iaddh");

require("core-js/modules/esnext.math.imulh");

require("core-js/modules/esnext.math.isubh");

require("core-js/modules/esnext.math.rad-per-deg");

require("core-js/modules/esnext.math.radians");

require("core-js/modules/esnext.math.scale");

require("core-js/modules/esnext.math.seeded-prng");

require("core-js/modules/esnext.math.signbit");

require("core-js/modules/esnext.math.umulh");

require("core-js/modules/esnext.number.from-string");

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

require("core-js/modules/esnext.promise.all-settled");

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

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

require("core-js/modules/esnext.reflect.define-metadata");

require("core-js/modules/esnext.reflect.delete-metadata");

require("core-js/modules/esnext.reflect.get-metadata");

require("core-js/modules/esnext.reflect.get-metadata-keys");

require("core-js/modules/esnext.reflect.get-own-metadata");

require("core-js/modules/esnext.reflect.get-own-metadata-keys");

require("core-js/modules/esnext.reflect.has-metadata");

require("core-js/modules/esnext.reflect.has-own-metadata");

require("core-js/modules/esnext.reflect.metadata");

require("core-js/modules/esnext.set.add-all");

require("core-js/modules/esnext.set.delete-all");

require("core-js/modules/esnext.set.difference");

require("core-js/modules/esnext.set.every");

require("core-js/modules/esnext.set.filter");

require("core-js/modules/esnext.set.find");

require("core-js/modules/esnext.set.from");

require("core-js/modules/esnext.set.intersection");

require("core-js/modules/esnext.set.is-disjoint-from");

require("core-js/modules/esnext.set.is-subset-of");

require("core-js/modules/esnext.set.is-superset-of");

require("core-js/modules/esnext.set.join");

require("core-js/modules/esnext.set.map");

require("core-js/modules/esnext.set.of");

require("core-js/modules/esnext.set.reduce");

require("core-js/modules/esnext.set.some");

require("core-js/modules/esnext.set.symmetric-difference");

require("core-js/modules/esnext.set.union");

require("core-js/modules/esnext.string.at");

require("core-js/modules/esnext.string.code-points");

require("core-js/modules/esnext.string.match-all");

require("core-js/modules/esnext.string.replace-all");

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

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

require("core-js/modules/esnext.symbol.pattern-match");

require("core-js/modules/esnext.weak-map.delete-all");

require("core-js/modules/esnext.weak-map.from");

require("core-js/modules/esnext.weak-map.of");

require("core-js/modules/esnext.weak-set.add-all");

require("core-js/modules/esnext.weak-set.delete-all");

require("core-js/modules/esnext.weak-set.from");

require("core-js/modules/esnext.weak-set.of");

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

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

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

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

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

require("core-js/modules/web.url.to-json");

require("core-js/modules/web.url-search-params");

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


var fn = function fn() {};

new Promise(function () {});

var Test = function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,我们编译过后的代码中添加了很多 polyfill。

useBuiltIns: 'usage'

使用了 usage 后,preset-env 会自动检测代码,如果需要依赖某个插件的时候,preset-env 就会直接依赖,不像使用 “entry” 一样,不管用不用,一股脑全部加载。

我们修改一下配置文件为 "usage":

babel.config.js

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                corejs: 3,
                useBuiltIns: 'usage',

            }
        ]
    ]
}

然后我们重新创建一个 demo-usage.js 文件用来测试,

src/demo-usage.js:

const fn = () => {};

new Promise(() => {});

class Test {
    say(){}
}

const c = [1, 2, 3].includes(1);
var a = 10;

我们运行 babel 看结果:

npx babel ./src/demo-usage.js -o lib/demo-usage.js

lib/demo-usage.js:

"use strict";

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

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

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

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

var fn = function fn() {};

new Promise(function () {});

var Test = function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

可以看到,相比 entry,preset-env 只帮我们导入了我们需要的插件:

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

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

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

useBuiltIns: false

默认行为,不会使用 polyfill。

corejs

2, 3 或者 {version: 2 | 3, proposals: boolean}, 默认 2.

前面说了,useBuiltIns设置 usage 或者 entry 的时候需要依赖 core-js 做插件注入,corejs是设置 corejs 的版本号。

我们看一下源码,看 preset-env 是怎样使用useBuiltIns做到 buildins 插件注入的,

packages/babel-preset-env/src/index.js:

...
export default declare((api, opts) => {
 
  const {
    configPath,
    debug,
    exclude: optionsExclude,
    forceAllTransforms,
    ignoreBrowserslistConfig,
    include: optionsInclude,
    loose,
    modules,
    shippedProposals,
    spec,
    targets: optionsTargets,
    useBuiltIns,
    corejs: { version: corejs, proposals },
  } = normalizeOptions(opts);
  	
  	const polyfillPlugins = getPolyfillPlugins({
    useBuiltIns,
    corejs,
    polyfillTargets: targets,
    include: include.builtIns,
    exclude: exclude.builtIns,
    proposals,
    shippedProposals,
    regenerator: pluginNames.has("transform-regenerator"),
    debug,
  });
}
export const getPolyfillPlugins = ({
  useBuiltIns,
  corejs,
  polyfillTargets,
  include,
  exclude,
  proposals,
  shippedProposals,
  regenerator,
  debug,
}: {
  useBuiltIns: BuiltInsOption,
  corejs: typeof SemVer | null | false,
  polyfillTargets: Targets,
  include: Set<string>,
  exclude: Set<string>,
  proposals: boolean,
  shippedProposals: boolean,
  regenerator: boolean,
  debug: boolean,
}) => {
  const polyfillPlugins = [];
  
  if (useBuiltIns === "usage" || useBuiltIns === "entry") {
    const pluginOptions = {
      corejs,
      polyfillTargets,
      include,
      exclude,
      proposals,
      shippedProposals,
      regenerator,
      debug,
    };
    if (corejs) {
      
      if (useBuiltIns === "usage") {
        
        if (corejs.major === 2) {
          
          polyfillPlugins.push([addCoreJS2UsagePlugin, pluginOptions]);
        } else {
           
          polyfillPlugins.push([addCoreJS3UsagePlugin, pluginOptions]);
        }
        
        if (regenerator) {
          polyfillPlugins.push([addRegeneratorUsagePlugin, pluginOptions]);
        }
      } else {
        
        if (corejs.major === 2) {
          
          polyfillPlugins.push([replaceCoreJS2EntryPlugin, pluginOptions]);
        } else {
           
          polyfillPlugins.push([replaceCoreJS3EntryPlugin, pluginOptions]);
          if (!regenerator) {
            polyfillPlugins.push([removeRegeneratorEntryPlugin, pluginOptions]);
          }
        }
      }
    }
  }
  return polyfillPlugins;
};

代码中都有注释,getPolyfillPlugins 会根据 useBuiltIns 的配置加载不同的插件,比如我们的配置文件,

babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                corejs: 3,
                useBuiltIns: 'usage',
            }
        ]
    ]
};

getPolyfillPlugins 方法会走以下代码:

 
          polyfillPlugins.push([addCoreJS3UsagePlugin, pluginOptions]);

我们看一下 addCoreJS3UsagePlugin 是什么,

packages/babel-preset-env/src/polyfills/corejs3/usage-plugin.js:

import corejs3Polyfills from "core-js-compat/data";
import corejs3ShippedProposalsList from "./shipped-proposals";
import getModulesListForTargetVersion from "core-js-compat/get-modules-list-for-target-version";
import filterItems from "../../filter-items";
import {
  BuiltIns,
  StaticProperties,
  InstanceProperties,
  CommonIterators,
  CommonInstanceDependencies,
  PromiseDependencies,
  PossibleGlobalObjects,
} from "./built-in-definitions";
import {
  createImport,
  getType,
  has,
  intersection,
  isPolyfillSource,
  getImportSource,
  getRequireSource,
  isNamespaced,
} from "../../utils";
import { logUsagePolyfills } from "../../debug";

import type { InternalPluginOptions } from "../../types";
import type { NodePath } from "@babel/traverse";

const NO_DIRECT_POLYFILL_IMPORT = `
  When setting \`useBuiltIns: 'usage'\`, polyfills are automatically imported when needed.
  Please remove the direct import of \`core-js\` or use \`useBuiltIns: 'entry'\` instead.`;

const corejs3PolyfillsWithoutProposals = Object.keys(corejs3Polyfills)
  .filter(name => !name.startsWith("esnext."))
  .reduce((memo, key) => {
    memo[key] = corejs3Polyfills[key];
    return memo;
  }, {});

const corejs3PolyfillsWithShippedProposals = corejs3ShippedProposalsList.reduce(
  (memo, key) => {
    memo[key] = corejs3Polyfills[key];
    return memo;
  },
  { ...corejs3PolyfillsWithoutProposals },
);

export default function(
  _: any,
  {
    corejs,
    include,
    exclude,
    polyfillTargets,
    proposals,
    shippedProposals,
    debug,
  }: InternalPluginOptions,
) {
  const polyfills = filterItems(
    proposals
      ? corejs3Polyfills
      : shippedProposals
      ? corejs3PolyfillsWithShippedProposals
      : corejs3PolyfillsWithoutProposals,
    include,
    exclude,
    polyfillTargets,
    null,
  );
	...
}

我们看到,首先是获取所有的 polyfills,我们顺便把corejs的 proposal 参数跟shippedProposals参数一起讲了,

corejs(proposal)& shippedProposals

我们可以看到代码:

const polyfills = filterItems(
    proposals
      ? corejs3Polyfills
      : shippedProposals
      ? corejs3PolyfillsWithShippedProposals
      : corejs3PolyfillsWithoutProposals,
    include,
    exclude,
    polyfillTargets,
    null,
  );

proposals 默认为 false,我们 demo 中也是默认设置 false,所以会走:

shippedProposals
      ? corejs3PolyfillsWithShippedProposals
      : corejs3PolyfillsWithoutProposals

我们 demo 中 shippedProposals 也是 false,所以直接返回一个 corejs3PolyfillsWithoutProposals 数组,我们看一下 corejs3PolyfillsWithoutProposals,

packages/babel-preset-env/src/polyfills/corejs3/usage-plugin.js:

const corejs3PolyfillsWithoutProposals = Object.keys(corejs3Polyfills)
  .filter(name => !name.startsWith("esnext."))
  .reduce((memo, key) => {
    memo[key] = corejs3Polyfills[key];
    return memo;
  }, {});

会遍历 corejs3Polyfills 集合,然后过滤掉 “esnext.” 打头的插件,

import corejs3Polyfills from "core-js-compat/data";

node_modules/core-js-compat/data.json:

{
  ...
  "es.promise": {
    "chrome": "67",
    "edge": "74",
    "electron": "4.0",
    "firefox": "69",
    "ios": "11.0",
    "node": "10.4",
    "opera": "54",
    "opera_mobile": "48",
    "safari": "11.0",
    "samsung": "9.0"
  }
  ...
}

内容有点多,我就只截取了一个,比如我们的 promise,那如果设置了 proposals 为 true 的话,会直接使用 corejs3Polyfills 集合,如果 proposals 为 false 并且 shippedProposals 为 true 的时候,polyfills 返回的就是 corejs3PolyfillsWithShippedProposals 集合,corejs3PolyfillsWithShippedProposals 集合是 corejs3PolyfillsWithoutProposals 跟 corejs3ShippedProposalsList 集合的并集,

const corejs3PolyfillsWithShippedProposals = corejs3ShippedProposalsList.reduce(
  (memo, key) => {
    memo[key] = corejs3Polyfills[key];
    return memo;
  },
  { ...corejs3PolyfillsWithoutProposals },
);

那么 corejs3ShippedProposalsList 里面默认有哪些插件呢?

packages/babel-preset-env/src/polyfills/corejs3/shipped-proposals.js:

export default (["esnext.global-this", "esnext.string.match-all"]: string[]);

其实就两个,一个是 global 对象,还有一个就是 string 的 matchAll 方法. 所以只有当 corejs 的 proposals 的时候这里的shippedProposals配置才会起作用。

我们已经根据浏览器配置和 corejs 的参数还有shippedProposals参数获取到了我们当前环境所需要的所有插件,那么当使用 usage 的时候,usage-plugin.js 是怎样根据代码加载对应的插件的呢?

比如我们 demo-usage.js 代码中的:

new Promise(() => {});

也就是说当 usage-plugin 插件加载到 Promise 的时候,这个时候就需要判断当前环境是不是需要添 "Promise" 的 polyfill 了

packages/babel-preset-env/src/polyfills/corejs3/usage-plugin.js:

   
    ReferencedIdentifier({ node: { name }, scope }: NodePath) {
      if (scope.getBindingIdentifier(name)) return;

      this.addBuiltInDependencies(name);
    },

代码中添加了 ast 节点钩子函数,也就是当读到 new Promise(() => {}); 的时候会触发这个钩子函数,name 就是 Promise, 然后会调用 addBuiltInDependencies 方法:

 this.addBuiltInDependencies = function(builtIn) {
   	
        if (has(BuiltIns, builtIn)) {
          
          const BuiltInDependencies = BuiltIns[builtIn];
          this.addUnsupported(BuiltInDependencies);
        }
      };

我们看一下 BuiltIns 集合,

export const BuiltIns: ObjectMap<string[]> = {
  AggregateError: ["esnext.aggregate-error", ...CommonIterators],
  ArrayBuffer: [
    "es.array-buffer.constructor",
    "es.array-buffer.slice",
    "es.object.to-string",
  ],
  DataView: ["es.data-view", "es.array-buffer.slice", "es.object.to-string"],
  Date: ["es.date.to-string"],
  Float32Array: ["es.typed-array.float32-array", ...TypedArrayDependencies],
  Float64Array: ["es.typed-array.float64-array", ...TypedArrayDependencies],
  Int8Array: ["es.typed-array.int8-array", ...TypedArrayDependencies],
  Int16Array: ["es.typed-array.int16-array", ...TypedArrayDependencies],
  Int32Array: ["es.typed-array.int32-array", ...TypedArrayDependencies],
  Uint8Array: ["es.typed-array.uint8-array", ...TypedArrayDependencies],
  Uint8ClampedArray: [
    "es.typed-array.uint8-clamped-array",
    ...TypedArrayDependencies,
  ],
  Uint16Array: ["es.typed-array.uint16-array", ...TypedArrayDependencies],
  Uint32Array: ["es.typed-array.uint32-array", ...TypedArrayDependencies],
  Map: MapDependencies,
  Number: ["es.number.constructor"],
  Observable: [
    "esnext.observable",
    "esnext.symbol.observable",
    "es.object.to-string",
    ...CommonIteratorsWithTag,
  ],
  Promise: PromiseDependencies,
  RegExp: ["es.regexp.constructor", "es.regexp.exec", "es.regexp.to-string"],
  Set: SetDependencies,
  Symbol: SymbolDependencies,
  URL: ["web.url", ...URLSearchParamsDependencies],
  URLSearchParams: URLSearchParamsDependencies,
  WeakMap: WeakMapDependencies,
  WeakSet: WeakSetDependencies,
  clearImmediate: ["web.immediate"],
  compositeKey: ["esnext.composite-key"],
  compositeSymbol: ["esnext.composite-symbol", ...SymbolDependencies],
  fetch: PromiseDependencies,
  globalThis: ["esnext.global-this"],
  parseFloat: ["es.parse-float"],
  parseInt: ["es.parse-int"],
  queueMicrotask: ["web.queue-microtask"],
  setTimeout: ["web.timers"],
  setInterval: ["web.timers"],
  setImmediate: ["web.immediate"],
};

我们找到 Promise:

export const PromiseDependencies = ["es.promise", "es.object.to-string"];
export const BuiltIns: ObjectMap<string[]> = {
  ...
	Promise: PromiseDependencies,
  ...
}

可以看到,Promise 依赖了两个插件:“es.promise” 跟 “es.object.to-string”,就是这样我们的 demo-usage.js 编译完毕后会自动导入这两个插件:

lib/demo-usage.js

"use strict";

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

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

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

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

var fn = function fn() {};

new Promise(function () {});

var Test = function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, [{
    key: "say",
    value: function say() {}
  }]);

  return Test;
}();

var c = [1, 2, 3].includes(1);
var a = 10;

useBuildIns 的 entry 我就不带着看源码了,跟 usage 差不多,也就是当遍历到 “import corejs” 节点的时候使用 corejs3Polyfills 替换掉它,小伙伴自己去看源码哦。

configPath

告诉 preset-env 从哪里开始寻找 browserslist 的配置,一直往上一级递归直到找到配置文件,默认是根目录。

ignoreBrowserslistConfig

是否禁止寻找 browserslist配置文件,默认是 false

OK~ 我们用了一篇很长的文章介绍了 babel 的 preset-env,是真的很长啊,有些地方可能不太好理解,因为源码实在是太多了,我也没法把所有的源码都贴过来,小伙伴一定要结合文章 demo 然后去看源码,相信我,你会有不一样的收获的,下节我们介绍 babel-plugin-transfrom-runtime 跟 babel-runtime,然后对比 preset-env,大家敬请期待!!

demo 源码](https://github.com/913453448/babel-demo.git)
https://vvbug.blog.csdn.net/article/details/107052867

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant