理解babel的基本原理和使用方法 一、babel的执行过程 二、插件 三、预设(preset)——babel的插件套装 四、配置 五、具体使用实例

  babel是一个编译器,用于将ECMA2015+代码转换为向后兼容的javascript语法,其原因在于目前浏览器并不能及时的兼容js的新语法,而开发过程中我们往往会选择es6、jsx、typescript进行开发,而浏览器并不能识别并执行这些代码,因此就必须将这些代码编译并转换成浏览器识别的代码,所以我们才会发现所有的项目构建工具都是使用babel,这就显示出来babel的重要性。虽然经常使用,但是每次使用都是使用固定的配置代码,却没有了解其执行原理。知其然就需要知其所以然,所以记下自己对于babel的理解。

  想要了解一个东西就需要先从宏观上分析它,babel也不例外。我们知道对于一个计算机语言,我们敲出来的代码都是字符串,想要执行就必须经过编译。比如react、vue等框架也需要对其类html代码进行编译才能生成viertual dom。既然babel需要将高级语法转换,那么babel也势必需要进行编译,而babel的执行过程就是一个编译转换的工程。如下图是babel的执行过程:

理解babel的基本原理和使用方法
一、babel的执行过程
二、插件
三、预设(preset)——babel的插件套装
四、配置
五、具体使用实例

   由此我们可以看到babel的核心就是parse、transform和generator三个部分。

  接下来我们采用babel中的插件演示一个最简单的例子。

var {parse} = require('@babel/parser');
var {default: generate} = require('@babel/generator');
var code = `const name = "jyy";`; // 原始代码
var ast = parse(code);  // 源代码生成的ast
var targetCode = generate(ast); // 将ast转成目标代码
console.log(targetCode);    //{ code: 'const name = "jyy";', map: null, rawMappings: undefined }

  parse和generate顾名思义就是编译器和生成器,你可能会发现缺少转换过程,而且生成的目标代码和原始代码都是一样的,只是多了属性而已,这相当于什么都没有做啊。那是因为转换过程的具体操作需要插件来实现,如果没有使用插件,最后生成的目标代码是和原始代码一样的。

  1.parse

  在babel中编译器插件是@babel/parser,其作用就是将源码转换为AST,使用前需要npm install @babel/parser,使用方法如下:

const babelParser = require("@babel/parser");
const code = "const name= 'jyy';";
const ast = babelParser.parse(code);
console.log(ast);

  执行后输出的结果如下:

 1 Node {
 2   type: 'File',
 3   start: 0,
 4   end: 12,
 5   loc:
 6    SourceLocation {
 7      start: Position { line: 1, column: 0 },
 8      end: Position { line: 1, column: 12 } },
 9   errors: [],
10   program:
11    Node {
12      type: 'Program',
13      start: 0,
14      end: 12,
15      loc: SourceLocation { start: [Position], end: [Position] },
16      sourceType: 'script',
17      interpreter: null,
18      body: [ [Node] ],
19      directives: [] },
20   comments: [] }

  这就是babel生成的ast结构,你可能会发现这个ast和平常我们看到的ast好像结构不同,因为没有发现“name"、“=”等词法单元。是的,我一开始看到的时候也在怀疑这个输出到底是不是ast。后来发现babel的parser是根据babel的AST结构生成的,他基于ESTree规范。于是深入输出发现了code的词法单元的位置,就在上面代码的标红处,可以使用ast.program.body[0],得到如下的输出:

理解babel的基本原理和使用方法
一、babel的执行过程
二、插件
三、预设(preset)——babel的插件套装
四、配置
五、具体使用实例

  从结果中我们看到了declarations(声明)关键字和我们源码中的const,接着继续输出declarations,结果如下:

