webpack5是如何使用babel转化代码的(1)-业务开发时的babel配置

1,013 阅读5分钟

在前端开发工程中,因为js代码的更新换代速度快于浏览器的更新速度,所以导致了部分旧的浏览器,比如ie系列,不支持部分js代码。为了能够在这些浏览器中运行我们的项目,需要进行兼容处理。这就是babel的产生来由。本文主要讲业务开发者如何配置babel.config.js文件。以及理清babel的各种常用预设和插件的关系。

一,新建一个基础的webpack5项目

初始化项目

npm init -y

安装webpack相关依赖

npm install webpack webpack-cli webpack-dev-server -D

新建src文件夹,并写上代码

./src/main.js

  function test(){
    return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve(22)
      },0)
    })
  }
  async function test2(){
    const result =await test()
    console.log("----",result)
  }
  test2()

新建config文件夹,并新建webpack.config.js文件

./config/webpack.config.js

const path = require("path");
module.exports = {
  entry: path.resolve(__dirname, "../src/main.js"), // 入口文件,打包从这个文件开始
  devtool:false,
  output: {
    path: path.resolve(__dirname, "../dist"),//出口文件,打包生成的文件放置到这个文件夹下
    clean: true,
    filename: "./js/[name].[chunkhash].js"		//打包成的文件名。name取的原始文件名,chunkhash生成哈希值,这样每次打包出来不一样,避免浏览器缓存读取旧文件。
  },
  mode: "development"  //开发模式
};

在package.json的script中添加命令

"scripts": {
    "build": "webpack --config ./config/webpack.config.js  --progress --color "
 },

安装html插件

npm install html-webpack-plugin -D

新建public文件夹并创建index.html文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

配置webpack:

 const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...其他配置
     plugins: [
        new HtmlWebpackPlugin({
          template: "./public/index.html", //用来做模板的html的文件路径(从项目根目录开始)
          filename: "index.html", //生成的html的名字
          title:'webpack5的项目配置',//这个就对应上文的title
          inject: "body" //打包出来的那个js文件,放置在生成的body标签内
        })
      ],
}

二,测试es6的代码打包情况

然后 运行npm run build,查看打包出来的文件:

/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!*********************!*\
  !*** ./src/main.js ***!
  \*********************/

  function test(){
    return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve(22)
      },0)
    })
  }
  async function test2(){
    const result =await test()
    console.log("----",result)
  }
  test2()

/******/ })()
;

可以看到,es6的代码并没有转化成es5。

三,理解babel

这时候就需要引入babel。

Babel是一个工具集,主要用于将ES6版本的JavaScript代码转为ES5等向后兼容的JS代码,从而可以运行在低版本浏览器或其它环境中。

我们通常说的babel7啊,babel6啊,这个包实际上说的是@babel/core这个核心库的版本。

下面列出的是 Babel 能为你做的事情:

语法转换:将es6+转化为es5语法
补齐API:通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块),比如说给ie浏览器增加Promise

babel只是提供了一个“平台”,让更多有能力的plugins入驻这个平台,是这些plugins提供了将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法的能力。

在加入plugins测试之前我们需要知道一些前置知识,babel将ECMAScript 2015+ 版本的代码分为了两种情况处理:

  • 语法层: let、const、class、箭头函数等,这些需要在构建时进行转译,是指在语法层面上的转译
  • api方法层:Proxy,Promise、includes、map等,这些是在全局或者Object、Array等的原型上新增的方法,它们可以由相应es5的方式重新定义

babel对这两种情况的转译是不一样的,我们需要给出相应的配置。

四,语法转换

1,引入@babel/preset-env

对于预设的选择,Babel插件实在太多,假如只配置插件数组,那我们前端工程要把ES2015,ES2016,ES2017…下的所有插件都写到配置项里,我们的Babel配置文件会非常臃肿。

preset预设就是帮我们解决这个问题的。Babel7.8官方的插件和预设目前有100多个,实际发展至今(2022-05-27),Babel官方的preset,我们实际可能会用到的其实就只有4个:

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

一个普通的vue工程,Babel官方的preset只需要配一个"@babel/preset-env"就可以了。所以这里我就只引入@babel/preset-env。

安装基本的babel:

npm install  babel-loader @babel/core @babel/preset-env -D

这里讲一下,为啥是这三个依赖:

