likes
comments
collection
share

了解一下Babel?

作者站长头像
站长
· 阅读数 14

前言

Babel在前端拥有着举足轻重的地位,几乎所有大型的项目都离不开Babel。也许你配置过Babel,但是配置工程师向来都只是一个起点,只是浅尝辄止的研究往往都是知其然不知其所以然。所以,让我们开始吧,真正的去了解和拥抱Babel

Babel是什么

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。它需要完成以下内容:

  • 语法转换,一般是指高级语言特性的降级
  • 通过 Polyfill(例如core-js) 方式在目标环境中添加缺失的功能
  • 源码转换,比如JSX

总的来说,编译是Babel的核心,所以Babel它自身的实现就是基于编译原理,深入AST来生成目标代码,同时通过各种工具(比如Webpack, core-js)来相互配合,进行工程化协作,最后构建出符合产出的良好项目结构代码。

Babel包解析

Babel本身是一个使用Lerna构建的Monorepo风格的仓库,其中包含了上百个包,Babel的包大致分为两种:

  • 一种是在工程本身上起作用的,对于业务而言并不透明的,比如支持@babel/core的能力的包例如@babel/preser@babel/code-frame等等
  • 一种是外供给工程使用的,对于业务而言是透明的可用的,比如编写插件使用的@babel/types,配置时候经常使用的@babel/preset-env等等。

下面,我们对Babel中比较重要的包进行梳理和简单讲解。

@babel/core

@babel/coreBabel实现转换的核心,它可以根据配置实现源码的编译转换,提供了基础的编译能力。而它的能力是由更底层的@babel-parser@babel-traverse@babel-generator@babel/types等包所提供。这些基础的包提供了基础的AST处理能力。

下面,我们一一分析每个包的作用和功能。

  • @babel-parserBabel用来对JavaScript进行解析的解析器,它提供了parse()方法用来将源码编译成一个AST语法树。
  • 有了语法树,我们就需要对这个AST进行遍历和修改,这个时候,@babel-traverse提供了对AST遍历的功能,而@babel/types提供了对AST节点修改的能力(后面我们会使用这个包来自定义插件)。
  • 修改完成AST后,我们还需要将这个AST进行聚合生成符合要求的JavaScript代码,这个能力是由@babel-generator提供的。

以上就是典型的Babel底层编译的流程了。

了解一下Babel?

@babel/cli

@babel/cliBabel提供的命令行,可以在终端中通过命令行的方式运行、编译。其原理很简单,就是使用commander库搭建基本的命令行,@babel/cli主要是负责获取配置内容,最终依赖@babel/core来完成编译。

@babel/preset-env

相比于前面两个插件,想必大家更熟悉@babel/preset-env,因为我们会通过@babel/preset-env来配置编译降级,@babel/preset-env允许我们配置需要支持的目标环境(浏览器环境或者node环境),利用babel-polyfill完成补丁接入。@babel/preset-env的实现原理也比较简单,主要分三步:

  1. @babel/preset-env 会收集目标环境的特性支持情况。这包括浏览器或 Node.js 的版本号,以及目标环境中支持的 ECMAScript 特性和语法(主要是调用@babel/helper-compilation-targets支持)。
  2. @babel/preset-env 会根据收集到的特性支持情况,确定需要转换的特性和语法。这包括需要转换的 ECMAScript 版本、需要转换的特性和语法等(主要是调用@babel/preset-env/lib/normalize-options支持)。
  3. @babel/preset-env 会根据确定的需要转换的特性和语法,加载相应的插件,并对代码进行转换。这包括语法转换、特性转换、模块转换等等。

Babel的使用

上面简单介绍了一下Babel的部分插件包,现在我们自己动手完成一个Babel的基础使用,以及Babel插件的编写。

初始化

我们新建一个文件夹,然后初始化项目

npm init -y

上面提到,Babel的使用离不开@babel/core@babel/cli这两个,所以,我们安装一下:

npm install @babel/core @babel/cli -D

接着,我们在根目录创建两个文件夹srcdist,分别用来当做源码目录,和打包后的输出目录。

现在,我们在src下创建index.js:

const add = (a, b) => a+b;

同时在package.json中增加以下配置:

"scripts": {
    "babel": "babel ./src --out-dir ./dist"
  },

这个时候,我们运行npm run babel进行编译,可以看到,编译成功了,并且在dist目录下生成了编译后的index.js文件。

引入插件

细心的同学会发现,上面编译后的代码并没有将箭头函数编译成普通函数,这是因为上面提到的问题,Babel本身拥有上百个包,每个包各司其职,因为Babel不可能毫无设计的大包大揽,Babel是可插拔的。

为了解决箭头函数的“问题”,我们需要使用@babel/plugin-transform-arrow-functions,所以,我们安装一下:

npm install @babel/plugin-transform-arrow-functions -D

接着,增加文件.babelrc(当然还有其他的配置方式,可以看官网):

{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

我们再运行npm run babel,可以看到dist下面的index.js编译成了如下:

const add = function (a, b) {
  return a + b;
};

这个效果是符合预期的。

现在,修改一下src/index.js:

const add = (a, b) => a+b;

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`Hello, ${this.name}!`);
  }
}

const person = new Person('Alice');
person.sayHello();

运行npm run babel,发现编译成功了,但是Class并没有被转换,原因如上,我们缺少了对Class进行转换的插件,安装一下:

npm install @babel/plugin-proposal-class-properties -D

需要注意的是,在某些情况下,Babel 会根据目标环境的支持情况,自动判断是否需要进行转换。如果目标环境已经支持某种语法,Babel 就不会对其进行转换,这样可以减少代码的体积和运行时性能开销。所以我们还需要引入一下我们最常使用的@babel/preset-env,来明确我们的环境:

npm install @babel/preset-env -D

修改.babelrc:

{
  "presets": [
    [
      "@babel/preset-env", 
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 11"]
        }
      }
    ]
  ],
  "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-arrow-functions"]
}

需要注意一下, .babelrcplugins是从左向右执行的,上面的配置就是先对类进行转换,再对箭头函数进行转换。而presets是从右向左执行,刚好相反。

运行npm run babel,我们发现打包的文件中class也被转换了。

编写插件

上面我们通过@babel/plugin-proposal-class-properties@babel/plugin-transform-arrow-functions熟悉了Babel中如何使用人们编写好的插件,那么我们是否可以自己去写一个Babel的插件来实现特定的功能呢,答案显示是可以的。那么,我们就开始编写一个属于自己的插件吧。

编写的插件功能: 将编译的文件中的加法变成减法:例如 var a = 2 + 1,编译后变成 var a = 2 - 1

如何加载插件

第一种方式,就是我们和别的插件一样,发布到npm上,然后下载下来,在.babelrc中配置好插件名称就好了(当然也可以使用npm link来将本地的插件软链接过来,详细的使用可以参考我另一篇文章:npm核心原理和操作指南)。

另一种方式,直接在项目中编写插件,然后在.babelrc中通过访问相对路径的方式来加载插件。

我们采用第二种方式,首先,我们在根目录下创建一个文件夹plugins,然后在该文件夹下增加一个conversion-calc.js:

module.exports = function (babel) {
  // 这个types就是@babel/types,可以用来修改AST的节点
  const { types } = babel;
  return {
    visitor: {
      BinaryExpression(path) { //需要处理的节点路径
        let node = path.node;
        let left = node.left;
        let operator = node.operator;
        let right = node.right;
        if (!isNaN(left.value) && !isNaN(right.value) && operator === '+') {
          // binaryExpression 是一个 AST(抽象语法树) 节点类型,表示二元表达式
          const expression = types.binaryExpression('-', left, right);
          // 替换
          path.replaceWith(expression);
        }
      }
    }
  }
}

这个插件很简单,上面注释已经解释了,只要跟着API来即可。

现在,我们将这个插件放入.babelrc:

{
  "presets": [
    [
      "@babel/preset-env", 
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 11"]
        }
      }
    ]
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-arrow-functions",
    "./plugins/conversion-calc.js"
  ]
}

然后,我们在src/index.js中增加一行代码: var a = 2 + 1;

运行npm run babel,发现dist/index.js编译后如下:

"use strict";

/// ...
var person = new Person('Alice');
person.sayHello();
var result = 1 - 2;

