灏天阁

webpack5之Babel/ESlint/浏览器兼容

· Yin灏

Babel 用法

在实际开发中我们很少直接去接触 babel,但是 babel 对于前端开发来说又是必不可少的。Babel 到底是什么呢,它其实是一个工具链,跟 postcss 一样,它能够将 ECMAScript 后版本语法代码转为 ES5 代码,包括:语法转换、源代码转换、Polyfill 实现功能。

babel 核心安装

我们来安装babel核心库@babel/core,如果我们想要在命令行使用需要安装@babel/cli,安装npm install @babel/core @babel/cli。我们如果想使用 babel 的功能,就需要安装 bable 的插件,我们可以使用 babel 的预设插件@babel/preset-env,安装npm install @babel/preset-env -D

编写一个 main.js 文件

Snipaste_2022-03-27_16-14-24.png

执行命令

npx babel src --out-dir dist --presets=@babel/preset-env

生成结果

Snipaste_2022-03-27_16-14-42.png

可以看到输出文件给我们转换成了 ES5 的语法。

babel-loader

在实际开发中我们会使用babel-loader来配置我们的 babel,安装npm install babel-loader -D。配置如下

{
  test: /\.(j|t)sx?$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env', {
          targets: 'last 2 version'  // 配置的targets属性会覆盖browserslist,实际推荐在browserslist中配置
        }]
      }
    }
  ]
}

babel 配置文件

除了在 loader 中配置 babel 预设插件,一般情况会独立抽取一个 babel 配置文件,官方为我们提供了两种编写方法

  • babel.config.json(或者.js.cjs.mjs)文件
  • .babelrc.json(或者.babelrc.js.cjs.mjs)文件

实际推荐使用babel.config.json/js ,我们新建一个babel.config.json

// babel.config.json
{
  "presets": [["@babel/preset-env"]]
}

Polyfill

现在我们修改一下 main.js,我们使用一个 promise Api,并重新打包

// main.js
const a = "123";
const fn = Promise.resolve(3);
fn.then((res) => {
  console.log(res);
});

Snipaste_2022-03-27_17-50-13.png

现在可以看到打包结果中并没有帮我们将 Promise 进行转换,那么这就有可能在一些低版本或者旧的浏览器上不支持 Promise。因此我们提出了一个Polyfill的东西,我们使用了一些语法新特性(例如:Promise, Generator, Symbol 等以及实例方法 Array.prototype.includes 等),它将会将给我们进行填充,就是将这些 Api 进行转换,用旧版本浏览器认识的语法 api 去实现这些新特性。那么我们该如何使用呢。

在之前我们是通过安装@babel/polyfill这个库来实现的,我们会之间在入口文件头顶引入。但是现在我们已经不再使用了,在 babel7.4.0 之后,是引入core-jsregenerator-runtime这两个依赖来提供polyfill功能。安装npm install core-js regenerator-runtime。并且需要在 @babel/preset-env 后添加额外属性

  • useBuiltIns: 设置以什么样的方式来使用 polyfill,它有三个值false,usage,entry
    • false: 不使用任何 polyfill 有关的代码
    • usage: 代码中需要哪些 polyfill,就会自动引用相关的 API(推荐)
    • entry: 代码中需要哪些 polyfill,需要在入口加入 import “core-js/stable”; import “regenerator-runtime/runtime”; 体积会变大
  • corejs: 设置corejs的版本,目前使用较多的是 3.x 的版本
    • 另外 corejs 可以设置是否对提议阶段的特性进行支持
    • 设置 proposals 属性为 true 即可

babel.config.json 配置如下所示

// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

Plugin-transform-runtime

除了上述的 polyfill 作用的是全局的,所以当我们在编写工具库的时候,如果我们使用上述 babel polyfill 功能,那就可能出现污染全局代码,因此官方推荐了使用@babel/plugin-transform-runtime这个插件来实现 polyfill。安装npm install @babel/plugin-transform-runtime -D。配置如下

// babel.config.json
{
  "presets": [["@babel/preset-env"]],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

Snipaste_2022-03-27_18-12-23.png

注意因为目前使用了 corejs3,所以我们需要安装对应的库,安装npm install --save @babel/runtime-corejs3

Jsx 支持

当我们在写 React 时,我们会使用 jsx 语法,babel 官方提供了 jsx 的预设@babel/preset-react,安装 npm install @babel/preset-react -D,配置如下所示

// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    ["@babel/preset-react"]
  ]
}

Typescript 支持

