深入解析Babel - polyfill

1,004 阅读6分钟

1. babel的作用

  • babel对于前端开发来说,目前是不可缺少的一部分:

    • 开发中,我们想要使用ES6+的语法,想要使用TypeScript,开发React项目,它们都是离不开Babel的
  • 那么,Babel到底是什么呢?

    • Babel是一个工具链,主要用于旧浏览器或者环境中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript
    • 包括:语法转换、源代码转换、Polyfill实现目标环境缺少的功能

2. babel命令行

  • babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配置来单独使用

  • 如果希望在命令行尝试使用babel,需要安装如下库:

    • @babel/core:babel的核心代码,必须安装
    • @babel/cli:可以让我们在命令行使用babel
    • npm install @babel/cli @babel/core
  • 使用babel来处理我们的源代码:

    • src:是源文件的目录
    • --out-dir:指定要输出的文件夹build
    • npx babel ./src --out-dir ./build
  • 转换块级作用域的插件:

    • npm install @babel/plugin-transform-block-scoping -D
    • 终端命令:npx babel src --out-dir build --plugins=@babel/plugin-transform-block-scoping
  • 转换箭头函数相关的插件:

    • npm install @babel/plugin-transform-arrow-functions -D
    • 终端命令:npx babel src --out-dir build --plugins=@babel/plugin-transform-arrow-functions
  • 预设(预设的含义后续补充)的使用:不需要安装一个个单独的插件

    • npm install @babel/preset-env -D
    • npx babel src --out-dir build --presets=@babel/preset-env

3. babel底层原理

  • babel是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?

    • 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是编译器的工作
    • 事实上我们可以将babel看成就是一个编译器
    • Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码
  • Babel也拥有编译器的工作流程:

    • 解析阶段(Parsing)
    • 转换阶段(Transformation)
    • 生成阶段(Code Generation)
  • babel编译器执行原理

    image.png

4. webpack和babel的结合

4.1 webpack中的使用

  • 安装webpack:npm install webpack webpack-cli -D

  • 编写webpack配置文件

    const path = require('path')
    
    module.exports = {
      mode: 'development',
      devtool: false,
      entry: './src/index.js',
      output: {
        clean: true,
        filename: 'bundle.js',
        path: path.resolve(__dirname, './build')
      },
      module: {
        rules: [
          {
            test: /\.m?js$/,
            use: {
              loader: 'babel-loader',
              options: {
                // 应用插件
                // plugins: [
                //   '@babel/plugin-transform-arrow-functions',
                //   '@babel/plugin-transform-block-scoping'
                // ]
                presets: [
                  '@babel/preset-env'
                ]
              }
            }
          }
        ]
      }
    }
    

4.2 babel-preset

  • 使用过程中如果一个个去安装使用的插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。

  • 常见的预设有三个:

    • env
    • react
    • TypeScript

5. 浏览器兼容性问题

5.1 兼容性问题

  • 开发中,浏览器的兼容性问题,应该如何去解决和处理?

    • 这里指的兼容性是针对不同的浏览器支持的特性:比如css特性、js语法之间的兼容性
  • 市面上有大量的浏览器:

    • 有Chrome、Safari、IE、Edge、Chrome for Android、UC Browser、QQ Browser等等
    • 它们的市场占率是多少?我们要不要兼容它们呢?
  • 其实在很多的脚手架配置中,都能看到类似于这样的配置信息:

    • 这里的 > 1% 就是占有率
    > 1%
    last 2 versions
    not dead
    

5.2 浏览器的市场占有率

  • 查询到浏览器的市场占有率的方法

    • 通常会查询的一个网站就是 caniuse

    占有率.png

