Webpack5学习 --- babel

895 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

在开发中我们很少直接去接触babel,但babel对于前端开发来说是不可缺少的一部分。

尤其是在我们想要使用ES6+的语法,想要使用TypeScript的时候,我们都离不开Babel。

babel简介

Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript

包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等

Babel使用的是微内核架构,即babel只提供核心的架构@babel/core

@babel/core和postcss一样,能做的功能是有限的,或者说基本没有什么功能

如果我们需要使用babel的转换功能,那么我们就需要为babel添加对应的插件或预设

在命令行使用

# @babel/core:babel的核心代码,必须安装
# @babel/cli:可以让我们在命令行使用babel
npm install @babel/cli @babel/core
 # 使用babel来处理我们的源代码
 # src:是源文件的目录
 # --out-dir:指定要输出的文件夹dist
 # npx babel <源文件或文件夹路径> --out-dir <输出文件夹>
 npx babel src --out-dir dist

此时,babel并没有给我们转换任何的代码,即转换前后代码是基本一致的

所以如果我们需要转换,我们可以为babel配置对应的plugins

# @babel/plugin-transform-arrow-functions ==> 转换箭头函数
# @babel/plugin-transform-block-scoping ===> 将const 和 let 转换为 var
npm install @babel/plugin-transform-arrow-functions  @babel/plugin-transform-block-scoping -D
# 转换 使用多个插件用逗号分隔(前后不要有空格)
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

但是如果要转换的内容过多,一个个设置和下载插件是比较麻烦的,我们可以使用预设(preset)

所谓预设,其实就是插件的集合包

# 安装基本预设
npm install @babel/preset-env -D
# 编译
npx babel src --out-dir dist --presets=@babel/preset-env

babel的底层原理

我们可以将babel看成就是一个编译器

而编译器的核心工作就是从一种源代码(原生语言)转换成另一种源代码(目标语言)

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

Babel的工作流程:

  1. 解析阶段(Parsing) ===> 将源代码经过解析,生成AST(抽象语法树)
  2. 转换阶段(Transformation) ===> 遍历阶段,将对应的节点应用对应的插件,进行代码的转换,形成新的AST
  3. 生成阶段(Code Generation) ===> 将新的AST转回我们平时常见的代码,此时新生成的代码就是已经被转换后的代码

I8NiQy.png

I8SGfR.png

babel-loader

# 安装
npm i @babel/core babel-loader -D
// 为babel配置plugins
module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          plugins: [
            '@babel/plugin-transform-arrow-functions',
            '@babel/plugin-transform-block-scoping'
          ]
        }
      }
    }
  ]
}
// 为babel配置presets
// babel会根据browserslist查询到的需要适配的浏览器所支持的语法, 自动使用预设包中所需要使用的plugin
// 常见的预设有@bebel/preset-env, @babel/preset-typescript, @babel/preset-react
module: {
    rules: [
     {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
         presets: [
          '@babel/preset-env'
         ]
        }
      }
     }
    ]
}
module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          // 可以给预设设置一些参数,使用数组表示 
          // 参数1是预设包,参数2是传入预设包的参数
          // targets就是需要兼容的目标浏览器 --- 配置的targets属性会覆盖browserslist中的配置
	  // 但是我们一般不推荐在这里进行设置,而是在browserslist中进行设置
          // 因为在browserslist中设置的配置是可以在多个工具之间进行共享的
          presets: [
            ['@babel/preset-env', {
              targets: 'chrome 88'
            }]
          ]
        }
      }
    }
  ]
}

配置文件

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

  • babel.config.json(或者.js,.cjs,.mjs)文件 --- 可以直接作用于Monorepos项目的子包,更加推荐 --- 使用与Bebel7版本及以上
  • .babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件 --- 早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的

babel.config.js --- 位于项目根目录下

module.exports = {
  presets: [
    '@babel/preset-env'
  ]
}

webpack.config.js

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

babel的配置文件不仅仅可以导出一个对象,同样可以导出一个函数, 该函数可以接收一个参数, 这个参数就是babel在编译时候使用的api对象

module.exports = api => {
  // 开启babel对于配置文件的缓存操作
  // babel会自动在每次打包的时候判断配置文件是否发生改变,并尽可能的使用缓存来提升编译时babel的打包性能
  api.cache(true)

  const presets = [
    '@babel/preset-env'
  ]

  const plugins = [
    ['@babel/plugin-transform-runtime', {
      corejs: {
        version:  3,
        proposals: true
      }
    }]
  ]

  return {
    presets,
    plugins
  }
}

polyfill

babel默认情况下只能对新语法特性进行转换,

而一些新的API(例如: Promise, Generator, Symbol等以及实例方法例如Array.prototype.includes等)是没有做实现的,

为了可以在没有实现这些新特性API的浏览器中可以使用这些新特性API,就需要使用polyfill

所以polyfill更类似于一种补丁,可以帮助我们更好的使用JavaScript

