三石的webpack(babel篇)

800 阅读13分钟

建议有一定基础后再阅读 官方文档

本文会先介绍 Babel 及其原理,然后介绍如何在 webpack 中配置 Babel,最后具体解释 webpack 相关插件的配置

Babel是什么

Babel 是一个Javscript编译器,可以将高级语法(主要是ECMAScript 2015+ )编译成浏览器支持的低版本语法,它可以帮助你用最新版本的Javascript写代码,提高开发效率。主要分为两类:语法部分和API部分

Babelpostcss 很像,是一个工具集,他自己不会做任何事情,需要依靠他自身的插件。
Babel 也是一个可以用到浏览器集合的工具。

一个完整的Babel转码工程通常包括如下:

  • Babel配置文件
  • Babel相关的npm包
  • 需要转码的JS文件

Babel编译

Babel 编译分为三个阶段:

  • 解析(Parsing):将代码字符串解析成抽象语法树。
  • 转换(Transformation):对抽象语法树进行转换操作。
  • 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。

具体编译可以参考 这篇文章

预设和插件

Babel 有丰富的预设和插件,它的配置可以直接写到options里或者单独写道配置文件里。

其配置里的两大核心:插件数组(plugins) 和 预设数组(presets)。 其中 presets 可以被看作是一组 plugins 的集合

为什么需要 presets:
Bable 中是通过各种 plugins 来指导如何进行代码转换,我们需要转换哪些新的语法,都可以将相关的 plugins 一一列出,理论上只要有 plugins 我们就能正常使用 Babel。
但是这其实非常复杂,因为我们往往需要根据兼容的浏览器的不同版本来确定需要引入哪些插件,为了解决这个问题,babel给我们提供了预设插件组,可以根据选项参数来灵活地决定提供哪些插件。

常用 presets:

  • @babel/preset-env              es6+ 语法
  • @babel/preset-typescript    TypeScript
  • @babel/preset-react            React
  • @babel/preset-flow              Flow

插件和预设的执行顺序:

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

短名称:
插件和预设都可以使用相应短名称,但除了常用的几个,其他还是推荐全名称

webpack 中使用 Babel

webpack 通过 babel-loader 使用 Babel

安装

# 环境要求: 
webpack 4.x || 5.x | babel-loader 8.x | babel 7.x 

# 安装依赖包: 
npm install -D babel-loader @babel/core @babel/preset-env webpack

只要使用 babel-loader,那么一定要安装 babel-loader 和 @babel/core;因为 @babel/core是babel核心库,所有的核心api都在这里面。babel-loader依赖 @babel/core

@babel/preset-env 是用来处理 es6+ 语法的兼容问题,例如 箭头函数,如果没有它,也可以完成转码,但转码后的代码仍然是ES6的,相当于没有转码

webpack.config.js 配置

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', { targets: "defaults" }]
              ],
            }
          }
        ]
      },
    ]
  }

.babelrc 配置

和他许多其他工具都有类似的配置文件一样,如 .eslintrc.prettierrc,上述 babel 相关配置,我们也可以单独提取到 .babelrc

    {
        presets: [
            '@babel/preset-react',
            [
                '@babel/preset-env', {
                    useBuiltIns: 'usage', // 
                    corejs: '2',
                    targets: {
                        chrome: '70',
                        ie: '11'
                    }
                }
            ]
        ],
        plugins: [
            '@babel/plugin-transform-react-jsx',
            '@babel/plugin-proposal-class-properties'
        ]
    };

除了 .babelrc,还可以使用 babelrc.js 或 babel.config.js 配置文件,亦或是直接将配置写到 package.json 中,它们效果都是一样的

命令行使用 Babel

有时候,我们需要使用命令行来执行Babel

# 安装依赖包: 
npm install -D @babel/cli @babel/core @babel/preset-env

@babel/cli是Babel命令行转码工具
@babel/cli依赖@babel/core,因此也需要安装@babel/core这个Babel核心npm包
@babel/preset-env 已经说过了,es6 -> es5

# 执行babel:
npx babel main.js -o compiled.js