理解babel的基本原理和使用方法
一、babel的执行过程
二、插件
三、预设(preset)——babel的插件套装
四、配置
五、具体使用实例

   可以看到这个结果确实将我们的源码构造成了一个AST。

  2.transform

  这个过程虽然在本文中名字叫transfom,但是事实上babel官网中并没有这个词,更没有称为转换器的结构。想要知道为什么没有,我们需要知道bable是一个工具链,所谓工具链就是babel是依赖于它的插件的,只有有了插件babel才能发挥出真正的作用,没有插件的babel只是会将源码生成AST,然后在通过生成器生成和原来的源码一摸一样的代码,这样的过程是没有任何作用的。插件发挥作用的地方基本都是在tranfrom这个过程,当源码通过parse生成了ast后,我们可以通过转换插件,对ast进行操作。比如@babel/plugin-transform-react-jsx是将react中的jsx转换为react的节点对象。这样这些插件都涉及到对ast的操作,babel提供了一些工具插件,让我们可以方便的操作ast节点,也就更方便我们开发适合自己项目的插件。比如在babel官网中设计到的插件,点这里。下面介绍两个比较重要的插件,同时用这两个实现一个比较简单的操作ast过程。

  @babel/types

  这个插件的api非常多,见这里,我也没有实际用过,在这里只是简单的介绍下了。它的作用是创建、修改、删除、查找ast节点,因为ast也是一个树状结构,我们可以像js操作dom节点一样,使用types对ast进行操作。

  另外我们知道ast的节点也是分为多种类型,比如ExpressionStatement是表达式、ClassDeclaration是类声明、VariableDeclaration是变量声明等等,同样的这些类型都对应了其创建方法:t.expressionStatement、t.classDeclaration、t.variableDeclaration,也对应了判断方法:t.isExpressionStatement、t.isClassDeclaration、t.isVariableDeclaration。这个插件往往和traverse遍历插件一起使用,因为types只能对单一节点进行操作,一般是在对节点的迭代中使用,所以这个插件的例子会放在traverse的实例中。

  @babel/traverse

  这个插件的作用是对ast进行遍历parse,在迭代的过程中可以定义回调函数,回调函数的参数提供了丰富的增、删、改、查以及类型断言的方法,比如replaceWith/remove/find/isMemberExpression。下面以一个例子结合types和traverse进行演示。

  假设我们在开发过程中使用一个函数findEleById来代替document.getElementById,如果我们直接使用findEle而不对其进行处理,js代码执行过程中是会报错,因为window下是没有这个函数的。但是我们可以使用babel修改其ast,将findEleById改为document.getElementById,这样babel的生成器生成的最新代码就是document.getElementById,然后js引擎就可以编译通过了。当然这个过程对于开发者是隐藏的,开发者只需要关注于使用findEleById便捷的开发就可以了,后续的操作交给babel。见如下代码:

var t = require('@babel/types');
var {parse} = require('@babel/parser');
var {default: traverse} = require('@babel/traverse');
var {default: generate} = require('@babel/generator');

var orginCode = `findEleById("jyy")`;   // 原始代码
// 生成原始AST
var originAST = parse(orginCode, {
    sourceType: "module"
});
// 对AST进行遍历并操作
traverse(originAST,{
    Identifier(path){
        var {node} = path;
        // 找到findEleById,将其替换成为目标节点
        if(node && node.name === "findEleById"){
            var newNode = t.memberExpression(t.identifier("document"), t.identifier("getElementById")); // 创建目标节点
            path.replaceWith(newNode);  // 替换原始节点
            path.stop();
        }
    }
});
const targetCode = generate(originAST, { /* options */ }, orginCode);   // 将转换后的AST生成目标代码
console.log(targetCode);    // { code: 'document.getElementById("jyy");',map: null,rawMappings: undefined }

  从上面代码可以看到基本的转换过程,生成的最终代码可以直接交付给浏览器引擎编译执行了。

  在babel的工具插件还有一些,因为本文不是为了讲解如何开发babel插件,所以这里仅介绍以上两个插件只为介绍babel在transform阶段的基本的工作原理。如果你真的需要开发自己的babel插件,那么需要了解babel提供的插件们,并了解其api的使用。

  3.generator

  这个过程已经在上面的实例中有所展现,使用的插件是@babel/generator,作用就是将转换好的ast重新生成代码。这样的代码就就可以安全的在浏览器运行。

  4.babel-core——整合基本插件

  我们发现基本的babel插件如@babel/parse、@babel/generator都是提供了代码转换的基本功能,而另外的一些工具类型的插件比如@babel/types、@babel/traverser起作用是提供操作ast节点的功能。然而在开发插件的过程中如果每个都需要去引入实在太麻烦,所以就有了@babel/core插件,顾名思义就是核心插件,他将底层的插件进行封装,并另外加入了其他功能,比如读取、分析配置文件,这个后面会在配置中讲到。而这个插件将复杂的过程进行简化,如下代码所示:

1 var babel = require("@babel/core");
2 var code = "<div class='c'>jyy</div>";  // 代码
3 babel.transform(code,{plugins: ["@babel/plugin-transform-react-jsx"],},function(err, result){
4     console.log(result.code);   
5     // React.createElement("div", {
6     //   class: "c"
7     // }, "jyy");
8 });

  可以发现,我们可以使用transform就可以完成整个步骤。另外我们查看core的依赖可以发现,它依赖于底层的插件并基于次进行进行封装:

理解babel的基本原理和使用方法
一、babel的执行过程
二、插件
三、预设(preset)——babel的插件套装
四、配置
五、具体使用实例

   core的api很多,可以查看babel官网,另外我们看transform函数的参数第一个为原始代码,第二个为用于在转换过程中对ast进行操作的插件,例子中我们使用的是转换jsx的插件,第三个参数是一个回调函数。

二、插件

  我们一般不会自己开发babel的转换插件,实际项目中往往都是直接使用现成的插件。而配置插件却时常让人很烦闷,因为有各种插件,什么stage-1、env、es2015等等,各种插件的各种配合设置给人摸不到头脑的感觉,会想问为什么不能出一个统一的插件,里面包含所有的转换功能,这样在配置的时候只需要在plugins里面放一个插件名就好了呢?这个问题主要是有以下几点原因:

  一是因为js发展太快了,我们知道这几年js新的语法和函数不断地出现,如果把所有对最新语法的转换放在一个插件中,那么每次出现新的语法就需要不断的修改代码

  二是babel不仅仅包含对ecma2015+的转换,还包括ts、flow和上面我们提到的jsx的转换,全都揉在一起的话实在是太大太乱了,不易于维护

  三是这样做可以让用户更*地去选择,就像菜市场去买菜,我只会去买我想要买的菜。这无疑减少了项目打包时的负担,进而影响到所占用的网络的带宽。事实上这种方式广泛的存在于产品中。比如echarts,我们可以选择其中的某些图表,然后下载对应的代码,而这样做的前提就是降低模块之间的耦合。

  以上的分析都是我在瞎扯淡,仅供参考,谁知道babel的开发者是怎么想的。但至少是本人的一个见解。

  一般情况下,项目不需要我们去开发babel插件,因为这些插件都写好了,看官网。里面的插件非常多,而且有些插件貌似很小,仅仅具体到一个语法,比如arrow-function,这是ES2015中的箭头函数语法,下面使用这个插件进行演示:

var babel = require("@babel/core");
var code = `num => {
   return num ** 2;
}`;  // 代码
babel.transform(code,{plugins: ["@babel/plugin-transform-arrow-functions"]},function(err, result){
    console.log(result.code);
    // (function (num) {
    //     return num ** 2;
    // });
});

  结果如我们预期,使用arrow-function插件后,确实将箭头函数转换为了es5的语法。但是我们还会发现代码中的**,幂等运算符是ES2016的新特性。这样的代码还是不能被es5识别,于是我们加入转换幂等运算符的插件:

var babel = require("@babel/core");
var code = `num => {
   return num ** 2;
}`;  // 代码
babel.transform(code,{plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-exponentiation-operator"]},function(err, result){
    console.log(result.code);
    // (function (num) {
    //     return Math.pow(num, 2);
    // });
});

  发现结果已经将幂等运算符进行了转换。对于插件的配置需要记住以下几点:

  1.plugin的段名称

  配置plugin的时候,可以设置插件的短名称,可以将省略babel-plugin,例如:

  ["@babel/babel-plugin-name"]和["@babel/name"]是等价的

  2.排列顺序

  多个插件的执行顺序是按照从前到后的顺序执行,例如["@babel/name1","@babel/name2"]两个插件的执行顺序是先执行name1,然后执行name2。

三、预设(preset)——babel的插件套装

  那么问题来了新语法新特性那么多,难道我们要挨个去加吗?当然不是,babel已经预设了几套插件,将最新的语法进行转换,可以使用在不同的环境中,如下:

  @babel/preset-env
  @babel/preset-flow
  @babel/preset-react
  @babel/preset-typescript

  从名字上就能看出他们使用的环境了,需要注意的是env,他的作用是将最新js转换为es6代码。预设是babel插件的组合,我们可以看下package.json(截取一部分):

理解babel的基本原理和使用方法
一、babel的执行过程
二、插件
三、预设(preset)——babel的插件套装
四、配置
五、具体使用实例

   由此看到他组合了很多的插件,是一个官方提供的,这样我们只需要使用一个插件就可以了。那么有了这个插件,我们使用上一个例子,来测试一下:

var babel = require("@babel/core");
var code = `num => {
   const offset = 23;
   return num ** 2 + offset;
}`;  // 代码
babel.transform(code,{presets: ["@babel/preset-env"]},function(err, result){
    console.log(result.code);
    // "use strict";
    // (function (num) {
    //     var offset = 23;
    //     return Math.pow(num, 2) + offset;
    // });
});

  可以看到,代码中额外将const转为var,还加上了use strict

  需要注意的是因为@babel/preset-env是预设的包含多个插件,所以不同于单一的插件,需要使用presets参数,如代码红色标记所示。

  对于env插件,我们还需要知道他是以前es2015、es2016和es2017的集合,另外他默认不支持stage-x插件

  stage-x(babel7已废弃)

  那么什么是stage-x呢?state-x里面包含了当年最新规范的草案,每年更新。因为有可能项目所使用的是最新的语法,那么官方的预设插件还没有将其纳入,这时候就需要使用state-x。如下是state-x的阶段:

  • Stage 0 - 稻草人: 只是一个想法,经过 TC39 成员提出即可。
  • Stage 1 - 提案: 初步尝试。
  • Stage 2 - 初稿: 完成初步规范。
  • Stage 3 - 候选: 完成规范和浏览器初步实现。
  • Stage 4 - 完成: 将被添加到下一年度发布。

  所以我们经常会在代码中看到这样的preset配置:[es2015, react, stage-0]。好在在babel7,stage-x以被废弃,详情点这里

  额外注意的

  1.preset可以设置短名称 

  和插件一样preset也可以设置段名称。可以省略preset,例如:

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

  可以省略为:

{
    presets: ["@babel/env", "@babel/react"] 
}

  2.排列顺序

  预设的执行顺序也同样重要,preset在plugin之前执行,而且和plugin不同的是,preset是从后往前执行,比如我们使用react,那么应该这么写:

{
    presets: ["@babel/env", "@babel/react"]
}

  因为我们需要将react中的jsx转为js,然后将js在转换为es5,所以需要将react的插件放在后面,让他先执行。

四、配置

   实际项目中我们不会亲自动手去调用babel的api去转换代码,而且如果我们整个项目很可能都是用es6编写,不可能手动调用babel的api去一个一个转换,我们希望使用命令行,通过传递文件夹的名称去交给babel转换。这个时候babel-cli就出现了,比如,我们想要转换某个文件夹下的文件,那么我们可以在控制台输入这样的命令:

babel src --out-dir lib --presets=@babel/preset-env,@babel/react

  这段命令的意思是,将src文件夹下的所有文件使用env和react预设进行转换,并且将转换后的文件存放在lib文件夹下。这样就节省了我们很多的时间。需要注意的是:babel-cli只是一套命令,想要执行babel的转换工作,仍然需要引入babel-core。

  此时仍然有个问题预设也是有参数的,另外还有plugins等等,当然可以将这些参数加在命令中,但是这样还是会很复杂。解决这个问题的通用方法就是在项目中创建一个配置文件,在里面配置相应的插件和预设。

  1. .babelrc

  只是项目中经常用到的方式,在项目根目录创建名为.babelrc文件,内部包括两个方面:plugins和p'resets

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

  2. babel.config.js

  这种方式是使用js代码编写,并导出一个和上面方式相同的对象

1 module.exports = function () {
2   const presets = [ ... ];
3   const plugins = [ ... ];
4   return {
5     presets,
6     plugins
7   };
8 }

  3. package.json

  这种该方式是在项目配置文件package.json中进行配置,如下:

1 {
2   "name": "my-package-babel",
3   "version": "1.0.0",
4   "babel": {
5     "presets": [ ... ],
6     "plugins": [ ... ],
7   }
8 }

  这三种都是等效的,当配置完成后,可以在babel-cli的命令行中配置,比如:

babel --config-file /path/to/my/babel.config.json --out-dir dist ./src

  事实上,真正收取并处理分析配置文件的还是@babel/core,在core源码里面对于支持的配置文件名如下:

理解babel的基本原理和使用方法
一、babel的执行过程
二、插件
三、预设(preset)——babel的插件套装
四、配置
五、具体使用实例

五、具体使用实例

  下面以真实项目的搭建为例,简单介绍babel的具体使用方式。

  项目的框架使用react,构建工具是webpack。webpack有babel-loader的插件,加入这个loader之后表示所有打包的文件都会由babel-loader来处理,同时使用到的babel插件有@babel/core、@babel/preset-env、@babel/preset-react。此时webpack中的配置文件关于babel的配置如下:

  webpack.config.js:

1 {
2     test: ".js$",
3     loader: "babel-loader",
4     exclude: "/node_modules"
5 }

  .babelrc

{
    "presets": ["@babel/env", "@babel/react"],
    "plugins": ["transform-runtime"]
}