我们在项目中也会使用 TypeScript 来开发,而 TypeScript 代码是需要转换成 JavaScript 代码,要知道原来我们会使用ts-loader来处理 ts 文件。但是 babel 也为我们提供了预设插件 @babel/preset-typescript。那么这两种有什么区别呢,我们分吧来使用一下。

  • ts-loader

安装npm install ts-loader -D,配置如下

{
  test: /\.(j|t)sx?$/,
  exclude: /node_modules/,
  use: [
    'ts-loader'
  ]
}

我们先执行 tsc –init,因为当我在下载 ts-loader 同时也会默认帮助我们安装了 typescrit,现在初始化 tsconfig.json 文件,并且修改 main.js 为 main.ts

// main.ts
const a: string = "123";
const fn = Promise.resolve(3);
fn.then((res) => {
  console.log(res);
});

重新打包后,我们发现 ts-loader 并没有帮助我们实现polyfill

Snipaste_2022-03-27_18-33-07.png

  • @babel/preset-typescript

现在我们来使用一下@babel/preset-typescript,安装npm install @babel/preset-typescript -D,配置如下

// webpack
{
  test: /\.(j|t)sx?$/,
  exclude: /node_modules/,
  use: [
    'babel-loader'
  ]
}
// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    ["@babel/preset-react"],
    ["@babel/preset-typescript"]
  ]
}

现在在重新打包试试

Snipaste_2022-03-27_18-43-44.png

现在我们可以看到已经帮我们提供了 polyfill

  • ts-loader 和@babel/preset-typescript 的区别

那么我们在开发中应该选择 ts-loader 还是 babel-loader 呢,我们区分一下两者

  • ts-loader(TypeScript Compiler)
    • 来直接编译 TypeScript,那么只能将 ts 转换成 js
    • 如果我们还希望在这个过程中添加对应的 polyfill,那么 ts-loader 是无能为力的
    • 我们需要借助于 babel 来完成 polyfill 的填充功能
  • babel-loader(Babel)
    • 来直接编译 TypeScript,也可以将 ts 转换成 js,并且可以实现 polyfill 的功能
    • 但是 babel-loader 在编译的过程中,不会对类型错误进行检测

那么如何做到既能实现 polyfill 又能提供类型错误检测呢,首先我们肯定希望使用 babel 的预设的,我们可以在 package.jsonscript 中添加一条命令

"script" {
  "ts-check": "tsc --noEmit"
 }

当我们在编译时执行一下该命令,但是个人推荐使用一个ForkTsCheckerWebpackPlugin的插件,它能在开发和构建环境提供类型错误检测,安装npm install fork-ts-checker-webpack-plugin -D。我们可以如下配置

// webpack
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
//...
{
  plugins: [
    //...
    new ForkTsCheckerWebpackPlugin({
      async: false,
    }),
  ];
}

所以最终在编译 ts 文件我们采取的方案是@babel/preset-typescriptForkTsCheckerWebpackPlugin这两插件的结合。

Babel 编译的原理

我么通过一张图来查看 Babel 编译的整个过程

Snipaste_2022-03-27_19-04-44.png

Babel 编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码,工作流程总结:

  1. 解析阶段(Parsing)
  2. 转换阶段(Transformation)
  3. 生成阶段(Code Generation)

ESlint 配置

ESLint 是一个静态代码分析工具,它能帮助我们在项目中建立统一的团队代码规范,保持正确、统一的代码风格,提高代码的可读性、可维护性。并且 ESLint 的规则是可配置的,我们可以自定义属于自己的规则。安装 ESlintnpm install eslint -D

现在我们来配置一下我们的 ESLint 规则,我们先初始化 ESlint

npx eslint --init

生成 .eslintrc.js文件

Snipaste_2022-03-27_13-44-36.png

配置文件的主要属性介绍一下

  • env: 运行的环境,比如是浏览器,并且我们会使用 es2021(对应的 ecmaVersion 是 12)的语法
  • extends: 可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个)
  • parserOptions: 这里可以指定 ESMAScript 的版本、sourceType 的类型
  • parser: 默认情况下是 espree(也是一个 JS Parser,用于 ESLint),如果我们我们需要编译其他比如 TypeScript,还需要指定对应的解释器
  • plugins: 指定我们用到的插件
  • rules: 自定义的一些规则

对应的规则我们可以从ESlint 中文官网查找

现在我们用命令行工具测试一下,修改 main.js 文件

const a = "123";
const fn = () => {
  return a;
};
console.log(fn(a));

执行下 eslint 命令

npx eslint ./src/main.js

Snipaste_2022-03-27_13-52-25.png

