Babel

Babel 是 JavaScript 的转译器。用于将 ES Next 的代码转换成浏览器或者其他环境支持的代码。注意:不是转化为 ES5 ,因为不同类型以及不同版本的浏览器对 ES Next 新特性的支持程度都不一样,对于目标环境已经支持的部分,Babel 可以不转化,所以 Babel 会依赖运行环境的版本。

CLI

@babel/cli是的 babel 的 CLI 工具。让我们可以使用babel命令编译文件,该命令存放在babel/cli/bin目录下。

Babel 工作流程

babel 运行分为三个阶段:

  1. 解析 Parser :通过模块 @babel/parser将源码解析成 AST
  2. 转换 Transfrom:通过一系列插件 转换成新的 AST
  3. 生成 Generator:模块 @babel/generator将转换后的 AST 重新生成新的代码
1.png

@babel/core

@babel/core 依赖于@babel/parse、 @babel/traverse、@babel/types、@babel/template 等 模块, 通过 @babel/parse 解析器,基于ESTree 规范,将源码转换成 AST。再使用 @babel/traverse 模块遍历 AST,并在遍历过程中调用 visitor 函数进行 AST 的增删改,该模块提供了 path 的 api。然后转换能力全部通过插件进行。

@babel/core 模块提供了以下几个方法可进行转换:

  1. 字符串形式的 JavaScript 代码可直接使用 babel.transform 来编译:
babel.transform('code', options) // { code, map, ast }
  1. 如果是文件可使用异步 API:
babel.transformFile("file.js", options, (err, result:  { code, map, ast })  => {})
  1. 或使用同步 API:
babel.transformFileSync('file.js', options) // { code, map, ast }
  1. 直接从 Babel AST(抽象语法树)进行转换
babel.transformFromAst(ast, code, options) // { code, map, ast }

源码如下:

2.png
// source.js

const arr = [1, 2, 3, 4]
arr.forEach(v => v * 2)
// index.js
const babel = require('@babel/core')
const fs = require('fs')
const code = fs.readFileSync('./source.js').toString()

// @babel/core 内部引用了 parser、generator等模块
const res = babel.transform(code, {
  // 是否生成ast
  ast: true,
  // 是否生成解析的代码
  code: true,
  // 是否生成 sourcemap
  sourceMaps: true
  // 用到的插件
  // plugins: ['@babel/plugin-transform-arrow-functions']
})

console.log(res)

可以看出转换后的结果包含源码的 ast、转换后的 code 以及 map 等。

从 code 可以看出没有进行转换,是因为 core 模块本身不提供转换能力,需要使用插件进行转换。

3.png

对箭头函数进行转换:

// index.js

// 设置转换箭头函数的插件

const res = babel.transform(code, {

  plugins: ['@babel/plugin-transform-arrow-functions']

})
4.png

以上看出,箭头函数转换成了普通函数 ,但是 const 没有转换成 var,是因为只使用了转换箭头函数的插件。

traverse

@babel/traverse 是用来自动遍历AST的工具,它会访问树中的所有节点,在进入每个节点时触发 enter 钩子函数,退出每个节点时触发 exit 钩子函数。开发者可在钩子函数中对 AST 进行修改。

import traverse from '@babel/traverse'

traverse(ast, {
  enter(path) {
    // 进入 path 后触发
  },
  exit(path) {
    // 退出 path 前触发
  }
})

types

@babel/types 是作用于 AST 的类 lodash 库,其封装了大量与 AST 有关的方法,大大降低了转换 AST 的成本。@babel/types 的功能主要有两种:一方面可以用它验证 AST 节点的类型,例如使用 isClassMethod 或 assertClassMethod 方法可以判断 AST 节点是否为 class 中的一个 method;另一方面可以用它构建 AST 节点,例如调用 classMethod 方法,可生成一个新的 classMethod 类型 AST 节点 。

template

@babel/template 实现了计算机科学中一种被称为准引用(quasiquotes)的概念。说白了,它能直接将字符串代码片段(可在字符串代码中嵌入变量)转换为 AST 节点。例如下面的例子中,@babel/template 可以将一段引入 axios 的声明直接转变为 AST 节点。

import template from '@babel/template'

const ast = template.ast(`
  import vue from 'vue'
`)

Presets 和 Plugins

Presets

预设是一系列插件的集合。

比如:

  • @babel/preset-env

  • @babel/preset-react

  • @babel/preset-typescript

preset 的顺序

preset 是逆序排列的(从后往前)。主要是为了确保向后兼容。这里涉及到配置的继承和重写概念。