1.babel-loader:因为我是用的webpack,要让webpack允许babel来处理js代码。如果不用webpack,需要安装@babel/cli这个依赖。
2.@babel/core:babel的核心库,我们平时说的babel,就是说的它,他提供一个平台,可以拔插想要的预设和插件。实际上实现代码转换的是这些预设和插件,而不是babel本身。
3.@babel/preset-env:它是babel之前一大堆预设的超集,可以对标准的es6语法进行转化。这里安装它就是让它来进行es6转成es5的。

配置对应的webpack:

 module: {
    rules: [{
        test: /\.js$/,
        use: {
          loader: "babel-loader"
        },
        exclude: /node_modules/
	}]
}

然后新建babel.config.js文件:

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

2,Babel 配置文件的理解

Babel的配置文件是Babel执行时默认会在当前目录寻找的文件,主要有.babelrc,.babelrc.js,babel.config.js和package.json。它们的配置项都是相同,作用也是一样的,只需要选择其中一种。

plugin代表插件,preset代表预设,它们分别放在plugins和presets,每个插件或预设都是一个npm包。

plugins插件数组和presets预设数组是有顺序要求的。如果两个插件或预设都要处理同一个代码片段,那么会根据插件和预设的顺序来执行。规则如下:

  • 插件比预设先执行
  • 插件执行顺序是插件数组从前向后执行
  • 预设执行顺序是预设数组从后向前执行

每个插件是插件数组的一成员项,每个预设是预设数组的一成员项,默认情况下,成员项都是用字符串来表示的,例如"@babel/preset-env"。

如果要给插件或预设设置参数,那么成员项就不能写成字符串了,而要改写成一个数组。数组的第一项是插件或预设的名称字符串,第二项是个对象,该对象用来设置第一项代表的插件或预设的参数。例如给@babel/preset-env设置参数:

const presets = [
  [
    "@babel/preset-env",
    {
      targets: {
        chrome: "58",
        ie: "11"
      },
      useBuiltIns: "usage",
      corejs: 2 // 新版本的@babel/polyfill包含了core-js@2和core-js@3版本,所以需要声明版本,否则webpack运行时会报warning
    }
  ]
];

再次打包之后,虽然会发现代码中的箭头函数,const,let等es6语法没掉了,打包出来的js还是包含箭头函数(在打包文件的最开头,这个是因为本文使用webpack5,webpack5之后打包默认使用ecma6了)。而其中还包含Promise,map等,是因为它们属于api而不是语法,目前执行到这一步,是因为只使用了@babel/preset-env,babel只会进行语法的转化,而不会进行api的补齐

image-20220525230322749

接下来讲一下,为啥@babel/preset-env我们并没有设置任何参数,打包出来的代码却已经完成了es5的转化?

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

  "browserslist": [
    "> 0.1%",
    "last 2 versions",
    "ie>=9"
  ]

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

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

如下设置,则转化出来的代码,不会再有箭头函数,const,let等es6的语法:

  "browserslist": [
    "> 0.1%",
    "last 2 versions",
    "ie>=9"
  ]

2,preset-env配置target参数

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

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

const presets = [
  [
    "@babel/preset-env",
    {
      targets: {
        chrome: "58",
        ie: "9"
      }
    }
  ]
];

如果我们对@babel/preset-env的targets参数项进行了设置,那么就不使用browserslist的配置,而是使用targets的配置。

正常情况下,我们推荐使用browserslist的配置而很少单独配置@babel/preset-env的targets。因为browserslist还会有其他插件使用,这样可以保持统一。

这时候,我们打包项目出来,会发现语法层面已经完成了转化,代码在高版本的浏览器中可以正常打开,但是在低版本浏览器或者ie中打开:

image-20220605171857792

这是因为目前为止,我们只做了语法转换,而Promise等api,在ie中是没有的,需要补齐。

五,API补齐

1,理解为什么需要做api补齐

虽然上文@babel/preset-env做了语法翻译,但在低版本浏览器还是没有比如Promise、数组的map等。所以不仅要使用@babel/preset-env进行ES6转ES5,还要借助 @babel/polyfill把缺失的变量或者函数补充到低版本的浏览器里。从而让低版本的浏览器也可以支持promise这些新的API。

新的API分类两类,一类是Promise、Map、Symbol、Proxy、Iterator等全局对象及其对象自身的方法,例如Object.assign,Promise.resolve;另一类是新的实例方法,例如数组实例方法[1, 4, -5, 10].find((item) => item < 0)。

