likes
comments
collection
share

babel插件的实现思路

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

babel插件的实现思路

什么是babel

Babel 是一个工具链,由大量的工具包组成。主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

@babel/core

Babel 的核心功能包含在 @babel/core 模块中。看到 core 这个词了吧,意味着核心,没有它,在 babel 的世界里注定寸步难行。不安装 @babel/core,无法使用 babel 进行编译。

babel-core 的一个核心作用是把 js 代码分析成 ast(抽象语法树) ,方便各个插件分析语法进行相应的处理,用于对 AST 进行转换, 实现转换为ES5代码、压缩、功能增强等目的。比如有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。

下面这张图抽象说明babel的工作过程

babel插件的实现思路

  • 解析。解析步骤接收代码并输出 AST。
  • 转换。转换步骤接收AST并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作,来进行功能增强
  • 生成。代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。

AST抽象语法树

AST是一种源代码的抽象语法结构的树形表示。树中的每个节点都表示源代码中出现的一个构造。

AST 的世界”里所有的一切都是 节点(Node),不同类型的节点之间相互嵌套形成一颗完整树形结构。

AST作用

语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全、代码混淆压缩等

AST 如何生成

js 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。 整个解析过程主要分为以下两个步骤:

分词:将整个代码字符串分割成最小语法单元数组 语法分析:在分词基础上建立分析语法单元之间的关系

词法分析

词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元:

例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。

词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。

最终,整个代码将被分割进一个tokens列表(或者说一维数组)。

语法分析

语法分析阶段会把token转换成 AST 的形式,这个阶段会使用token中的信息把它们转换成一个 AST 的表述结构,使用type属性记录当前的类型

例如 let 代表着一个变量声明的关键字,所以它的 type 为 VariableDeclaration,而 a = 1 会作为 let 的声明描述,它的 type 为 VariableDeclarator

在线ast抽象语法树生成:astexplorer.net/

实例

let a = 1

babel插件的实现思路