@babel/preset-env

@babel/preset-env 预设除了包含所有稳定的转码插件,还可以根据设置参数项进行针对性语法转换以及polyfill的部分引入

@babel/preset-env的参数项,数量有10多个,但大部分我们要么用不到,要么已经或将要弃用。目前常用的是 targets、useBuiltIns、modules和corejs 这四个

基本写法

  • @babel/preset-env 可以简写成 @babel/env
// .babelrc
{
  "presets":  ["@babel/env"],  
  "plugins": []
}
  • 如果需要对某个preset设置参数,该preset就不能以字符串形式直接放在presets的数组项了。而是应该再包裹一层数组,数组第一项是该preset字符串,数组第二项是该preset的参数对象
// .babelrc
{
  "presets":  [["@babel/env", {}]],  
  "plugins": []
}

targets

browserslist是什么:

如果使用过vue或react的官方脚手架cli工具,那么就会看到 package.json里看到browserslist项,例如:

"browserslist": [
  "> 1%",
  "not ie <= 8"
]

上面的配置含义是,目标环境是市场份额大于1%的浏览器并且不考虑IE8及以下的IE浏览器。

我们用 browserslist 来指定代码最终要运行在哪些浏览器或node.js环境。Autoprefixer、postcss等就可以根据我们的browserslist,来自动判断是否要增加CSS前缀(例如'-webkit-')。

browserslist除了写在package.json里,也可以单独写在 .browserslistrc 里。

babel 与 browserslist:

Babel也可以使用browserslist,如果你使用了@babel/preset-env这个预设,此时Babel就会读取browserslist的配置。

targets作用:

如果设置了 targets,那么 babel 就不使用 browserslist 配置,而是使用 targets 配置。如果targets不配置,browserslist也没有配置,那么@babel/preset-env就对所有ES6语法转换成ES5的。

正常情况下,我们推荐使用browserslist的配置而不是targets

useBuiltIns

useBuiltIns这个参数项主要和polyfill的行为有关

取值:
"usage" || "entry" || false 默认值

  • false:polyfill就会全部引入。设置后,需要手动引入 polyfill
  • entry:会根据配置的目标环境引入需要的polyfill。设置后,需要在项目入口处手动引入 polyfill
  • usage:会根据配置的目标环境,并考虑项目代码里使用到的ES6特性引入需要的polyfill,设置后,不需要手动引入 polyfill

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

corejs

这个参数项只有useBuiltIns设置为'usage'或'entry'时,才会生效。

取值:
3 || 2 默认值

取值为2:
Babel转码的时候使用的是core-js@2版本。
此时需要安装并引入core-js@2版本,或者直接安装并引入polyfill也可以。
取值为3:
某些新API只有core-js@3里才有,例如数组的flat方法,这时候就要设置为3引入 core-js@3 的API模块进行补齐。
此时必须安装并引入 core-js@3 版本才可以,否则Babel会转换失败并提示:

`@babel/polyfill` is deprecated. Please, use required parts of `core-js` and `regenerator-runtime/runtime` separately

modules

该项用来设置是否把ES6的模块化语法改成其它模块化语法

取值:
"amd" || "umd" || "systemjs" || "commonjs" || "cjs" || false || "auto"默认值

在该参数项值是 'auto' 或不设置的时候,会发现我们转码前的代码里es6 的 import都被转码成 commonjs 的 require了

polyfill

polyfill 是什么

Babel 只是提供了语法转换的规则,但是不同浏览器对新特性支持程度不一样的,它并不能弥补浏览器缺失的一些新的功能,如一些内置的方法和对象,如Promise,Array.from等。为了能将新API转成浏览器原生可执行的代码,这就用到了polyfill(垫片)技术

所谓垫片,是指垫平不同浏览器之间差异的东西。polyfill广义上讲是为环境提供不支持的特性的一类文件或库,既有Babel官方的库,也有第三方的

polyfill 如何起作用的

polyfill 的垫片是在全局变量上挂载目标浏览器缺失的功能,这样做的缺点很明显,是可能会造成全局污染。因此我们有代替polyfill的解决方案,会在后面章节讲到。