继承和重写是面向对象编程语言中的概念,是指一个类扩展自父类,并且重新实现了一些属性和方法。这种思想不止在编程语言中用到,在配置文件中也广泛使用。比如 eslint 中 extends的配置。

除了整体重写以外,还支持文件级别和环境级别的重写:

文件级别:
overrides: [
  {
    test: 'src/test.js',
    plugins: ['pluginA']
  }
]
环境级别:
{

    envName: 'development',
    env: {
        development: {
            plugins: ['pluginA']
        },
        production: {
            plugins: ['pluginB']
        }
    }
}

preset 的参数

preset 可以接受参数,参数由插件名和参数对象组成一个数组。

@babel/preset-env

最常用的是@babel/preset-env,是一个智能预设。其转译是按需的,对于环境支持的语法可以不做转换的。开发者只需使用最新的 JavaScript,而无需微观管理目标环境所需的语法转换(以及可选的浏览器 polyfill)。

通过配置 targets 属性指定目标浏览器,让 Babel 只转译环境不支持的语法。如果没有配置会默认转译所有 ES Next 的语法。

// 增加 presets配置
presets: [
   '@babel/preset-env'
]

默认转成 ES5:

5.png

然后给targets指定 chrome: '67'(chrome67 版本是支持 const 和箭头函数的)

  presets: [
    [
      '@babel/preset-env'
        {
          targets: {
            chrome: '67'
          }
        }
    ]
  ]

const 和箭头函数并没有进行转换。

6.png

创建 preset

导出一个函数,函数的返回值为配置对象。

preset 可以是插件组成的数组:

module.exports = (api, opts) => ({
  plugins: ['pluginA', 'pluginB', 'pluginC']
})

也可以包含其他的 preset 和带参数的插件:

module.exports = (api, opts) => ({
  presets: [require('@babel/preset-env')],
  plugins: [[require('pluginA'), { loose: true }]]
})

比如 Vue-CLI 的 preset 就依赖于@babel/preset-env:

vue-cli 的 babel presets:

7.png

@vue/cli-plugin-babel/preset源码:

8.png

Plugins

插件分为语法插件和转换插件。

语法插件

作用于解析阶段。

大多数语法都可以被 babel 转换,极少数情况下需要使用插件解析特定类型的语法。官方的语法插件以

babel-plugin-syntax开头。

语法插件虽名为插件,但事实上其本身并不具有功能性。所对应的语法功能其实都已在@babel/parser里实现,插件的作用只是将对应语法的解析功能打开。

转换插件

作用于转换阶段,用于转换代码。

官方的转换插件以babel-plugin-transform(正式)或 babel-plugin-proposal(提案)开头。

转换插件将启用相应的语法插件,因此你不必同时指定这两种插件。

为什么在配置文件中有了预设还需要插件?

  1. 有些最新的语法可能还处于提案阶段,没有加入预设集合
  2. 根据项目需求,自己编写的插件

执行顺序

plugin 和 preset 都是通过数组的形式在配置文件中配置,生效顺序是先 plugin 后 preset,plugin 从左到右,preset 从右到左,这样的生效顺序使得配置里的插件可以覆盖 preset 里面插件的配置的,也就是重写。

core-js

Babel 把 ES Next 标准分为 syntaxbuilt-in 两种类型。syntax 是语法,比如:const、 () => 等。babel 默认只转换 syntax 类型。而 Promise、Set、Map 等环境所内置的 API 、静态方法 Array.from、Object.assign、实例方法Array.prototype.includes等属于 built-in。而 built-in 类型需要通过 polyfill 来完成转译。

Babel 在 7.4.0 版本中废弃 @babel/polyfill ,改用 core-js 替代。

配置 useBuiltIns

@babel/preset-env中通过 useBuiltIns 参数来控制 built-in 的注入。可选值为 'entry'、'usage'、false 。默认值为 false,不注入 polyfill。

  1. false :默认值,不注入 polyfill。
  2. 'entry':需在整个项目的入口处手动引入 core-js ,import 'core-js'。该方式编译后 Babel 会把目标环境不支持的所有 built-in 都注入进来,不管这些 API 是否使用到,极大的增加打包后包的体积。
  3. 'usage':不需在项目的入口处手动引入,Babel 会在编译源码的过程中根据 built-in 的使用情况来选择注入相应的实现。如下图:
9.png

配置 corejs 版本

