Babel 插件起手式

前言

据圣经记载,曾经有一种很高很高的塔,是由一群说着一样语言、勤劳而又团结的人民兴修的,他们但愿由此能通往天堂,上帝拦阻了人的计划,是出于爱和保护,让人依靠上帝认识上帝,因而将他们的语言打乱,让他们不再能明白对方的意思,并把他们分散到了世界各地。所以曾经高耸入云的塔,被世人称做“巴别塔(Babel)”,也称为混乱之塔。javascript

木秀于林,风必摧之,JavaScript 也没能逃过这种命运。它自诞生以来,以迅雷不及掩耳之势,凭借着自身的灵活性与易用性,在浏览器端大放异彩,普遍的应用于不一样标准的各个浏览器。但是好景不长,一个被称做 ECMA 的邪恶组织在暗中不断对 JavaScript 进行着实验,将其培养为恐怖的生化武器。科学家们们为了知足各自的私欲,在 ES4 上集成了各自所需的特性,以此想要达成对语言规范的控制权,可被寄予厚望的 ES4 仍是没能顶住压力,最终因难产而死。为了继续将实验进行下去,名为 DC 和 M$ 的科学家起了一个更为保守、渐进的提案,被人们普遍接受并时隔两年问世,称为 ES5。长期以来,名为 TC39 的实验室在暗中制定了 TC39 process 流水线,它包含 5 个 Stage:java

  • Stage 0Strawman阶段)- 该阶段是一个开放提交阶段,任何在TC39注册过的贡献都或TC39成员均可以进行提交
  • Stage 1Proposal阶段)- 该阶段是对所提交新特性的正式建议
  • Stage 2Draft阶段)- 该阶段是会出现标准中的第一个版本
  • Stage 3Canidate阶段)- 该阶段的提议已接近完成
  • Stage 4Finished阶段)- 该阶段的会被包括到标准之中

自 2015 年来,JavaScript 迈入了一个崭新的 ES6 纪元,它表明着集众家之长的 ES2015 的问世,这使得 JavaScript 它不只拥有了本身的 ES Module 规范,还解锁了 Proxy、Async、Class、Generator等特性,它已经逐渐成长为一个健壮的语言,而且凭着高性能的 Node 框架开始占领服务端市场,近几年携手 React Native 角逐移动开发,它高喊着自由、民主,逐渐俘获一个又一个少年少女的心扉。node

任何语言都依赖于一个执行环境,对于 JavaScript 这样的脚本语言来说,它始终依赖于 JavaScript 引擎,而引擎通常会附带在浏览器上,不一样浏览器间的引擎版本与实现是不一样的,所以就很容易带来一个问题——各个浏览器对 JavaScript 语言的解析结果上会有很大的不一样。对于开发者而言,咱们须要放弃语言新特性并写出兼容代码以此来支持不一样的浏览器用户的使用;对于用户来说,强制用户更换最新浏览器是不合理也不现实的。git

这种情况直到 Babel 的出现才得以解决,Babel 是一个 JavaScript 编译器,主要用于将 ES2015+ 语法标准的代码转换为向后兼容的版本,以此来适应老版本的运行环境。Babel 不只是一个编译器,它更是 JavaScript 走向统1、标准化的桥梁,软件开发者可以以偏好的编程语言或风格来写做源代码,并将其利用 Babel 翻译成统一的 JavaScript 形式。github

Babel 是混乱诞生之地,同时也是混乱终结之地,为了世界的和平,咱们都须要尝试学习一下 Babel 插件的基础知识,以备不时之需。编程

抽象语法树

在计算机科学中,抽象语法和抽象语法树实际上是源代码的抽象语法结构的树状表现形式,又称为 AST(Abstract Syntax Tree)。AST 经常使用来进行语法检查、代码风格的检查、代码的格式、代码的高亮、代码错误提示、代码自动补全等,它的应用十分普遍,在 JavaScript 里 AST 遵循 ESTree 的规范。json

为了直观展现,咱们先来定义一个函数:数组

function square(n) {
  return n * n;
}
复制代码

它的 AST 转换结果以下(省略了一些空字段和位置字段):浏览器

{
  "type": "File",
  "program": {
    "type": "Program",
    "body": [
      {
        "type": "FunctionDeclaration",
        },
        "id": {
          "type": "Identifier",
            "identifierName": "square"
          },
          "name": "square"
        },
        "params": [
          {
            "type": "Identifier",
            "name": "n"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "body": [
            {
              "type": "ReturnStatement",
              "argument": {
                "type": "BinaryExpression",
                "left": {
                  "type": "Identifier",
                  "name": "n"
                },
                "operator": "*",
                "right": {
                  "type": "Identifier",
                  "name": "n"
                }
              }
            }
          ],
        }
      }
    ],
  },
}
复制代码

