带你玩转babel工具链(六)是时候来看看preset-env的源码了

1,757 阅读6分钟

一、前言

本文将带你学习preset-env源码,彻底理解这些配置后的含义。

往期回顾:

二、基本配置

preset-env的配置中,添加了core-js的polyfill的支持。useBuiltIns指定按需加载。

npm i @babel/preset-env core-js@3
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ]
}

简单使用一下,我们写了一段includes的api,看看打包后的代码是如何polyfill

console.log([].includes('1'))

@babel/preset-env帮我们在顶部添加了一段导入代码。实现了includes的api

image.png

以上就是一个简单的例子,下面介绍下参数详细的作用

三、通过参数分析源码过程

我们以上面的代码为例

{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "xxx",
      "corejs": 3
    }]
  ]
}
console.log([].includes('1'))

注意:在下面的例子中,将统一使用corejs@3

下面我将一一演示preset-env的参数帮助理解,并且对大家以后的项目配置也有一定的帮助。大家耐心看完~

大家先打开源码位置: node_modules/@babel/preset-env/lib/index.js

参数1:forceAllTransforms

源码

  if ((0, _semver.lt)(api.version, "7.13.0") || opts.targets || opts.configPath || opts.browserslistEnv || opts.ignoreBrowserslistConfig) {
    {
      var hasUglifyTarget = false;

      if (optionsTargets != null && optionsTargets.uglify) {
        hasUglifyTarget = true;
        delete optionsTargets.uglify;
        console.warn(`
The uglify target has been deprecated. Set the top level
option \`forceAllTransforms: true\` instead.
`);
      }
    }
    targets = getLocalTargets(optionsTargets, ignoreBrowserslistConfig, configPath, browserslistEnv);
  }
  // 需要转换的目标环境 如果为true 就全部转换
  const transformTargets = forceAllTransforms || hasUglifyTarget ? {} : targets;

preset-env中,在一开始会调用getLocalTargets获取当前你配置的targets

例如我配置:

"targets": [
    "last 2 versions",
]

经过getLocalTargets处理后,targets如下

image.png

它会列出,浏览器所能支持的最低版本。

当你在preset-env中配置上forceAllTransforms: true,那么代表所有的代码都需要polyfill

我们跟着源码继续往下看~

参数2:include、exclude

源码

  // 1. 指定包含的插件,比如配置targets之后,有些插件被排除了,但是我就是想用这个插件
  // 2. 指定要包含的corejs polyfill语法,例如es.map, es.set等
  const include = transformIncludesAndExcludes(optionsInclude);

  // 1. 指定排除的插件,比如配置targets之后,有些插件被包含了,但是我想排除它
  // 2. 指定要排除的corejs polyfill语法,例如es.map, es.set等
  const exclude = transformIncludesAndExcludes(optionsExclude);

includeexclude是相对立的,支持配置两种模式

  • 插件名称,例如@babel/plugin-transform-xxx
  • polyfill名, 例如es.array.includes

什么场景需要这种配置呢?我们知道preset-env是支持targets配置的,但是不一定非常准确,有时候可能会把我们需要支持的语言特性排除掉了,所以这时候就需要include,来单独添加插件或polyfill。同样的exclude使用来排除,浏览器支持的语言特性。

在下面的配置中,我添加了targets配置,设置当前环境为chrome最新的两个版本。那么对于上面的例子来讲,是不会被polyfill的。

{
    "presets": [
      ["@babel/preset-env", {
        "useBuiltIns": "usage",
        "corejs": 3,
        "targets": [
          "last 2 Chrome versions"
        ]
      }]
    ]
}

结果如我们预期那样:

image.png

这时候我添加一个配置

{
    "presets": [
      ["@babel/preset-env", {
        "useBuiltIns": "usage",
        "corejs": 3,
        "targets": [
          "last 2 Chrome versions"
        ],
       "include": [
          "es.array.includes" // 这里添加了配置
       ]
      }]
    ]
}

重新打包看下,发现已经能正常的polyfill了

image.png

当然,你也可以配置插件,例如:你的浏览器其实不支持for of语法,但被targets排除掉了。这种情况就可以配置上插件名。

{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3,
      "targets": [
        "last 2 Chrome versions"
      ],
      "include": [
        // 在这里配置
        "@babel/plugin-transform-for-of"
      ]
    }]
  ]
}

以上就是include的作用,exclude想必不用多说了~

参数3:targets

targets的写法大家可以参考这篇文章Browser list详解

源码:

// 获取所有插件对应的环境
const compatData = getPluginList(shippedProposals, bugfixes);

我们先看下compatData长什么样?

image.png

可以发现preset-env中,列出了所有插件对应的浏览器最低可以支持的版本。在后面将通过targets做进一步的筛选。

其实babel@babel/compat-data中维护了一套配置。 我们定位到这个目录

node_modules/@babel/compat-data/data/plugins.json

image.png

core-js中,也同样维护了一份polyfilltargets配置

node_modules/core-js-compat/data.json

image.png

参数4:modules

