Appearance
带你玩转 babel 工具链(六)是时候来看看 preset-env 的源码了
一、前言
本文将带你学习preset-env
源码,彻底理解这些配置后的含义。
往期回顾:
二、基本配置
在preset-env
的配置中,添加了core-js
的 polyfill 的支持。useBuiltIns
指定按需加载。
js
npm i @babel/preset-env core-js@3
npm i @babel/preset-env core-js@3
js
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
简单使用一下,我们写了一段includes
的 api,看看打包后的代码是如何polyfill
的
js
console.log([].includes("1"));
console.log([].includes("1"));
@babel/preset-env
帮我们在顶部添加了一段导入代码。实现了includes
的 api
以上就是一个简单的例子,下面介绍下参数详细的作用
三、通过参数分析源码过程
我们以上面的代码为例
js
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "xxx",
"corejs": 3
}]
]
}
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "xxx",
"corejs": 3
}]
]
}
js
console.log([].includes("1"));
console.log([].includes("1"));
注意:在下面的例子中,将统一使用corejs@3
下面我将一一演示preset-env
的参数帮助理解,并且对大家以后的项目配置也有一定的帮助。大家耐心看完~
大家先打开源码位置: node_modules/@babel/preset-env/lib/index.js
参数 1:forceAllTransforms
源码:
js
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;
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
。
例如我配置:
js
"targets": [
"last 2 versions",
]
"targets": [
"last 2 versions",
]
经过getLocalTargets
处理后,targets
如下
它会列出,浏览器所能支持的最低版本。
当你在preset-env
中配置上forceAllTransforms: true
,那么代表所有的代码都需要polyfill
我们跟着源码继续往下看~
参数 2:include、exclude
源码:
js
// 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);
// 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);
include
和exclude
是相对立的,支持配置两种模式
- 插件名称,例如
@babel/plugin-transform-xxx
- polyfill 名, 例如
es.array.includes
什么场景需要这种配置呢?我们知道preset-env
是支持targets
配置的,但是不一定非常准确,有时候可能会把我们需要支持的语言特性
排除掉了,所以这时候就需要include
,来单独添加插件或polyfill
。同样的exclude
使用来排除,浏览器支持的语言特性。
在下面的配置中,我添加了targets
配置,设置当前环境为chrome
最新的两个版本。那么对于上面的例子来讲,是不会被 polyfill 的。
js
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": [
"last 2 Chrome versions"
]
}]
]
}
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": [
"last 2 Chrome versions"
]
}]
]
}
结果如我们预期那样:
这时候我添加一个配置
js
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": [
"last 2 Chrome versions"
],
"include": [
"es.array.includes" // 这里添加了配置
]
}]
]
}
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": [
"last 2 Chrome versions"
],
"include": [
"es.array.includes" // 这里添加了配置
]
}]
]
}
重新打包看下,发现已经能正常的 polyfill 了
当然,你也可以配置插件,例如:你的浏览器其实不支持for of
语法,但被targets
排除掉了。这种情况就可以配置上插件名。
js
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": [
"last 2 Chrome versions"
],
"include": [
// 在这里配置
"@babel/plugin-transform-for-of"
]
}]
]
}
{
"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 详解
源码:
js
// 获取所有插件对应的环境
const compatData = getPluginList(shippedProposals, bugfixes);
// 获取所有插件对应的环境
const compatData = getPluginList(shippedProposals, bugfixes);
我们先看下compatData
长什么样?
可以发现preset-env
中,列出了所有插件对应的浏览器最低可以支持的版本。在后面将通过targets
做进一步的筛选。
其实babel
在@babel/compat-data
中维护了一套配置。 我们定位到这个目录
js
node_modules/@babel/compat-data/data/plugins.json
node_modules/@babel/compat-data/data/plugins.json
在core-js
中,也同样维护了一份polyfill
的targets
配置
node_modules/core-js-compat/data.json
参数 4:modules
源码:
js
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),
});
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
它的作用是什么呢,我们先看看文档:
意思就是,我们可以告诉babel
,我们已经支持了部分语言特性,例如webpack
它自身已经可以识别esm
, 动态import
, top-level-await
了,并且还自己实现了。那么我们可以告诉babel
, 你不需要自己去编译了~剩下交给我。。
所以我们能打开babel-loader
, 看下它的配置
告诉babel
以上的语法都是支持的。
这样,在下面的源码里,就可以做到按需添加模块转换插件
js
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;
};
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
模块转换插件
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"
}
{
auto: "transform-modules-commonjs",
amd: "transform-modules-amd",
commonjs: "transform-modules-commonjs",
cjs: "transform-modules-commonjs",
systemjs: "transform-modules-systemjs",
umd: "transform-modules-umd"
}
总结一下:
获取当前环境是否支持命名空间导出,例如
export * as xxx from 'xxx'
获取对应的模块插件,如果还支持
top-level-await
就返回syntax-top-level-await
, 如果有动态 import, 就返回syntax-dynamic-import
(其中有一些细节,不详细展开了)js// 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" }
// 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
之后所需的插件。
由于modules
默认值为auto
, 所以默认的模块规范就是commonjs
, 进而使用@babel/transform-modules-commonjs
进行转换。
其他配置同理~
参数 5:useBuiltIns
该配置必须和
corejs
搭配使用
源码:
前面说到babel
维护了一套compactData
配置。
下面就会根据环境配置,筛选出需要的插件
js
// 根据目标环境 筛选出需要的插件
const pluginNames = (0, _helperCompilationTargets.filterItems)(
compatData,
include.plugins,
exclude.plugins,
transformTargets,
modulesPluginNames,
(0, _getOptionSpecificExcludes.default)({
loose,
}),
_shippedProposals.pluginSyntaxMap
);
// 根据目标环境 筛选出需要的插件
const pluginNames = (0, _helperCompilationTargets.filterItems)(
compatData,
include.plugins,
exclude.plugins,
transformTargets,
modulesPluginNames,
(0, _getOptionSpecificExcludes.default)({
loose,
}),
_shippedProposals.pluginSyntaxMap
);
获取到需要的插件后,就到达很关键的地方了, 我们看下polyfill
是如何添加的
js
// 获取polyfill插件
const polyfillPlugins = getPolyfillPlugins({
useBuiltIns,
corejs,
polyfillTargets: targets,
include: include.builtIns,
exclude: exclude.builtIns,
proposals,
shippedProposals,
regenerator: pluginNames.has("transform-regenerator"),
debug,
});
// 获取polyfill插件
const polyfillPlugins = getPolyfillPlugins({
useBuiltIns,
corejs,
polyfillTargets: targets,
include: include.builtIns,
exclude: exclude.builtIns,
proposals,
shippedProposals,
regenerator: pluginNames.has("transform-regenerator"),
debug,
});
js
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;
};
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"
的配置下,打包结果如下 可以看到能够实现按需引入在
useBuiltIns: "entry"
的配置下,还需要在入口文件中添加core-js
的导入,如何你还想支持async
语法,还需要引入regenerator-runtime/runtime.js
jsimport "core-js"; // 其他语言特性支持 import "regenerator-runtime/runtime.js"; // 支持async console.log([].includes("1"));
import "core-js"; // 其他语言特性支持 import "regenerator-runtime/runtime.js"; // 支持async console.log([].includes("1"));
可以看到,会把所有的 polyfill 都引入进来,所以
entry
的配置并不推荐使用,会全量引入
。在
useBuiltIns: false
配置下,core-js
配置将失效,不会帮助引入polyfill
参数 6:corejs
corejs 就比较简单了,指定 corejs 的版本就可以了,但是必须搭配useBuiltIns
使用哦~
参数 7:debug
源码:
js
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."
);
}
}
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
后,控制台就能看见你使用了哪些插件
四、babel-plugin-polyfill-corejs3
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿
helper-define-polyfill-provider