AST 既然是树形结构,那咱们就能够将它看做是一个个 Node,每一个 Node 都实现了如下规范:bash

interface Node {
  type: string;
  loc: SourceLocation | null;
}
复制代码

type 表示不一样的语法类型,上面的 AST 中具备 FunctionDeclaration、BlockStatement、ReturnStatement 等类型,咱们能够经过每一个 Node 中的 type 字段进行分别,全部 type 可见文档

工做流程

经过配置 Babel 的 presets、plugin等信息,Babel 会将源代码进行特定的转换,并输出更为通用的目标代码,其中最主要的三部分为:编译(parse)、转换(transform)、生成(generate)。

image.png

编译

Babel 的编译功能主要由 @babel/parser 完成,它的最终目标是转换为 AST 抽象语法树,在此过程当中主要包含两个步骤:

  1. 词法分析(Lexical Analysis),它会将源代码转换为扁平的语法片断数组,也称做令牌流(tokens)
  2. 语法分析(Syntactic Analysis),它将上阶段获得的令牌流转换成 AST 形式

为了获得编译结果,咱们引入 @babel/parser 包,对一段普通函数进行编译,而后查看打印结果:

import * as parser from '@babel/parser';

function square(n) {
  return n * n;
}

const ast = parser.parse(square.toString());
console.log(ast);
复制代码

转换

转换步骤会对 AST 进行节点遍历,并对节点进行 CRUD 操做。在 Babel 中是经过 @babel/traverse 完成的,咱们接着上一段代码的编译过程进行编写,咱们但愿将 n * n ,转化为 Math.pow(n, 2) :

import traverse from '@babel/traverse';
// ...
const ast = parser.parse(square.toString());

traverse(ast, {
  enter(path) {
    if (t.isReturnStatement(path.parent) && t.isBinaryExpression(path.node)) {
      path.replaceWith(t.callExpression(
        t.memberExpression(t.identifier('Math'), t.identifier('pow')),
        [t.stringLiteral('n'), t.numericLiteral(2)]
      ))
    }
  }
});

console.log(JSON.stringify(ast));
复制代码

在此过程当中,咱们使用了 @babel/types 用来作类型判断与生成指定类型的节点。

生成

在 Babel 中主要是用 @babel/generator 进行生成,它将通过转换的 AST 从新生成为代码字符串。根据上面 Demo,改写下代码:

import generator from '@babel/generator';
// ...同上
console.log(generator(ast));
复制代码

最终咱们获得了转化后的代码结果:

{ 
  code: 'function square(n) {\n return Math.pow("n", 2);\n}',
  map: null,
  rawMappings: null
}
复制代码

插件构造

咱们先来看来定义一个插件基本结构:

// plugins/hello.js
export default function(babel) {
  return {
    visitor: {}
  };
}
复制代码

而后咱们在配置文件中能够按如下方式进行简单引用:

// babel.config.js
module.exports = { plugins: ['./plugins/hello.js'] };
复制代码

visitor

在插件中,有个 visitor 对象,它表明访问者模式,Babel 内部是经过上面提到的 @babel/traverse 进行遍历节点,咱们能够经过指定节点类型进行访问 AST:

module.exports = function(babel) {
  return {
    visitor: {
      Identifier(path) {
        console.log('visiting:', path.node.name)
      }
    }
  };
};
复制代码

这样当进行编译 n * n 时,就能看到两次输出。visitor 也提供针对节点的 enter 与exit 访问方式,让咱们改写下程序:

visitor: {
      Identifier: {
        enter(path) {
          console.log('enter:', path.node.name);
        },
        exit(path) {
          console.log('exit:', path.node.name);
        }
      }
    }
复制代码

这样一来,再编译刚才的程序,就有了 4 次打印,visitor 是按照 AST 的自上到下进行深度优先遍历,进入节点时会访问节点一次,退出节点时也会访问一次。让咱们写一段代码来测试一下 traverse 的访问顺序:

import * as parser from '@babel/parser';
import traverse from '@babel/traverse';

function square(n) {
  return n * n;
}
const ast = parser.parse(square.toString());

traverse(ast, {
  enter(path) {
    console.log('enter:', path.node.type, path.node.name || '');
  },
  exit(path) {
    console.log('exit:', path.node.type, path.node.name || '');
  }
});
复制代码

打印结果:

enter: Program
enter: FunctionDeclaration
enter: Identifier square
exit: Identifier square
enter: Identifier n
exit: Identifier n
enter: BlockStatement
enter: ReturnStatement
enter: BinaryExpression
enter: Identifier n
exit: Identifier n
enter: Identifier n
exit: Identifier n
exit: BinaryExpression
exit: ReturnStatement
exit: BlockStatement
exit: FunctionDeclaration
exit: Program
复制代码

path