function demo(n) {
  return n * n;
}
{
  "type": "Program", // 整段代码的主体
  "body": [
    {
      "type": "FunctionDeclaration", // function 的类型叫函数声明;
      "id": { // id 为函数声明的 id
        "type": "Identifier", // 标识符 类型
        "name": "demo" // 标识符 具有名字 
      },
      "expression": false,
      "generator": false,
      "async": false, // 代表是否 是 async function
      "params": [ // 同级 函数的参数 
        {
          "type": "Identifier",// 参数类型也是 Identifier
          "name": "n"
        }
      ],
      "body": { // 函数体内容 整个格式呈现一种树的格式
        "type": "BlockStatement", // 整个函数体内容 为一个块状代码块类型
        "body": [
          {
            "type": "ReturnStatement", // return 类型
            "argument": {
              "type": "BinaryExpression",// BinaryExpression 二进制表达式类型
              "start": 30,
              "end": 35,
              "left": { // 分左 右 中 结构
                "type": "Identifier", 
                "name": "n"
              },
              "operator": "*", // 属于操作符
              "right": {
                "type": "Identifier",
                "name": "n"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module" //表示的是在严格模式下解析并且允许模块定义(即能识别import和expor语法);script识别不了。
}

常用的 AST 节点类型对照表

类型原名称中文名称描述
Program程序主体整段代码的主体
VariableDeclaration变量声明声明一个变量,例如 var let const
FunctionDeclaration函数声明声明一个函数,例如 function
ExpressionStatement表达式语句通常是调用一个函数,例如 console.log()
BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
BreakStatement中断语句通常指 break
ContinueStatement持续语句通常指 continue
ReturnStatement返回语句通常指 return
SwitchStatementSwitch 语句通常指 Switch Case 语句中的 Switch
IfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}
Identifier标识符标识,例如声明变量时 var identi = 5 中的 identi
CallExpression调用表达式通常指调用一个函数,例如 console.log()
BinaryExpression二进制表达式通常指运算,例如 1+2
MemberExpression成员表达式通常指调用对象的成员,例如 console 对象的 log 成员
ArrayExpression数组表达式通常指一个数组,例如 [1, 3, 5]
FunctionExpression函数表达式例如const func = function () {}
ArrowFunctionExpression箭头函数表达式例如const func = ()=> {}
AwaitExpressionawait表达式例如let val = await f()
ObjectMethod对象中定义的方法例如 let obj = { fn () {} }
NewExpressionNew 表达式通常指使用 New 关键词
AssignmentExpression赋值表达式通常指将函数的返回值赋值给变量
UpdateExpression更新表达式通常指更新成员值,例如 i++
Literal字面量字面量
BooleanLiteral布尔型字面量布尔值,例如 true false
NumericLiteral数字型字面量数字,例如 100
StringLiteral字符型字面量字符串,例如 vansenb
SwitchCaseCase 语句通常指 Switch 语句中的 Case

对应工具插件

Babel 插件本质上就是编写各种 visitor 去访问 AST 上的节点,并进行 traverse。当遇到对应类型的节点,visitor 就会做出相应的处理,从而将原本的代码 transform 成最终的代码。

transform 阶段使用 @babel/traverse,可以遍历 AST,并调用 visitor 函数修改 AST,修改 AST 涉及到 AST 的判断、创建、修改等,这时候就需要 @babel/types 了,当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑。

  • @babel/parser : 将 js 代码 ------->>> AST 抽象语法树;
  • @babel/traverseAST 节点进行递归遍历;
  • @babel/types 包含手动构建 AST 和检查 AST 节点类型的方法
  • @babel/generator : AST 抽象语法树 ------->>> 新的 js 代码;
  • @babel/template:用以字符串形式的代码来构建AST树节点,快速优雅开发插件
Babylon: Babylon是 Babel 的解析器,作用是把js字符串解析成AST
@babel/parser(babylon) :babel官方的解析器,最初fork于acorn,后来完全走向了自己的道路,从babylon改名之后,其构建的插件体系非常强大
Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。就是触发访问者(Visitors)的步骤
babel-core中集成了这些插件
acorn 和 Esprima 很类似,输出的ast都是符合 EsTree 规范的,目前webpack的AST解析器用的就是acorn

blog.csdn.net/gcc_java/ar…

blog.csdn.net/qiwoo_weekl…

babel中文文档www.tkcnn.com/babel/docs/…

代码字符串生成 AST 模板

transform 阶段有 @babel/traverse,可以遍历 AST,并调用 visitor 函数修改 AST,修改 AST 自然涉及到 AST 的判断、创建、修改等,这时候就需要 @babel/types 了,当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑。

export default function (babel) {
  // 即@babel/types,用于生成AST节点
  const { types: t } = babel;

  return {
    name: "ast-transform", // not required
    visitor: {
      Identifier(path) {
        path.node.name = path.node.name.split("").reverse().join("");
      },
    },
  };
}

其中path(路径)提供了访问/操作AST 节点的方法。path 本身表示两个节点之间连接的对象。例如path.node可以访问当前节点,path.parent可以访问父节点等。path.remove()可以移除当前节点。具体 API 见下图。

babel插件的实现思路

Babel Types

类型判断

import * as types from "@babel/types";

// 是否为标识符类型节点
if (types.isIdentifier(node)) {
  // ...
}

// 是否为数字字面量节点
if (types.isNumberLiteral(node)) {
  // ...
}

// 是否为表达式语句节点
if (types.isExpressionStatement(node)) {
  // ...
}

创建节点

import * as types from "@babel/types";
import generator from "@babel/generator";

const log = (node: types.Node) => {
  console.log(generator(node).code);
};

log(types.stringLiteral("Hello World")); // output: Hello World

babel/template

template当作为带有字符串参数的函数调用时,您可以提供占位符,这些占位符将在使用模板时被替换。

您可以使用两种不同类型的占位符:句法占位符(例如%name%)或标识符占位符(例如NAME)。@babel/template默认情况下支持这两种方法,但不能混合使用。如果您需要明确说明所使用的语法,可以使用该syntacticPlaceholders (opens new window)选项。

请注意语法占位符是在 Babel 7.4.0 中引入的。如果您不控制@babel/template版本(例如,从@babel/core@^7.0.0对等依赖项导入时),则必须使用标识符占位符。

语法占位符

import template from "@babel/template";
import generate from "@babel/generator";
import * as t from "@babel/types";

const buildRequire = template(`
  var %importName% = require(%source%);
`);

const ast = buildRequire({
  importName: t.identifier("myModule"),
  source: t.stringLiteral("my-module"),
});

console.log(generate(ast).code);

标识符占位符

const buildRequire = template(`
  var IMPORT_NAME = require(SOURCE);
`);

const ast = buildRequire({
  IMPORT_NAME: t.identifier("myModule"),
  SOURCE: t.stringLiteral("my-module"),
});

如何给所有的async函数添加try/catch

babel文档:www.babeljs.cn/docs/plugin…

1)借助AST抽象语法树,遍历查找代码中的await关键字

2)找到await节点后,从父路径中查找声明的async函数,获取该函数的body(函数中包含的代码)

3)创建try/catch语句,将原来async的body放入其中

4)最后将async的body替换成创建的try/catch语句

async function fn() {
   await f()
}
async function fn() {
    try {
        await f()
    } catch (e) {
        console.log(e)
    }
}
module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       // 设置需要范围的节点类型
       CallExression: (path, state) => { 
         do soming ……
       }
     }
   }
 }