polyfill 使用方法(了解就好):

  1. 直接在html文件引入Babel官方的polyfill.js脚本文件;
  2. 在前端工程的入口文件里引入polyfill.js;
  3. 在前端工程的入口文件里引入@babel/polyfill;
  4. 在前端工程的入口文件里引入core-js/stable与regenerator-runtime/runtime;
  5. 在前端工程构建工具的配置文件入口项引入polyfill.js;
  6. 在前端工程构建工具的配置文件入口项引入@babel/polyfill;
  7. 在前端工程构建工具的配置文件入口项引入core-js/stable与regenerator-runtime/runtime;

上述引入方法参考

@babel/polyfill

功能:

@babel/preset-env 只是提供了语法转换的规则,@babel/polyfill 通过该写全局prototype的方式实现挂载目标浏览器缺失的功能,补齐转换功能

@babel/polyfill 是babel 官方提供的polyfill,本质是由两个npm包core-js与regenerator-runtime组合而成的,所以在使用层面上还可以再细分为是引入@babel/polyfill本身还是其组合子包

安装:

这个包是在运行时起作用的,所以要安装到生产依赖里。

npm install --save @babel/polyfill

使用:

entry 入口处,导入该依赖

// webpack.config.js

module.exports = {
  entry: [
    '@babel/polyfill', // require('@babel/polyfill')
    './app'
  ]
}

也可以直接在entry文件里导入,这样就不用在 webpack.config.js 里引入了

// index.js

import "@babel/polyfill";

const App = () => {
  ...
}

上述两种 @babel/polyfill 使用,会把你目标浏览器中缺失的所有es6的新的功能都做垫片处理。但它的体积很大,这样我们没有用到的那部分功能的转换其实是无意义的,造成打包后的体积无谓的增大

推荐使用:

我们可以在 presets 的选项里配置 "useBuiltIns": "usage",此时也不需要我们单独引入 import @babel/polyfill 了,它会在使用的地方自动注入。

// .babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage"
      }
    ]
  ]
}

推荐使用:

除此之后,我们在开发库的时候,往往使用 @babel/plugin-transform-runtime + @babel/runtime

接下来章节就会讲到

@babel/runtime

作用:

我们使用 babel 做语法转换时,配置文件

  {
    "presets": [
      "@babel/env"
    ],
    "plugins": [
    ]
  }

此时我们会发现,每个js文件,上方都会注入babel的辅助函数,用来转换 es6 语法。这就导致了,如果有很多个js,那么这些辅助函数就会重复注入到各个js文件里。这会导致我们用构建工具打包出来的包非常大。

@babel/runtime 包把所有语法转换会用到的辅助函数都集成到一起,比如 _classCallCheck 函数,用来保证类不能直接当做一个普通函数调用 安装:

npm install --save @babel/runtime

npm install --save-dev @babel/cli @babel/core @babel/preset-env

问题:

我们看到, @babel/runtime 只是集中了辅助函数定义,但如何在webpack打包的时候,自动引入呢?这就需要 @babel/plugin-transform-runtime

@babel/plugin-transform-runtime

作用

  • 自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代;
  • 当代码里使用了core-js的API,自动引入@babel/runtime-corejs3/core-js-stable/,以此来替代全局引入的core-js/stable;
  • 当代码里使用了Generator/async函数,自动引入@babel/runtime/regenerator,以此来替代全局引入的regenerator-runtime/runtime;

作用1我们已经了解了,与 @babel/runtime 配合使用,避免了重复定义辅助函数;那么作用2和作用3是干嘛的呢?

我们知道,@babel/polyfill 是在全局上挂载API的,那么它的使用可能会造成全局污染。因此在开发类库、第三方模块或者组件库时,就不能再使用 @babel/polyfill了,此时应该使用transform-runtime。transform-runtime的转换是非侵入性的,也就是它不会污染你的原有的方法。遇到高级API它会做API转换,给API另起一个名字后使用,不影响业务代码

安装