源码

  const shouldSkipExportNamespaceFrom = modules === "auto" && (api.caller == null ? void 0 : api.caller(supportsExportNamespaceFrom)) || modules === false && !(0, _helperCompilationTargets.isRequired)("proposal-export-namespace-from", transformTargets, {
    compatData,
    includes: include.plugins,
    excludes: exclude.plugins
  });

  // modules如果是umd这些模块规范,就会加载下面这些插件
  // proposal-dynamic-import
  // proposal-export-namespace-from
  // syntax-top-level-await

  // modules: false
  // 只支持语法,不进行转换
  // syntax-dynamic-import
  // syntax-export-namespace-from
  const modulesPluginNames = getModulesPluginNames({
    modules,
    transformations: _moduleTransformations.default,
    shouldTransformESM: modules !== "auto" || !(api.caller != null && api.caller(supportsStaticESM)),
    shouldTransformDynamicImport: modules !== "auto" || !(api.caller != null && api.caller(supportsDynamicImport)),
    shouldTransformExportNamespaceFrom: !shouldSkipExportNamespaceFrom,
    shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait)
  });

在上面的代码中,我们可以看到,都有一段这样的代码:api.caller

它的作用是什么呢,我们先看看文档:

image.png

意思就是,我们可以告诉babel,我们已经支持了部分语言特性,例如webpack它自身已经可以识别esm, 动态import, top-level-await了,并且还自己实现了。那么我们可以告诉babel, 你不需要自己去编译了~剩下交给我。。

所以我们能打开babel-loader, 看下它的配置

image.png

告诉babel以上的语法都是支持的。

这样,在下面的源码里,就可以做到按需添加模块转换插件

const getModulesPluginNames = ({
  modules,
  transformations,
  shouldTransformESM, // 是否转换esm
  shouldTransformDynamicImport, // 是否转换动态import
  shouldTransformExportNamespaceFrom, // 是否转换命名导出 export * as ns from "mod";
  shouldParseTopLevelAwait // 是否编译toplevel await
}) => {
  const modulesPluginNames = [];

  if (modules !== false && transformations[modules]) {
    if (shouldTransformESM) {
      modulesPluginNames.push(transformations[modules]);
    }

    if (shouldTransformDynamicImport && shouldTransformESM && modules !== "umd") {
      modulesPluginNames.push("proposal-dynamic-import");
    } else {
      if (shouldTransformDynamicImport) {
        console.warn("Dynamic import can only be supported when transforming ES modules" + " to AMD, CommonJS or SystemJS. Only the parser plugin will be enabled.");
      }

      modulesPluginNames.push("syntax-dynamic-import");
    }
  } else {
    modulesPluginNames.push("syntax-dynamic-import");
  }

  if (shouldTransformExportNamespaceFrom) {
    modulesPluginNames.push("proposal-export-namespace-from");
  } else {
    modulesPluginNames.push("syntax-export-namespace-from");
  }

  if (shouldParseTopLevelAwait) {
    modulesPluginNames.push("syntax-top-level-await");
  }

  return modulesPluginNames;
};

另外,还会根据你的modules配置,去添加对应的模块转换插件, 可以看到默认是auto,使用了commonjs模块转换插件

{
  auto: "transform-modules-commonjs",
  amd: "transform-modules-amd",
  commonjs: "transform-modules-commonjs",
  cjs: "transform-modules-commonjs",
  systemjs: "transform-modules-systemjs",
  umd: "transform-modules-umd"
}

总结一下:

  1. 获取当前环境是否支持命名空间导出,例如export * as xxx from 'xxx'

  2. 获取对应的模块插件,如果还支持top-level-await就返回syntax-top-level-await, 如果有动态import, 就返回syntax-dynamic-import(其中有一些细节,不详细展开了)

    // node_modules/@babel/preset-env/lib/module-transformations.js
    {
      auto: "transform-modules-commonjs",
      amd: "transform-modules-amd",
      commonjs: "transform-modules-commonjs",
      cjs: "transform-modules-commonjs",
      systemjs: "transform-modules-systemjs",
      umd: "transform-modules-umd"
    }
    

    如果配置modules: false,其实不需要做转换了,只需要支持语法 ,以下是配置modules: false之后所需的插件。

    image.png

由于modules默认值为auto, 所以默认的模块规范就是commonjs, 进而使用@babel/transform-modules-commonjs进行转换。

其他配置同理~

参数5:useBuiltIns

该配置必须和corejs搭配使用

源码

前面说到babel维护了一套compactData配置。

image.png

下面就会根据环境配置,筛选出需要的插件

  // 根据目标环境 筛选出需要的插件
const pluginNames = (0, _helperCompilationTargets.filterItems)(compatData, include.plugins, exclude.plugins, transformTargets, modulesPluginNames, (0, _getOptionSpecificExcludes.default)({
    loose
  }), _shippedProposals.pluginSyntaxMap);

image.png

获取到需要的插件后,就到达很关键的地方了, 我们看下polyfill是如何添加的

