【今日收获】—— 神奇的Babel

416 阅读6分钟

Babel是什么?

在Babel官方文档中,是这样介绍Babel的

Babel是一个JavaScript编译器,用于将ECMAScript 2015+ 版本的代码转换为向后兼容的JavaScript语法,以便能够运行到当前版本和旧版本的浏览器或者其他环境中。简单来说,Babel的工作就是:

  • 语法转换
  • JS 源码转换
  • 通过 Polyfill 的方式在目标环境中添加缺失的特性

Babel基本原理

Babel的基本原理就是将源码转换为抽象语法树,然后对抽象语法树进行处理生成新的语法树,最后将新语法树生成新的JS代码。整个编译过程可分为三个阶段:

parsing(解析)-->transforming(转换)-->generating(生成)

注意:Babel只负责编译新标准下引入的新语法,比如Class,Array Function,ES Moudle等,而不会编译原生对象新引入的方法和API,比如Array.includes,Map,Set等,这些需要通过ployfill解决。

Babel基本使用

运行Babel所需的基本环境

1.babel/cli或babel/node

npm install i -S @babel/cli

@babel/cli 是 Babel 提供的内建命令行工具。提到 @babel/cli 这里就不得不提一下 @babel/node ,这哥俩虽然都是命令行工具,但是使用场景不同,babel/cli 是安装在项目中,而 @babel/node 是全局安装。

2.@babel/core

npm install i -S @babel/core

安装完 @babel/cli 后就在项目目录下执行babel test.js会报找不到 @babel/core 的错误,因为 @babel/cli 在执行的时候会依赖 @babel/core 提供的生成 AST 相关的方法,所以安装完 @babel/cli 后还需要安装 @babel/core。

安装完这两个插件后,如果在 Mac 环境下执行会出现 command not found: babel,这是因为 @babel/cli是安装在项目下,而不是全局安装,所以无法直接使用 Babel 命令,需要在 package.json 文件中加上下面这个配置项:

"scripts": {
   "babel":"babel"
 }

此时代码依然无法被编码,因为还编译文件还需要安装对应的插件或者预设。

配置文件 .babelrc

Babel的配置文件是.babelrc,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件。

该文件用来设置转码规则和插件,基本格式如下。

{
  "presets": [],
  "plugins": []
}

presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。

# ES2015转码规则
$ npm install --save-dev babel-preset-es2015

# react转码规则
$ npm install --save-dev babel-preset-react

# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3

然后,将这些规则加入.babelrc。

  {
    "presets": [
      "es2015",
      "react",
      "stage-2"
    ],
    "plugins": []
  }

插件

插件是用来定义如何转换你的代码的。在 Babel 的配置项中填写需要使用的插件名称,Babel 在编译的时候就会去加载 node_modules 中对应的 npm 包,然后编译插件对应的语法。

.babelrc

{
  "plugins": ["transform-decorators-legacy", "transform-class-properties"]
}

插件执行顺序

插件在预设前执行

插件的执行顺序是从左往右执行。也就是说在上面的示例中,Babel 在进行 AST 遍历的时候会先调用 transform-decorators-legacy 插件中定义的转换方法,然后再调用 transform-class-properties 中的方法。

插件传参

参数是由插件名称和参数对象组成的一个数组。

{
    "plugins": [
        [
            "@babel/plugin-proposal-class-properties", 
            { "loose": true }
        ]
    ]
}

预设

预设就是一堆插件(Plugin)的组合,从而达到某种转译的能力,就比如 react 中使用到的 @babel/preset-react ,它就是下面几种插件的组合。

@babel/plugin-syntax-jsx @babel/plugin-transform-react-jsx @babel/plugin-transform-react-display-name

当然我们也可以手动的在 plugins 中配置一系列的 plugin 来达到目的,就像这样:

{
  "plugins":["@babel/plugin-syntax-jsx","@babel/plugin-transform-react-jsx","@babel/plugin-transform-react-display-name"]
}

但是这样一方面显得不那么优雅,另一方面增加了使用者的使用难度。如果直接使用预设就清新脱俗多了~

{
  "presets":["@babel/preset-react"]
}

预设的执行顺序

前面提到插件的执行顺序是从左往右,而预设的执行顺序恰好反其道行之,它是从右往左

前面提到插件的执行顺序是从左往右,而预设的执行顺序恰好反其道行之,它是从右往左

{
  "presets": [
    "a",
    "b",
    "c"
  ]
}

它的执行顺序是 c、b、a,是不是有点奇怪,这主要是为了确保向后兼容,因为大多数用户将 "es2015" 放在 "stage-0" 之前。

Polyfill

polyfill 的翻译过来就是垫片,垫片就是垫平不同浏览器环境的差异,让大家都一样。

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

安装命令如下。

$ npm install --save babel-polyfill

注意 @babel/polyfill 不是在 Babel 配置文件中配置,而是在我们的代码中引入。

import '@babel/polyfill';
const arrFun = ()=>{}
const arr = [1,2,3]
console.log(arr.includes(1))
Promise.resolve(true)

编译后:

require("@babel/polyfill");
var arrFun = function arrFun() {};
var arr = [1, 2, 3];
console.log(arr.includes(1));
Promise.resolve(true);

这样在低版本的浏览器中也能正常运行了。

不知道大家有没有发现一个问题,这里是require("@babel/polyfill")将整个 @babel/polyfill 加载进来了,但是在这里我们需要处理 Array.includes 和 Promise 就好了,如果这样就会导致我们最终打出来的包体积变大,显然不是一个最优解。要是能按需加载就好了。其实 Babel 早就为我们想好了

按需加载useBuiltIns

回过头来再说 @babel/preset-env,他出现的目的就是实现民族大统一,连 stage-x 都干掉了,又怎么会漏掉 Polyfill 这一功能,在 @babel/preset-env 的配置项中提供了 useBuiltIns 这一参数,只要在使用 @babel/preset-env 的时候带上他,Babel 在编译的时候就会自动进行 Polyfill ,不再需要手动的在代码中引入@babel/polyfill 了,同时还能做到按需加载

注意,这里需要配置一下 corejs 的版本号,不配置编译的时候会报警告。

useBuiltIns 的机构参数:

false:此时不对Polyfill 做操作,如果引入 @babel/polyfill 则不会按需加载,会将所有代码引入

usage:会根据配置的浏览器兼容性,以及你代码中使用到的 API 来进行 Polyfill ,实现按需加载

entry:会根据配置的浏览器兼容性,以及你代码中使用到的 API 来进行 Polyfill ,实现按需加载,不过需要在入口文件中手动加上import ' @babel/polyfill'

辅助函数的代码复用

@babel/plugin-transform-runtime 可以让 Babel 在编译中复用辅助函数,从而减小打包文件体积.

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

配置 Babel:

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