path 做为节点访问的第一个参数,它表示节点的访问路径,基础结构是这样的:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "..."
  }
}
复制代码

其中 node 表明当前节点,parent 表明父节点,同时 path 还包含一些 node 元信息和操做节点的一些方法:

  • findParent  向父节点搜寻节点
  • getSibling 获取兄弟节点
  • replaceWith  用AST节点替换该节点
  • replaceWithMultiple 用多个AST节点替换该节点
  • insertBefore  在节点前插入节点
  • insertAfter 在节点后插入节点
  • remove   删除节点

路径是一个节点在树中的位置以及关于该节点各类信息的响应式 Reactive 表示。 当你调用一个修改树的方法后,路径信息也会被更新。 Babel 帮你管理这一切,从而使得节点操做简单,尽量作到无状态。

opts

在使用插件时,用户可传人 babel 插件配置信息,插件再根据不一样配置来处理代码,首先,在引入插件时,修改成数组引入方式,数组中第一个对象为路径,第二个元素为配置项 opts:

module.exports = {
  presets,
  plugins: [
    [
      './src/plugins/xxx.js',
      {
        op1: true
      }
    ]
  ]
};
复制代码

在插件中,可经过 state 进行访问:

module.exports = function(babel) {
  return {
    visitor: {
      Identifier: {
        enter(_, state) {
          console.log(state.opts)
          // { op1: true }
        }
      }
    }
  };
};

复制代码

nodes

当在编写 Babel 插件时,咱们时常须要对 AST 节点进行插入或修改操做,这时可使用 @babel/types 提供的内置函数进行构造节点,如下两种方式等效:

import * as t from '@babel/types';
module.exports = function({ types: t }) {}
复制代码

构建 Node 的函数名一般与 type 相符,除了首字母小写,好比构建一个 MemberExpression 对象就使用 t.memberExpression(...) 方法,其中构造参数取决于节点的定义。

Babel 插件实践

上面列举了一些 Babel 插件基本的用法,最重要的仍是在于在代码工程中进行实践,想象一下哪些场景咱们能够经过编写 Babel 插件来解决实际问题,而后 Just Do It。

一个最简单的插件实例

为了抛砖引玉,咱们来举一个最简单的示例。在代码调试过程当中,咱们经常使用到 Debugger 这个语句,便于进行函数运行时调试,咱们但愿经过使用 Babel 插件,当在开发环境时打印当前 Debugger 节点的位置,便于提醒咱们,而在生产环境直接将节点删除。

为了实现这样的插件,首先经过 ASTExplorer 找到 Debugger 的 Node type 为 DebuggerStatement,咱们须要使用这个节点访问器,再经过 NODE_ENV 判断运行环境,若为 production 则调用 path.remove方法,不然打印堆栈信息。

首先,建立一个名为 babel-plugin-drop-debugger.js 的插件,并编写代码:

module.exports = function() {
  return {
    name: 'drop-debugger',
    visitor: {
      DebuggerStatement(path, state) {
        if (process.env.NODE_ENV === 'production') {
          path.remove();
          return;
        }
        const {
          start: { line, column }
        } = path.node.loc;
        console.log(
          `Debugger exists in file: ${ state.filename }, at line ${line}, column: ${column}`
        );
      }
    }
  };
};
复制代码

而后在 babel.config.js 中引用插件:

module.exports = {
  plugins: ['./babel-plugin-drop-debugger.js']
};

复制代码

再建立一个测试文件 test-plugin.js :

function square(n) {
  debugger;
  return () => 2 * n;
}
复制代码

当咱们执行: npx babel test-plugin.js 时打印:

Debugger exists in file: /Users/xxx/test-plugin.js, at line 2, column: 2
复制代码

若执行: NODE_ENV=production npx babel test-plugin.js 时打印:

function square(n) {
  return () => 2 * n;
}
复制代码

总结

目前在工程中还没遇到须要 Babel 解决问题的场景,所以就先再也不继续深刻了,但愿以后能进行补充。在这篇文章中咱们对 Babel 插件有了一个基本的印象,若要了解 Babel 插件的基本使用方式请访问用户手册

Babel 主要由三部分组成:编译(parse)、转换(transform)、生成(generate),插件机制不开如下几个核心库:

  • @babel/parser ,Babel AST 解析器,原名为 babylon,由 acorn 改造而来
  • @babel/traverse ,对 AST Node 进行遍历与更新
  • @babel/generator ,根据 AST 与相关选项从新构建代码
  • @babel/types ,判断 AST 节点类型与构造新的节点

如下为一些实用的开发辅助:

值得一提的是,Babel 官方 Github 库的 API 文档和 Doc 不太健全,有时候只能经过源码去学习。但愿下次须要实现一个完整的 Babel 插件时,再继续进行探索吧。

参考

相关文章
相关标签/搜索