5.3 认识Browserslist工具

  • 怎么在css兼容性和js兼容性下共享配置的兼容性条件呢?

    • 就是当我们设置了一个条件: > 1%
    • 表达的意思是css要兼容市场占有率大于1%的浏览器,js也要兼容市场占有率大于1%的浏览器;
    • 如何通过工具来达到这种兼容性的,比如postcss-preset-envbabelautoprefixer
  • Browserslist 是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置

    • Autoprefixer
    • Babel
    • postcss-preset-env
    • eslint-plugin-compat
    • stylelint-no-unsupported-browser-features
    • postcss-normalize
    • obsolete-webpack-plugin
  • 编写规则

    • defaults:Browserslist的默认浏览器(> 0.5%, last 2 versions, Firefox ESR, not dead)

    • 5%:通过全局使用情况统计信息选择的浏览器版本。 >=,<和<=工作过。

      • 5% in US:使用美国使用情况统计信息。它接受两个字母的国家/地区代码。
      • > 5% in alt-AS:使用亚洲地区使用情况统计信息。有关所有区域代码的列表,请参见caniuse-lite/data/regions
      • > 5% in my stats:使用自定义用法数据。
      • > 5% in browserslist-config-mycompany stats:使用 来自的自定义使用情况数据browserslist-config-mycompany/browserslist-stats.json。
      • cover 99.5%:提供覆盖率的最受欢迎的浏览器。
      • cover 99.5% in US:与上述相同,但国家/地区代码由两个字母组成。
      • cover 99.5% in my stats:使用自定义用法数据。
    • dead24个月内没有官方支持或更新的浏览器

      • 现在是IE 10,IE_Mob 11,BlackBerry 10,BlackBerry 7, Samsung 4和OperaMobile 12.1
    • last 2 versions:每个浏览器的最后2个版本

      • last 2 Chrome versions:最近2个版本的Chrome浏览器
      • last 2 major versions或last 2 iOS major versions:最近2个主要版本的所有次要/补丁版本

5.4 Browserslist工具使用

  • 命令行使用

    • 通过命令来查询某些条件所匹配到的浏览器
      • npx browserslist ">1%, last 2 version, not dead"
  • 配置browserslist

    • 方案一:在package.json中配置

      image.png

    • 方案二:单独的一个配置文件.browserslistrc文件

      browserslistrc.png

  • 默认值

    image.png
  • 编写多个条件

    image.png

6. babel配置浏览器兼容性

  • 最终打包的JavaScript代码,是需要跑在目标浏览器上的,那么如何告知babel我们的目标浏览器呢?

    • browserslist工具
    • targets属性
    const path = require('path')
    
    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /\.m?js$/,
            use: {
              loader: 'babel-loader',
              options: {
                // 应用插件
                // plugins: [
                //   '@babel/plugin-transform-arrow-functions',
                //   '@babel/plugin-transform-block-scoping'
                // ]
                // presets: [
                //   '@babel/preset-env'
                // ]
                presets: [
                  // 在开发中针对babel的浏览器兼容查询使用browserslist工具, 而不是设置target
                  // 因为browserslist工具, 可以在多个前端工具之间进行共享浏览器兼容性(postcss/babel)
                  ['@babel/preset-env', {
                    targets: '>5%'
                  }]
                ]
              }
            }
          }
        ]
      }
    }
    
  • 如果两个同时配置了,哪一个会生效呢?

    • 配置的targets属性会覆盖browserslist;
    • 但是在开发中,更推荐通过browserslist来配置,因为类似于postcss工具,也会使用browserslist,进行统一浏览器的适配

7. babel的配置文件

  • 将babel的配置信息放到一个独立的文件中,babel给我们提供了两种配置文件的编写:

    • babel.config.json(或者.js,.cjs,.mjs)文件;
    • .babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
  • 它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等)

    • .babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的
    • babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐
  • 配置文件编写

    module.exports = {
      // plugins: [
      //   '@babel/plugin-transform-arrow-functions',
      //   '@babel/plugin-transform-block-scoping'
      // ]
      // presets: [
      //   '@babel/preset-env'
      // ]
      presets: [
        ['@babel/preset-env', {
          // targets: '>5%'
        }]
      ]
    }
    

8. babel和polyfill使用

8.1 认识polyfill

  • Polyfill是什么呢?

    • 翻译:一种用于衣物、床具等的聚酯填充材料, 使这些物品更加温暖舒适
    • 理解:更像是应该填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript
  • 为什么时候会用到polyfill呢?

    • 比如我们使用了一些语法特性(例如:Promise, Generator, Symbol等以及实例方法例如Array.prototype.includes 等)
    • 但是某些浏览器压根不认识这些特性,必然会报错
    • 我们可以使用polyfill来填充或者说打一个补丁,那么就会包含该特性了

8.2 使用polyfill

  • babel7.4.0之前,可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了

  • babel7.4.0之后,可以通过单独引入core-js和regenerator-runtime来完成polyfill的使用

    • npm install core-js regenerator-runtime --save