如果想让ES6新的API在低版本浏览器正常运行,我们就不能只做语法转换。

在前端web工程里,最常规的做法是使用polyfill,为当前环境提供一个垫片。所谓垫片,是指垫平不同浏览器之间差异的东西。polyfill提供了全局的ES6对象以及通过修改原型链Array.prototype等实现对实例的实现。

polyfill广义上讲是为环境提供不支持的特性的一类文件或库,狭义上讲是polyfill.js文件以及@babel/polyfill这个npm包。

我们可以直接在html文件引入polyfill.js文件来作为全局环境垫片, polyfill.js 有Babel官方的 polyfill.js,也有第三方的。我们引入一个Babel官方已经构建好的polyfill脚本。

简单起见,我们通过在html里引入polyfill.js的方式。

<script src="https://cdn.bootcss.com/babel-polyfill/7.6.0/polyfill.js"></script>

然后npm run build,ie中打开项目,就可以发现,代码执行了,且没有报错。

image-20220525220352380

补齐API的方式除了通过引入 polyfill.js 文件 ,还有通过在构建工具入口文件(例如webapck),babel配置文件等方式进行。这里讲的通过在HTML里直接引入 polyfill.js 文件 这种方式进行在现代前端工程里逐渐淘汰,很少使用了。但通过这一步,可以很明显地知道,确实是它为我们的ie构建了es6的垫片。

于是,就可以在webpack中引入polyfill.js了。

2,引入@babel/polyfill

先将cdn引入的polyfill.js去除。

这里,我先用@babel/polyfill演示一下使用。

安装@babel/polyfil,值得注意的是,因为它是需要给浏览器增加垫片的,所以它是生产需要的依赖。

 npm install --save @babel/polyfill

配置babel.config.js文件,主要是preset-env的useBuiltIns参数设置:

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

(一),preset-env的useBuiltIns为false或者不设置

useBuiltIns这个参数项主要和polyfill的行为有关。在我们没有配置该参数项或是取值为false的时候,如果项目的入口文件第一行有引入polyfill:

import '@babel/polyfill';
  function test(){
    return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve(22)
      },0)
    })
  }
  async function test2(){
    const result =await test()
    console.log("----",result)
  }
  test2()

然后修改babel.config.js:

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

polyfill就是我们上文cdn引入一样,会全部引入到最终的代码里。

(二),preset-env的useBuiltIns为entry

useBuiltIns取值为"entry"的时候,不再全部引入,而是会根据配置的目标环境找出需要的polyfill进行部分引入。

同样的,这种方法需要在入口文件引入polyfill:

import '@babel/polyfill';
function test(){
    return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve(22)
      },0)
    })
  }
  async function test2(){
    const result =await test()
    console.log("----",result)
  }
  test2()

然后修改babel.config.js配置:

const presets = [[
  "@babel/preset-env",
  {
    useBuiltIns: 'entry',
    corejs: 2//@babel/polyfill包含了core-js@2,需要声明下使用哪个版本,否则默认是2,并且会warning,如果要用3,则必须再安装npm install core-js@3 -S
  }
]];
const plugins = [];
module.exports = {
  plugins,
  presets
};

(注意:corejs只有在useBuiltIns取值为"entry"或"usage"的时候才会生效。)

这种方式,polyfill会考虑目标环境缺失的API模块引入到文件中。

(三),preset-env的useBuiltIns为usage

useBuiltIns取值"usage"的时候,会根据配置的目标环境找出需要的polyfill进行部分引入。

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

入口文件:

function test(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve(22)
    },0)
  })
}
async function test2(){
  const result =await test()
  console.log("----",result)
}
test2()

babel.config.js:

const presets = [[
  "@babel/preset-env",
  {
    useBuiltIns: 'usage',
    corejs: 3//@babel/polyfill包含了core-js@2,需要声明下使用哪个版本,否则默认是2,并且会warning,如果要用3,则必须再安装npm install core-js@3 -S
  }
]];
const plugins = [];
module.exports = {
  plugins,
  presets
};

注意,如果corejs取值为3,必须安装并引入core-js@3版本才可以,否则Babel会转换失败并提示

安装

npm install core-js@3 -S

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

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

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

可以看到,ie中代码正常运行,且无报错:

image-20220525235040228

至此,引入引入@babel/polyfill实现的低版本浏览器适配告一段落。

3,不使用@babel/polyfill