npm install --save-dev @babel/plugin-transform-runtime @babel/cli @babel/core @babel/preset-env

npm install --save @babel/runtime-corejs3
  {
    "presets": [
      "@babel/env"
    ],
    "plugins": [
      ["@babel/plugin-transform-runtime", {
        "corejs": 3  // 因为装的 @babel/runtime-corejs3
      }]
    ]
  }

@babel/runtime-corejs3 是什么?
我们注意到,本章节中,我们安装的 @babel/runtime-corejs3 ,而上章节讲的是 @babel/runtime,两者有什么不同呢?

在我们不需要开启core-js相关API转换功能的时候,我们只需要安装@babel/runtime就可以了。
在我们需要开启core-js相关API转换功能的时候,就需要安装@babel/runtime的进化版@babel/runtime-corejs3。这个npm包里除了包含Babel做语法转换的辅助函数,也包含了core-js的API转换函数。

除了这两个包,还有一个@babel/runtime-corejs2的包。它和@babel/runtime-corejs3的功能是一样的,只是里面的函数是针对core-js2版本的。

配置

@babel/plugin-transform-runtime 有许多配置项,当不设置配置项时,它的默认配置如下:

  { 
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "helpers": true,
          "corejs": false,
          "regenerator": true,
          "useESModules": false,
          "absoluteRuntime": false,
          "version": "7.0.0-beta.0"
        }
      ]
    ]
  }
  • helpers 该项是用来设置是否要自动引入辅助函数包,基本都是为 true 的,不然作用1就废了
  • corejs/regenerator 该项设置是否做API转换以避免污染全局环境,regenerator取值是布尔值,corejs取值是false、2和3(对应装的runtime版本)。在前端业务项目里,我们一般对corejs取false,即不对Promise这一类的API进行转换。而在开发JS库的时候设置为2或3。regenerator取默认的true
  • useESModules 该项设置是否使用ES6的模块化用法。在用webpack一类的打包工具的时候,我们可以设置为true,以便做静态分析。
  • absoluteRuntime 该项用来自定义@babel/plugin-transform-runtime引入@babel/runtime/模块的路径规则,取值是布尔值或字符串
  • version 该项主要是和@babel/runtime及其进化版@babel/runtime-corejs2、@babel/runtime-corejs3的版本号有关系,这三个包我们只需要根据需要安装一个。我们把安装的npm包的版本号设置给version即可。该项一般不填取默认值,填写版本号主要是为了减少打包体积。

小结

  • 要使用@babel/plugin-transform-runtime插件,只有@babel/plugin-transform-runtime是必须要装的
  • 对于@babel/runtime及其进化版@babel/runtime-corejs2、@babel/runtime-corejs3,我们只需要根据自己的需要安装一个,并设置对应的 corejs 选项
  • 在安装@babel/preset-env的时候,其实已经自动安装了@babel/runtime,不过在项目开发的时候,我们一般都会再单独npm install一遍@babel/runtime

解惑

  1. 每个转换后的文件上部都会注入这些相同的函数声明,那为何不用webpack一类的打包工具去掉重复的函数声明,而是要单独再引一个辅助函数包 @babel/runtime ?

答:webpack在构建的时候,是基于模块来做去重工作的。每一个函数声明都是引用类型,在堆内存不同的空间存放,缺少唯一的地址来找到他们。所以webpack本身是做不到把每个文件的相同函数声明去重的。因此我们需要单独的辅助函数包,这样webpack打包的时候会基于模块来做去重工作。

  1. 通过polyfill补齐API的方式也可以使代码在浏览器正常运行,为什么还需要 @babel/plugin-transform-runtime 来转换API?

答:API转换主要是给开发JS库或npm包等的人用的,业务工程一般仍然使用polyfill补齐API。可以想象,如果开发JS库的人使用polyfill补齐API,我们前端工程也使用polyfill补齐API,但JS库的polyfill版本或内容与我们前端工程的不一致,那么我们引入该JS库后很可能会导致我们的前端工程出问题。所以,开发JS库或npm包等的人会用到API转换功能。

参考

Babel教程