可以看到命令提示我们字符串需要使用单引号,并且提示我们可以使用--fix来修复,我们除了通过命令来检测代码规范也可以通过 vscode 插件来帮助我们检测

VSCode ESlint

我们在插件商场里搜索 ESlint,具体可见下方图片

Snipaste_2022-03-27_14-00-40.png

它会根据当前目录的 .eslintrc.js 配置文件来做检查,如果没有该配置文件它有默认的配置规范, 现在再来看我们的main.js文件,编辑器已经帮助我们将不规范的地方划上了波浪形

Snipaste_2022-03-27_14-01-48.png

ESlint Loader

那我们除了用这些方式外,能否在编译代码的时候,也希望进行代码的 eslint 检测,这个时候我们就可以使用 eslint-loader 来完成。安装npm install eslint-loader -D,配置如下

{
  test: /\.(j|t)sx?$/,
  exclude: /node_modules/,
  use: [
    'babel-loader',
    'eslint-loader'
  ]
}

自动修复保存

ESLint 会帮助我们提示错误(或者警告),但是不会帮助我们自动修复,需要手动添加--fix参数

eslint src --fix

在开发中我们希望文件在保存时,可以自动修复这些问题,我们可以选择使用另外一个工具: prettier,我们在 VSCode 插件商场里搜索 prettier

Snipaste_2022-03-27_14-15-14.png

但是在实际项目中并不太喜欢使用该插件,这个习惯因人而异吧。

碰到的问题

在使用 eslint-loader 碰到一个问题,我在编译时会报 TypeError: Cannot read property ‘getFormatter’ of undefined 这个错误,我当时安装的版本 “eslint”: “^8.11.0”,“eslint-loader”: “^4.0.2”,

Snipaste_2022-03-27_20-55-51.png

经过 debugger 我发现在 eslint-loader 的 getOptions.js 引用了 eslint 的CLIEngine

Snipaste_2022-03-27_21-00-51.png

但是这个 CLIEngine 导出的是 undefined,因此我去查看 eslint 的库默认引用的./lib/api.js,发现并没有导出 CLIEngine。

Snipaste_2022-03-27_21-02-35.png

看了一下 eslint 源码发现 CLIEngine 这玩意是来自 eslint/lib/cli-engine/index.js导出的,那我们只需要将 eslint-loader 的 options 里给传入一个 eslint/lib/cli-engine/index.js,重新打包后还是报错

Snipaste_2022-03-27_21-06-11.png

它说我们不能在 eslint 中通过这个路径导出模块,我看了下 eslint 的 package.json,发现它作了导出限制

Snipaste_2022-03-27_21-07-52.png

因此我们可以这样配置

// webpack
{
  test: /\.(j|t)sx?$/,
  exclude: /node_modules/,
  use: [
    'babel-loader',
    {
      loader: 'eslint-loader',
      options: {
        eslintPath: 'eslint/cli-engine'
      }
    }
  ]
}

在 eslint 的 package.json 中的 exports 里加上"./cli-engine": "./lib/cli-engine/index.js"

Snipaste_2022-03-27_21-10-23.png

现在再来重新打包

Snipaste_2022-03-27_21-11-48.png

可以看到已经正常了,但是因为我们改的地方包含了 node_module 里的 eslint 文件,这始终不是个好办法,最终想到是我们将原来 eslint 删除,降低 eslint 版本至 7.32.0。安装完后重新打包现在就都 ok 了。

浏览器兼容性

Browserslist

这里指的兼容性是针对不同的浏览器支持的特性: 比如 css 特性、js 语法之间的兼容性,而我们市面上使用的有  Chrome、Safari、IE、Edge、Chrome for Android、UC Browser、QQ Browser 等,其实我们一般可以在一些脚手架中的 package.json 里面看到这样的信息

> 1%
last 2 versions
not dead

这里的  > 1%  意思是大于市面上占有率的浏览器,这里的浏览器不同版本以及市场占有率可以从  caniuse.com/usage-table…查看,如下图

而我们需要兼容这些大于 1%这个条件的浏览器的兼容性,这时候就需要使用  Browserslist  工具,这是一个共享当前条件对应的目标浏览器的配置,当我们使用  Browserslist  工具配置好需要兼容的条件之后,我们将会使用以下的兼容工具帮助我们兼容 css 和 js。

这些工具就会依赖当前  Browserlist 配置的条件去做对应的兼容处理,而  Browserlist 在安装 webpack 时会自动安装,我们可以通过命令  browserslist  命令查看目标浏览器列表。

npx browserslist ">1%, last 2 version, not dead"