从babel7.4开始,官方不推荐再使用@babel/polyfill了,因为@babel/polyfill本身其实就是两个npm包的集合:core-js与regenerator-runtime。

因此从2019年年中开始,我们的新项目都应该使用core-js和regenerator-runtime这两个包。

对比2中使用@babel/polyfill的做法,不同的地方仅仅是移除@babel/polyfill,安装下面两个依赖:

 npm install --save core-js@3 regenerator-runtime

babel.config.js的配置和上文一样即可。

注意:这里core-js@3是因为我的babel.config.js中配置的3。

打包运行,可以看到ie中照样可以:

image-20220605172928011

4,这种方法的缺点

(一),修改了全局变量的原型

babel 的 polyfill 机制是,对于例如 Array.from 等静态方法,直接在 global.Array 上添加;对于例如 includes 等实例方法,直接在 global.Array.prototype 上添加。这样直接修改了全局变量的原型,有可能会带来意想不到的问题。

比如说,假设一个 npm 组件开发的的作者,开发时刚好用到了 Promise 方法, 按照上面的方法,写完发布到 npm 仓库,现在你在写业务开发时需要使用这个包,下载下来了,但是他的项目里面也用到了 Promise ,但是他的 Promise 是 自定义的 一套,和你使用的polyfill改写的不一致。这就会导致你的项目跑不起来。

(二),转义语法时,辅助函数重复引入

class 语法中,babel 自定义了 _classCallCheck这个函数来辅助;typeof 则是直接重写了一遍,自定义了 _typeof 这个函数来辅助。这些函数叫做 helpers。从上图中可以看到,helper 直接在转译后的文件里被定义了一遍。如果一个项目中有100个文件,其中每个文件都写了一个 class,那么这个项目最终打包的产物里就会存在100个 _classCallCheck 函数,他们的长相和功能一模一样,这显然不合理。会使打出来的包特别大。

六,@babel/plugin-transform-runtime提供辅助函数的自动引入功能

为了方便演示,这里采用@babel/cli的方式处理js,暂时不使用webpack。安装@babel/cli:

npm i @babel/cli -D

现在我们沿用第六节第三步的配置:

babel.config.js

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

api补齐不再采用@babel/polyfill,二是使用core-js和regenerator-runtime这两个包。

修改main.js:

class Person {
  sayname() {
    return "name";
  }
}
var john = new Person();
console.log(john);

然后npx babel ./src/main.js -o b.js查看生成的文件:

"use strict";

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

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); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }

var Person = /*#__PURE__*/function () {
  function Person() {
    _classCallCheck(this, Person);
  }

  _createClass(Person, [{
    key: "sayname",
    value: function sayname() {
      return "name";
    }
  }]);

  return Person;
}();

var john = new Person();
console.log(john);

可以看到转换后的代码上面增加了好几个函数声明,这就是注入的函数,我们称之为辅助函数。@babel/preset-env在做语法转换的时候,注入了这些函数声明,以便语法转换后使用。

但样这做存在一个问题。在我们正常的前端工程开发的时候,少则几十个js文件,多则上千个。如果每个文件里都使用了class类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会导致我们用构建工具打包出来的包非常大。

那么怎么办?一个思路就是,我们把这些函数声明都放在一个npm包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过webpack这一类的构建工具打包的时候,我们只会把使用到的npm包里的函数引入一次,这样就做到了复用,减少了体积。

@babel/runtime就是上面说的这个npm包,@babel/runtime把所有语法转换会用到的辅助函数都集成在了一起。

打开node_modules@babel\runtime\helpers\esm文件夹,就可以在这里找到所有的辅助函数,对于上文用到的_classCallCheck、_defineProperties、_createClass也在其中。

当我们把它改成手动从这里引入也行。

"use strict";

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

var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var _defineProperty = require("@babel/runtime/helpers/defineProperty");
var _createClass = require("@babel/runtime/helpers/createClass");

var Person = /*#__PURE__*/ (function () {
  function Person() {
    _classCallCheck(this, Person);
  }

  _createClass(Person, [
    {
      key: "sayname",
      value: function sayname() {
        return "name";
      }
    }
  ]);

  return Person;
})();

var john = new Person();
console.log(john);

这样就解决了代码复用和最终文件体积大的问题。不过,这么多辅助函数要一个个手动引入,很是麻烦。这个时候,Babel插件@babel/plugin-transform-runtime就可以来帮助我们自动引入对应的辅助函数。