# 安装 
# 注意: 这2个包是开发依赖 不是生产依赖 --- 因为他们的作用是在生产环境代码 为对应的代码加上补丁
npm install core-js regenerator-runtime --save
module: {
  rules: [
    {
      test: /\.js$/,
      // 包中的代码可能已经打上了polyfill,如果此时再次对包中的代码进行打补丁操作,可能会出现代码冲突,所以需要排除
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader'
      }
    }
  ]
}

useBuiltIns

false

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      { // useBuiltIns 可以有3种选项:false|usage|entry --- 这个属性就是告诉babel如何打补丁的
        useBuiltIns: false // false 不打补丁
      }
    ]
  ]
}

usage

// 会根据源代码中出现的语言特性,自动检测所需要的polyfill
// 这样可以确保最终包里的polyfill数量的最小化,打包的包相对会小一些
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3 // corejs的默认版本是2,但是一般使用的是3,所以需要手动指定
        // 可以设置的版本是major和minor 例如 corejs: 3.16 (corejs: 3.16.0 => error)
      }
    ]
  ]
}
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: { 
          version: 3, // 设置使用corejs的版本号
          propsals: true // 设置corejs在进行转换的时候对提议阶段的特性进行支持
        }
      }
    ]
  ]
}

entry

// 只要是目标浏览器需要支持的新特性,我就引入对应的polyfill,无论我的代码中有没有实际使用到
// 所以使用这种方式打包后的代码,体积相对也会比较大
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        // 默认这么写是不会生效的,
        // 需要在入口文件中添加 `import 'core-js/stable'; import 'regenerator-runtime/runtime'
        useBuiltIns: 'entry'
      }
    ]
  ]
}
// index.js
import 'core-js/stable
import 'regenerator-runtime/runtime'

Plugin-transform-runtime

我们使用的polyfill,默认情况是添加的所有特性都是全局

如果我们正在编写一个工具库,这个工具库需要使用polyfil,别人在使用我们工具时,工具库通过polyfill添加的特性,可能会污染它们的代码,此时可以使用Plugin-transform-runtime, 它所实现的polyfill功能是局部的

 # 安装
 npm install @babel/plugin-transform-runtime -D

注意:因为我们使用了corejs3,所以我们需要安装对应的库

I8SZhH.png

# 注意: 这个包是开发依赖
npm i @babel/runtime-corejs3
// @babel/plugin-transform-runtime 和 useBuiltIns不要混用
module.exports = {
  plugins: [
    // @babel/plugin-transform-runtime 所引入的polyfills是按照usage的方式进行引入的
    ['@babel/plugin-transform-runtime', {
      corejs: 3
    }]
  ]
}

编译JSX

 npm i react react-dom
 npm install @babel/preset-react -D
module.exports = {
 presets: [
  '@babel/preset-react'
 ]
}

编译TS

# ts-loader在编译的时候是依赖于tsc的,所以typescript会作为ts-loader的依赖包一起被下载下来
npm i ts-loader -D

typescript如何进行编译,是需要依赖于typescript的依赖配置文件的,所以需要先生成typescript所对应的依赖配置文件

tsc --init
// 配置
module: {
  rules: [
    {
      test: /\.ts$/,
      use: 'ts-loader'
    }
  ]
}

ts-loader本质上还是使用tsc来进行ts代码的编译,所以ts-loder会将typescript作为依赖项一起下载

但是在实际开发中,我们还有另一种编译ts的方式,也就是babel-loaderpreset-typescript

babel-loader的本身就可以使用babel对typescript进行编译,所以此时不需要安装typescript

# 安装 
# 虽然@babel/preset-typescrip中只有一个插件@babel/tranform-typescript
# 但是不排除以后在预设包中扩展其它插件的可能性,所以推荐使用预设包
npm i @babel/preset-typescript -D

webpack.config.js

module: {
  rules: [
    {
      test: /\.ts$/,
      // 为了避免babel多次转换出现错误,还是推荐不对node_modules下的文件进行打包
      exclude: /node_modules/, 
      loader: 'babel-loader'
    }
  ]
}

babel.config.js

module.exports = {
 presets: [
  '@babel/preset-typescript'
 ]
}

ts-loader vs preset-typescript

名称功能
ts-loader1. ts代码转换为js代码时,如果ts代码书写错误,在编译的时候会提示对应的错误信息
2. 不会为编译后的代码添加相应的polyfill,默认不会进行ES6代码的转换(需要在tsconfig.json中进行配置)
babel-loader1. 可以直接编译TypeScript,并且可以实现polyfill的功能
2. 在编译的过程中,不会对类型错误进行检测 也就是说如果出现错误,babel-loader并不会将错误输出在控制台

所以我们在实际开发中,可以将两者结合在一起使用

webpack.config.js

module: {
  rules: [
    {
      test: /\.ts$/,
      exclude: /node_modules/,
      // 也就是使用ts-loader对ts的编译转换能力
      // 使用babel的代码转换功能,但是不使用它对于TS代码的转换功能
      use: [
        'babel-loader',
        'ts-loader'
      ]
    }
  ]
}

babel.config.js

module.exports = {
 presets: [
  '@babel/preset-env'
 ]
}