注意: browserslist  命令后面不跟条件的话先会找.browserslistrc 配置文件,否则会使用默认条件。

这些列出的就是符合我们条件并且需要兼容的目标浏览器。

配置 browserslist 有两种方式:

  • package.json 配置
  • 独立.browserslistrc文件配置

package.json 配置

{
  //...
  "browserslist": ["> 1 %", "last 2 version", "not dead"]
}

.browserlistrc文件配置,在工程目录下新建  .browserslistrc 文件

注意:当条件之间使用逗号或者空格或者 or 的时候,条件之间为并集。当条件之间使用 and 为交集。当条件之间使用 not 为不包含。

具体 browserslist 配置见  github 地址

Postcss

接下来我们确认好需要兼容的目标浏览器后就需要使用工具去做兼容处理了,首先需要先介绍一下  postcss 。

PostCSS 是一个通过 JavaScript 来转换样式的工具,这个工具可以帮助我们进行一些 CSS 的转换和适配,比如自动添加浏览器前缀、css 样式的重置,但是实现这些工具,我们需要借助于 PostCSS 对应的插件,而这些插件就是上面所述的兼容工具。

我们可以使用命令行来测试下 postcss 工具下的 autoprefixer 插件,需要安装  postcss 、postcss-cli ,执行  npm intall postcss postcss-cli -D  安装。

使用终端命令需要借助  postcss-cli 依赖包,而转换又需要使用 postcss。

之后我们需要再下载用于做兼容处理的插件 autoprefixer,postcss 需要指定某个插件来做处理,npm intall autoprefixer -D 。安装完后我们就可以使用终端命令行来测试下。

npx postcss --use autoprefixer -o result.css ./src/css/index.css

执行命令后

可以看到输出的 result.css 文件已经给我们的样式属性自动添加了兼容代码。但是在实际项目中我们不可能使用命令来做转换处理,因此我们需要一个 loader 来处理样式文件,webpack 就使用  postcss-loader  来处理。安装  npm install postcss-loader -D 。

在 webpack.config.js 中添加 postcss-loader。

{
  {
    test: /.css$/i,
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          postcssOptions: {
            plugins: [
              'autoprefixer' // 需要指定使用的兼容插件,会转成require('autoprefixer')
            ]
          }
        }
      }
    ]
  }
}

但是我们除了在 css 文件中使用到 postcss-loader,还会在其他比如预处理器 less 文件中使用到。这使用我们重复写 postcss-loader 就会变的很繁琐。此时我们便可以提取出 postcss.config.js 文件,将配置都放在该文件内,而 webpack 配置中就变成

{
  rules: [
    {
      test: /.css$/i,
      use: ["style-loader", "css-loader", "postcss-loader"],
    },
    {
      test: /.less$/i,
      use: ["style-loader", "css-loader", "postcss-loader", "less-loader"],
    },
  ];
}

然后独立出 postcss.config.js 文件

目前 postcss 的配置已经差不多了还有一点,目前 autoprefixer 已经不在使用,我们推荐更多的是使用  postcss-preset-env,该插件包含了 autoprefixer 的功能,并且能将一些现代的 CSS 特性,转成大多数浏览器认识的 CSS,并且会根据目标浏览器或者运行时环境添加所需的 polyfill。安装  npm install postcss-preset-env -D 。修改 postcss.config.js 文件

module.exports = {
  plugins: ["postcss-preset-env"],
};

比如样式属性  color: #28f2d334 ,值设置成 8 位后,有些浏览器会识别,有些不会。这时  postcss-preset-env 会帮助我们兼容处理

重新打包构建后可以看到 color 属性值已经被转换成 rgba 格式了,并且对之前的属性做的前缀兼容处理。

当然最后我们需要补充一个点的是,当我们在 css 文件中@import 其他 css 文件时候,postcss-loader 和 css-loader 在处理完当前的 css 文件后并不会对@import 中的 css 文件再执行一次 postcss-loader,所以我们需要在 css-loader 中添加配置

{
  rules: [
    {
      test: /.css$/i,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            importLoaders: 1, // css中引入css不会从postcss-loader开始而是从css-loader转换开始,因此该配置保证loader向上一层开始转换
          },
        },
        "postcss-loader",
      ],
    },
    {
      test: /.less$/i,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            importLoaders: 2, // css中引入css不会从postcss-loader开始而是从css-loader转换开始,因此该配置保证loader向上一层开始转换
          },
        },
        "postcss-loader",
        "less-loader",
      ],
    },
  ];
}

- Book Lists -