于是就可以引入@babel/plugin-transform-runtime以及@babel/runtime,这两个插件的作用分别是:

@babel/runtime:将所有文件用到的公用辅助函数抽离出来形成一个第三方文件。
@babel/plugin-transform-runtime:自动识别哪些文件使用了哪些公用辅助函数,自动在那些文件中引入辅助函数。

如何使用呢?其实@babel/preset-env已经包含了@babel/runtime,但是我们使用时,最好还是重新安装下:

npm install --save-dev  @babel/plugin-transform-runtime @babel/runtime

然后修改babel.config.js配置即可:

const presets = [
  [
    "@babel/preset-env",
    {
      useBuiltIns: "usage",
      corejs: 3
    }
  ]
];
const plugins = ["@babel/plugin-transform-runtime"];
module.exports = {
  plugins,
  presets
};

也就是插件加上@babel/plugin-transform-runtime,参数使用它的默认参数。@babel/plugin-transform-runtime这个插件其实有很多作用,但使用默认参数配置时,将只发挥它自动删除文件中的辅助函数并且从node_modules@babel\runtime\helpers\esm中引入对应的辅助函数的作用。

这时再运行:npx babel ./src/main.js -o b.js,将会得到b文件:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _classCallCheck = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var Person = /*#__PURE__*/function () {
  function Person() {
    (0, _classCallCheck2.default)(this, Person);
  }

  (0, _createClass2.default)(Person, [{
    key: "sayname",
    value: function sayname() {
      return "name";
    }
  }]);
  return Person;
}();

var john = new Person();
console.log(john);

可以看到,它帮助我们自动引入了辅助函数。

至于@babel/plugin-transform-runtime关于API转换的功能,则需要参数配置之后才能开启,因为我们是业务代码的开发者,所以采用的就是polyfill补齐api的方案,于是第一个修改了全局变量原型的缺点,暂时不处理,下一篇文章具体讲一下这个。

于是到这里我们这时候就能得到,业务开发者最佳的babel配置。

七,业务开发者最佳的babel配置

在第六点时,其实已经实现了低版本浏览器的兼容,但是不是很完美,作为业务开发者而言,我们是可以接受全局变量的原型被polyfill改写的。但是为了包足够小,会想要解决第二个缺点:辅助函数重复引入的问题。

于是babel.config.js配置:

const presets = [
  [
    "@babel/preset-env",
    {
      useBuiltIns: "usage",
      corejs: 3
    }
  ]
];
const plugins = ["@babel/plugin-transform-runtime"];
module.exports = {
  plugins,
  presets
};

package.json

{
  "name": "babeltest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --config ./config/webpack.config.js  --progress --color "
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.18.2",
    "@babel/plugin-transform-runtime": "^7.18.2",
    "@babel/preset-env": "^7.18.2",
    "@babel/runtime": "^7.18.3",
    "babel-loader": "^8.2.5",
    "html-webpack-plugin": "^5.5.0",
    "webpack": "^5.72.1",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.9.0"
  },
  "browserslist": [
    "> 0.1%",
    "last 2 versions",
    "ie>=9"
  ],
  "dependencies": {
    "core-js": "^3.22.7",
    "regenerator-runtime": "^0.13.9"
  }
}

webpack配置:

rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader"
        },
        exclude: /node_modules/
      }
    ]

第一步,我们是安装了@babel/core,babel-loader,@babel/preset-env,让webpack允许babel处理js代码。我们在babel.config.js中增加了@babel/preset-env预设,然后package中设置了browserslist,于是babel就会根据这个browserslist,把项目代码中的es6语法转化为es5的语法。

第二步,本来要安装@babel/polyfill的,但是babel7.4之后,它拆解为core-js@3和regenerator-runtime两个包了。在安装了这两个包之后。我们给@babel/preset-env设置两个属性,将需要用到的api按需引入,这样就能在低版本浏览器补齐Promise,map等es6的API了。

第三步,安装@babel/runtime和@babel/plugin-transform-runtime,前者其实是babel完成语法转化过程中会用到的全部辅助函数。后者则是一个插件,它能把babel处理后的文件中的辅助函数删除,然后自动引入@babel/runtime中的辅助函数。这样辅助函数就只有一份。包就会小很多。

注:这几天看了网上很多这方面的文章,感觉这位大佬讲得最好,本文也有很多内容是复制这里的。 Babel 教程 - 姜瑞涛的官方网站 (jiangruitao.com)