8.3 结合babel使用

  • 在babel.config.js文件中进行配置,给preset-env配置一些属性:

    • useBuiltIns:设置以什么样的方式来使用polyfill
      • false
        • 打包后的文件不使用polyfill来进行适配
        • 并且此时是不需要设置corejs属性的
      • usage
        • 会根据源代码中出现的语言特性,自动检测所需要的polyfill
        • 这样可以确保最终包里的polyfill数量的最小化,打包的包相对会小一些
        • 可以设置corejs属性来确定使用的corejs的版本
      • entry
        • 如果我们依赖的某一个库本身使用了某些polyfill的特性,但是因为我们使用的是usage,之后用户浏览器可能会报错;
        • 要想解决这种报错,可以使用 entry;
        • 并且需要在入口文件中添加 import 'core-js/stable'; import 'regenerator-runtime/runtime';
        • 这样做会根据 browserslist 目标导入所有的polyfill,但是对应的包也会变大
      import 'core-js/stable'
      import 'regenerator-runtime/runtime'
      
    • corejs:设置corejs的版本,目前使用较多的是3.x的版本,本文使用的是3.26.1
      • 另外corejs可以设置是否对提议阶段的特性进行支持
      • 设置 proposals属性为true即可
    // babel.config.js
    module.exports = {
      presets: [
        ['@babel/preset-env', {
          // targets: '>5%'
          corejs: 3,
          // // false: 不使用polyfill进行填充
          // useBuiltIns: 'usage' // 开发中推荐使用usage
          useBuiltIns: 'entry'  // 注意在入口文件处引入
        }]
      ]
    }
    

9. babel对react的支持

  • 在编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的

  • 对react jsx代码进行处理需要如下的插件:

    • @babel/plugin-syntax-jsx
    • @babel/plugin-transform-react-jsx
    • @babel/plugin-transform-react-display-name
  • 开发中,我们并不需要一个个去安装这些插件,我们依然可以使用preset来配置

    • npm install @babel/preset-react -D
    // babel.config.js
    module.exports = {
      presets: [
        ['@babel/preset-env', {
          // targets: '>5%'
          // corejs: 3,
          // // // false: 不使用polyfill进行填充
          // // useBuiltIns: 'usage'
          // useBuiltIns: 'entry'
        }],
        ['@babel/preset-react']
      ]
    }
    
    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      mode: 'development',
      devtool: false, // 不生成source-map
      entry: './src/index.js',
      output: {
        clean: true,
        filename: 'bundle.js',
        path: path.resolve(__dirname, './build')
      },
      module: {
        rules: [
          {
            test: /\.m?jsx?$/,
            use: {
              loader: 'babel-loader',
            }
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: './index.html'
        })
      ]
    }
    

10. webpack对typescript支持

10.1 ts-loader

  • 安装

    • npm install ts-loader -D
    • npm install typescript -D
  • 编写一个tsconfig.json文件

    • tsc --init
  • 配置webpack

    image.png

  • 执行npm run build 进行打包

10.2 babel-loader

  • Babel是有对TypeScript进行支持;

    • 可以使用插件: @babel/tranform-typescript
    • 更推荐直接使用preset:@babel/preset-typescript
  • 安装@babel/preset-typescript:

    • npm install @babel/preset-typescript -D
  • 配置babel

    // babel.config.js
    module.exports = {
      presets: [
        ['@babel/preset-env'],
        ['@babel/preset-react'],
        ['@babel/preset-typescript']
      ]
    }
    

10.3 两者如何选择

  • 使用ts-loader(TypeScript Compiler)

    • 来直接编译TypeScript,那么只能将ts转换成js
    • 如果我们还希望在这个过程中添加对应的polyfill,那么ts-loader是无能为力的
    • 这时需要借助于babel来完成polyfill的填充功能
  • 使用babel-loader(Babel)

    • 来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能
    • 但是babel-loader在编译的过程中,不会对类型错误进行检测
  • 结合使用:

    • 使用Babel来完成代码的转换,使用tsc来进行类型的检查

    • 打包代码使用babel-loader

    • 开发阶段 执行tsc --noEmit --watch 监听类型错误,纠正错误后再进行打包

      image.png