// 获取polyfill插件
const polyfillPlugins = getPolyfillPlugins({
    useBuiltIns,
    corejs,
    polyfillTargets: targets,
    include: include.builtIns,
    exclude: exclude.builtIns,
    proposals,
    shippedProposals,
    regenerator: pluginNames.has("transform-regenerator"),
    debug
});
const getPolyfillPlugins = ({
  useBuiltIns,
  corejs,
  polyfillTargets,
  include,
  exclude,
  proposals,
  shippedProposals,
  regenerator,
  debug
}) => {
  const polyfillPlugins = [];

  if (useBuiltIns === "usage" || useBuiltIns === "entry") {
    const pluginOptions = {
      method: `${useBuiltIns}-global`,
      version: corejs ? corejs.toString() : undefined,
      targets: polyfillTargets,
      include,
      exclude,
      proposals,
      shippedProposals,
      debug
    };
    // 判断是否配置corejs
    if (corejs) {
      if (useBuiltIns === "usage") {
        if (corejs.major === 2) {
          // 添加 babel-plugin-polyfill-corejs2 和 babel-polyfill 插件
          polyfillPlugins.push([pluginCoreJS2, pluginOptions], [_babelPolyfill.default, {
            usage: true
          }]);
        } else {
          // 添加 babel-plugin-polyfill-corejs3 插件 和 babel-polyfill 插件
          polyfillPlugins.push([pluginCoreJS3, pluginOptions], [_babelPolyfill.default, {
            usage: true,
            deprecated: true
          }]);
        }
        // 添加 babel-plugin-polyfill-regenerator 插件
        if (regenerator) {
          polyfillPlugins.push([pluginRegenerator, {
            method: "usage-global",
            debug
          }]);
        }
      } else {
        if (corejs.major === 2) {
          // babel-polyfill 插件(全局引入)、babel-plugin-polyfill-corejs2插件
          // 注意插件执行顺序,先执行的babel-polyfill
          polyfillPlugins.push([_babelPolyfill.default, {
            regenerator
          }], [pluginCoreJS2, pluginOptions]);
        } else {
          // 添加 babel-plugin-polyfill-corejs3 插件 和 babel-polyfill 插件
          polyfillPlugins.push([pluginCoreJS3, pluginOptions], [_babelPolyfill.default, {
            deprecated: true
          }]);

          if (!regenerator) {
            polyfillPlugins.push([_regenerator.default, pluginOptions]);
          }
        }
      }
    }
  }

  return polyfillPlugins;
}

我们可以总结如下几点

  • 存在corejs配置
    • useBuiltIns: usage

      • 如果配置core-js@3

        • 添加 babel-plugin-polyfill-corejs3插件
        • 添加 babel-polyfill 插件 (@babel/preset-env/lib/polyfills/babel-polyfill.js)
      • 如果配置core-js@2

        • 添加 babel-plugin-polyfill-corejs2插件
        • 添加 babel-polyfill 插件 (@babel/preset-env/lib/polyfills/babel-polyfill.js)
      • 如果配置 transform-regenerator

        • 添加 babel-plugin-polyfill-regenerator 插件
    • useBuiltIns: entry | false

      • 如果配置core-js@3

        • 添加 babel-plugin-polyfill-corejs3插件
        • 添加 babel-polyfill 插件 (@babel/preset-env/lib/polyfills/babel-polyfill.js)
      • 如果配置core-js@2

        • 添加 babel-plugin-polyfill-corejs2
        • 添加 babel-polyfill 插件 (@babel/preset-env/lib/polyfills/babel-polyfill.js)
      • 如果没有配置 transform-regenerator 插件

        • 添加 regenerator 插件删除 regenerator 导入(@babel/preset-env/lib/polyfills/regenerator.js)

使用:

好的,上面就是polyfill插件的具体添加过程,下面我们来看看useBuiltIns是如何使用的。

  • useBuiltIns: "usage"的配置下,打包结果如下

    image.png 可以看到能够实现按需引入

  • useBuiltIns: "entry"的配置下,还需要在入口文件中添加core-js的导入,如何你还想支持async语法,还需要引入regenerator-runtime/runtime.js

    import "core-js"; // 其他语言特性支持
    import "regenerator-runtime/runtime.js"; // 支持async
    console.log([].includes('1'))
    

    image.png

    可以看到,会把所有的polyfill都引入进来,所以entry的配置并不推荐使用,会全量引入

  • useBuiltIns: false配置下,core-js配置将失效,不会帮助引入polyfill

参数6:corejs

corejs就比较简单了,指定corejs的版本就可以了,但是必须搭配useBuiltIns使用哦~

参数7:debug

源码

  if (debug) {
    console.log("@babel/preset-env: `DEBUG` option");
    console.log("\nUsing targets:");
    console.log(JSON.stringify((0, _helperCompilationTargets.prettifyTargets)(targets), null, 2));
    console.log(`\nUsing modules transform: ${modules.toString()}`);
    console.log("\nUsing plugins:");
    pluginNames.forEach(pluginName => {
      (0, _debug.logPlugin)(pluginName, targets, compatData);
    });

    if (!useBuiltIns) {
      console.log("\nUsing polyfills: No polyfills were added, since the `useBuiltIns` option was not set.");
    }
  }

使用:

当配置上debug: true后,控制台就能看见你使用了哪些插件

image.png

未完待续~

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