module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       // 设置AwaitExpression
       AwaitExpression(path) {
         // 获取当前的await节点
         let node = path.node;
       }
     }
   }
 }
// async节点的属性为true
const asyncPath = path.findParent(p => p.node.async)
// 1️⃣:函数声明
async function fn() {
  await f()
}

// 2️⃣:函数表达式
const fn = async function () {
  await f()
};

// 3️⃣:箭头函数
const fn = async () => {
  await f()
};

// 4️⃣:async函数定义在对象中
const obj = {
  async fn() {
•      await f()
  }
}
module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       // 设置AwaitExpression
       AwaitExpression(path) {
         // 获取当前的await节点
         let node = path.node;
         // 查找async函数的节点
         const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
       }
     }
   }
 }
// 引入babel-template
const template = require('babel-template');

// 定义try/catch语句模板
let tryTemplate = `
try {
} catch (e) {
console.log(CatchError:e)
}`;

// 创建模板
const temp = template(tryTemplate);

// 给模版增加key,添加console.log打印信息
let tempArgumentObj = {
   // 通过types.stringLiteral创建字符串字面量
   CatchError: types.stringLiteral('Error')
};

// 通过temp创建try语句的AST节点
let tryNode = temp(tempArgumentObj);

async函数体替换成try语句

module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       AwaitExpression(path) {
         let node = path.node;
         const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
         
         let tryNode = temp(tempArgumentObj);
         
         // 获取父节点的函数体body
         let info = asyncPath.node.body;

         // 将函数体放到try语句的body中
         tryNode.block.body.push(...info.body);

         // 将父节点的body替换成新创建的try语句
         info.body = [tryNode];
       }
     }
   }
 }

到这里,插件的基本结构已经成型,但还有点问题,如果函数已存在try/catch,该怎么处理判断呢?

若函数已存在try/catch,则不处理

// 示例代码,不再添加try/catch
async function fn() {
    try {
        await f()
    } catch (e) {
        console.log(e)
    }
}

通过isTryStatement判断是否已存在try语句

module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       AwaitExpression(path) {
       
        // 判断父路径中是否已存在try语句,若存在直接返回
        if (path.findParent((p) => p.isTryStatement())) {
          return false;
        }
       
         let node = path.node;
         const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
         let tryNode = temp(tempArgumentObj);
         let info = asyncPath.node.body;
         tryNode.block.body.push(...info.body);
         info.body = [tryNode];
       }
     }
   }
 }

添加报错信息

获取报错时的文件路径 filePath 和方法名称 funcName,方便快速定位问题

获取文件路径

// 获取编译目标文件的路径,如:E:\myapp\src\App.vue
const filePath = this.filename || this.file.opts.filename || 'unknown';

获取报错的方法名称

// 定义方法名
let asyncName = '';

// 获取async节点的type类型
let type = asyncPath.node.type;

switch (type) {
// 1️⃣函数表达式
// 情况1:普通函数,如const func = async function () {}
// 情况2:箭头函数,如const func = async () => {}
case 'FunctionExpression':
case 'ArrowFunctionExpression':
  // 使用path.getSibling(index)来获得同级的id路径
  let identifier = asyncPath.getSibling('id');
  // 获取func方法名
  asyncName = identifier && identifier.node ? identifier.node.name : '';
  break;

// 2️⃣函数声明,如async function fn2() {}
case 'FunctionDeclaration':
  asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
  break;

// 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
case 'ObjectMethod':
  asyncName = asyncPath.node.key.name || '';
  break;
}

// 若asyncName不存在,通过argument.callee获取当前执行函数的name
let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';

添加用户选项

用户引入插件时,可以设置`exclude``include``customLog`选项

`exclude`: 设置需要排除的文件,不对该文件进行处理

`include`: 设置需要处理的文件,只对该文件进行处理

`customLog`: 用户自定义的打印信息