当 useBuiltIns 设置为 'usage' 或者 'entry' 时,还需要设置 @babel/preset-env 的 corejs 参数,用来指定注入 built-in 的实现时,使用 corejs 的版本。否则 Babel 日志输出会有一个警告。

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          edge: '17',
          firefox: '60',
          chrome: '67',
          safari: '11.1'
        },
        useBuiltIns: 'usage',
        corejs: '3.6.5',
        // 开启调试模式,编译日志会打印在控制台
        debug: true
      }
    ]
  ]
}

@babel/runtime

以 class 为例:

// source.js

class Person {
  name = 'jack'

  age = 18

  static skill = 'eat'
}

编译结果:

// dist.js

'use strict'

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

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
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,

      enumerable: true,

      configurable: true,

      writable: true
    })
  } else {
    obj[key] = value
  }

  return obj
}

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

  _defineProperty(this, 'name', 'jack')

  _defineProperty(this, 'age', 18)
})

_defineProperty(Person, 'skill', 'eat')

可以看到,编译后 built-in 类型在 文件中通过 require 引入了 polyfill,而 syntax 类型的语法则在文件中定义了_defineProperties、_createClass、_classCallCheck、_defineProperty四个 helper 函数,用于创建构造函数、设置属性、创建实例。这只是 class 语法的辅助函数,其他 syntax 类型的语法也会生成 helper 函数,比如:extends 关键字。然而真实的项目开发中,文件动辄几十、上百甚至上千,这样打包后的文件会非常大。

@babel/runtime 模块提供了 helper 函数的集合。在编译后的文件中不是再创建 helper 函数,而是引用@babel/runtime 提供的 helper 函数。使用该模块还需要用到 @babel/plugin-transform-runtime插件。

yarn add @babel/plugin-transform-runtime -D

yarn add @babel/runtime

@babel/runtime 是一个运行时依赖,所以不需要加-D 参数。

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: '3.6.5',
        debug: true
      }
    ]
  ],
  plugins: ['@babel/plugin-transform-runtime']
}

再次编译:

'use strict'

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

var _createClass2 = _interopRequireDefault(
  require('@babel/runtime/helpers/createClass')
)

var _classCallCheck2 = _interopRequireDefault(
  require('@babel/runtime/helpers/classCallCheck')
)

var _defineProperty2 = _interopRequireDefault(
  require('@babel/runtime/helpers/defineProperty')
)

var Person = /*#__PURE__*/ (0, _createClass2['default'])(function Person() {
  ;(0, _classCallCheck2['default'])(this, Person)
  ;(0, _defineProperty2['default'])(this, 'name', 'jack')
  ;(0, _defineProperty2['default'])(this, 'age', 18)
})

;(0, _defineProperty2['default'])(Person, 'skill', 'eat')

可以看到 helper 函数都是引用@babel/runtime 模块提供的 helper 函数了。

@babel/plugin-transform-runtime

该插件除了可以在文件中引用 helper 函数外,还为编译后的代码创建了一个沙箱环境,避免污染全局作用域。

比如通过引入 polyfill 的方式实现数组的 includes方法,编译后的结果是:

'use strict'

require('core-js/modules/es.array.includes.js')

var arr = [1, 2, 3]

arr.includes(3)

再看 core-js 中实现 includes 方法的源码可以看出直接往 Array.prototype 上增加 includes 方法,如下:


1.png

直接修改原型的会造成全局污染。假如开发的是工具库,被其它项目引用,而恰好该项目自身实现了 Array.prototype.includes 方法,那这样就造成了冲突。

去掉 presets 的corejsuseBuiltIns属性,同时也就不再需要 @babel/runtime 和 core-js 模块了。

module.exports = {
  presets: [
    [
      '@babel/preset-env'
    ]
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3
      }
    ]
  ]
}

给@babel/plugin-transform-runtime 插件设置 corejs 配置,包含三个值:

  • false 默认

  • 2:安装 @babel/runtime-corejs2

  • 3:安装 @babel/runtime-corejs3

根据 corejs 得安装对应的模块,编译后的结果如下:

'use strict'

var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault')

var _includes = _interopRequireDefault(
  require('@babel/runtime-corejs3/core-js-stable/instance/includes')
)

var arr = [1, 2, 3]

;(0, _includes['default'])(arr).call(arr, 3)

看出 _includes 仅仅是一个局部变量,不会对 Array.prototype 造成污染。

如何验证?

给 presets 配置 corejs 的方式,将编译后的文件在不支持 Array.prototype.includes 的浏览器中运行,在浏览器控制台或者引用该文件之后就可以使用[].includes,而给@babel/plugin-transform-runtime 插件配置 corejs 的方式,编译后的文件同样的浏览器运行时会报错,不存在此方法。

参考

Babel 文档
Babel 用户手册

你可能感兴趣的:(Babel)