可以看到,我们的插件起作用了~~~

AST部分这边先略过了,下篇文章会详细的讲解,如果想看AST的结构,可以访问 AST操练场

预设

前面,我们已经在.babelrc中使用了预设(presets),预设其实是一组预定义的转换规则集合,可以在一个预设中包含多个转换规则和插件,从而简化配置过程。比如我们使用的@babel/preset-env,这个预设包含了一组转换规则和插件,无需单独指定每个转换规则和插件,用于根据目标环境自动确定需要转换的 ECMAScript 特性和语法,并将其转换为向后兼容的代码。

典型的预设

@babel/preset-stage-xxx

@babel/preset-stage-xxx用于提供 ECMAScript 的实验性特性。这些预设包含了一些还没有被正式纳入 ECMAScript 规范中,但是已经被提案并且正在积极开发和测试的特性。

这些预设按照提案的阶段进行分类,分别为 Stage 0Stage 1Stage 2Stage 3。其中,Stage 0 表示最初的草案,而 Stage 3 表示即将成为 ECMAScript 规范的最终草案。通常来说,只有 Stage 3 的特性才有可能被纳入下一个 ECMAScript 规范中。

需要注意的是,从 Babel 7.0 开始,这些预设已经被废弃,不再建议使用。如果你需要使用实验性特性,可以直接使用对应的插件,例如 @babel/plugin-proposal-xxx 等,以确保更好的灵活性和可控性。其实我们并不需要使用这些预设,因为我们已经有@bebel/preset-env

@babel/preset-env

@babel/preset-env是根据浏览器的不同版本中缺失的功能确定代码转换规则的,在配置的时候我们只需要配置需要支持的浏览器版本就好了,@babel/preset-env会根据目标浏览器生成对应的插件列表然后进行编译:

{
  "presets": [
    [
      "@babel/preset-env", 
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 11"]
        }
      }
    ]
  ]
}

在默认情况下 @babel/preset-env 支持将 JS 目前最新的语法转成 ES5,但需要注意的是,如果你代码中用到了还没有成为 JS 标准的语法,该语法暂时还处于 stage 阶段,这个时候还是需要安装对应的 stage 预设,不然编译会报错。

{
  "presets": [
    [
      "@babel/preset-env", 
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 11"]
        }
      }
    ]
  ],
  "stage-0"
}

虽然可以采用默认配置,但如果不需要照顾所有的浏览器,建议配置目标浏览器和环境,这样可以保证编译后的代码体积足够小,因为在有的版本浏览器中,新语法本身就能执行,不需要编译。

Polyfill

上面有提到,Babel除了对高级语言降级已适配当前环境外,还可以通过 Polyfill(例如core-js) 方式在目标环境中添加缺失的功能。那么,我们来实验一下。

首先,我们安装一下插件包:

npm install @babel/polyfill -D

需要注意的是,@babel/polyfill是在我们的代码中引入,而不是再.babelrc中配置,为了验证@babel/polyfill,我们在src目录增加一个test-polyfill.js:

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

这个时候,运行npm run babel看看dist/test-polyfill.js:

"use strict";

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

这样在低版本的浏览器中也能正常运行,只是我们可以看到全量的引入了polyfill,但是我们在这里其实只需要处理Array.includesPromise,其他的并不需要,所以按需引入才是最优解。

按需引入其实非常简单,@babel/preset-env已经为我们想好了,只需要配置useBuiltInscorejs即可,我们修改.babelrc如下:

{
  "presets": [
    [
      "@babel/preset-env", 
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 11"]
        },
        "corejs": "3",
        "useBuiltIns": "usage"
      }
    ]
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-arrow-functions",
    "./plugins/conversion-calc.js"
  ]
}

同时,我们把dist/test-polyfill.js中的引入删掉,再运行npm run babel,可以看到编译后的dist/test-polyfill.js:

"use strict";

require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
var arr = [1, 2, 3, 4, 5];
console.log(arr.includes(3));
Promise.resolve(true);

可以看到,已经做到按需加载了。

结语

Babel的整个过程就差不多讲完了,希望能够帮助到大家~~~

转载自:https://juejin.cn/post/7256425827331997754
评论
请登录