babel源码解析之(@babel/preset-env)

本文详细解析了@babel/preset-env的工作原理,从项目创建、配置到实际运行,逐步展示了如何根据浏览器列表自动选择需要的转换插件,并根据配置引入polyfill。通过实例分析,阐述了preset-env如何根据browserslist配置和代码内容,智能地转换ES6代码为ES5,以及如何处理polyfill的注入。文章最后提到了useBuiltIns和corejs的相关配置选项,帮助理解其在转换过程中的作用。
摘要由CSDN通过智能技术生成

前言

还记得之前写过一篇文章: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 => {
   
    // Feature is not implemented in that environment
    if (!plugin[environment]) {
   
      return true;
    }

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

    // If targets has unreleased value as a lowest version, then don't require a plugin.
    if (isUnreleasedVersion(lowestTargetedVersion, environment)) {
   
      return false;
    }

    // Include plugin if it is supported in the unreleased environment, which wasn't specified in targets
    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:

// @flow
...
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,
  ) {
   
   ...
    // make sure this class isn't directly called (with A() instead new A())
    if (!classState.isLoose) {
   
      constructorBody.body.unshift(
        t.expressionStatement(
          t.callExpression(classState.file.addHelper("classCallCheck"), [
            t.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值