Babel插件的相关知识点

201 阅读5分钟

0.前置知识

0.1 访问者模式

背景:访问者模式是行为型设计模式的一种,主要作用场景是:「 处理前端DOM树操作 」,因为前端的DOM中树结构非常的深,并且树中的各个节点的类型不同(但是节点类型范围就几种),所以当我们要对DOM树进行操作的时候常常要写很多代码。这个时候我们就可以通过访问者模式,将操作行为与被操作对象隔离开,进而将这部分代码抽象出来。
访问者(Visitor) ,当我们有一个对象集合,集合中的元素类型是不一样的,但类型是相对固定的,例如只有3种不同的类型,但是可能有30个元素。如果我们希望对集合中的所有元素进行某种操作,从接口的角度来看,由于类型不一致,我们很难通过一个统一的接口来遍历集合元素并对其进行操作。这时我们可以考虑使用访问者模式,它将 「获取某个元素」和对「元素的操作行为」进行了分离。
使用场景: 当我们需要使用Babel的插件的时候,常常会用到访问者模式。我们编写的Babel插件其实也是通过定义一个实例化visitor对象处理一系列的AST节点来完成我们对代码的修改操作

// 例如做项目,需求文档
// 项目经理访问需求文档的是想了解项目边界
// 开发人员访问需求文档的是想知道有哪些功能需要开发、难点与重点
// 测试人员访问需求文档是想评估测试用例多少
// 不同的立场返回项目的信息会不同,但是需求文档结构还是那么需求文档。

// 定义被访问者
class Element {
  accept(visitor) {
    visitor.visit(this);
  }
}
// 定义访问者
class Visitor {
  visit(element) {
    // 处理元素
  }
}
// 定义具体的元素
class ConcreteElement extends Element {
  // 具体的实现
}
// 定义具体的访问者
class ConcreteVisitor extends Visitor {
  visit(element) {
    // 处理具体的元素
  }
}
// 使用访问者模式
const element = new ConcreteElement();
const visitor = new ConcreteVisitor();
element.accept(visitor);
// 本质上就是通过被访问者通过accpet方法
// 1.将访问者实例作为参数传入element.accept(visitor)
// 2.然后在accept函数中调用visitor.visit(this)方法,被访问者实例给访问者visit方法参数,这样访问者就可以在方法中获取目标元素整体了。
// 实际上,但我们需要给一个对象的方法参数传入另一个对象实例的时候,就可以使用这种方法

下面是访问者模式实例:
老师家访学生,不同科目的老师就是访问者,通过学生的描述,老师对同一个学生做出一个判断

// 元素类
class Student {
    constructor(name, chinese, math, english) {
        this.name = name
        this.chinese = chinese
        this.math = math
        this.english = english
    }

    accept(visitor) {
        visitor.visit(this)
    }
}

// 访问者类
class ChineseTeacher {
    visit(student) {
        console.log(`语文${student.chinese}`)
    }
}

class MathTeacher {
    visit(student) {
        console.log(`数学${student.math}`)
    }
}

class EnglishTeacher {
    visit(student) {
        console.log(`英语${student.english}`)
    }
}

// 实例化元素类
const student = new Student('张三', 90, 80, 60)
// 实例化访问者类
const chineseTeacher = new ChineseTeacher()
const mathTeacher = new MathTeacher()
const englishTeacher = new EnglishTeacher()
// 接受访问
student.accept(chineseTeacher)
student.accept(mathTeacher)
student.accept(englishTeacher)

知识点1: 行为型设计模式:当为了完成某项功能,需要多个类协作才能完成的模式就是行为型模式,例如观察者模式需要监听者和触发者
知识点2:
juejin.cn/post/721502…

0.2 JS语法单元种类:

  • 关键字: const、let、var等
  • 标识符:可能是一个变量,也可能是if/else关键字,或者true/false常量
  • 运算符
  • 数字
  • 空格
  • 注释

1.一个完整的编译器的编译过程一定存在三个阶段:解析、转化处理、生成。其中我们一般需要做的就是处理转化这个阶段,将拆解后的AST的原代码,按照我们的逻辑进行增删改查。最后生成另一种形式的代码,说白了,就是将解析得到的AST树进行修改,变成新的AST树,然后生成新的代码
大概流程如下所示:

image.png 1.1 解析(一共两个步骤:词法分析、语法分析)

掌握:解析阶段的两个步骤,每个步骤的大概过程(词法分析怎么拆分token,token表现形式是什么样子,语法分析的产物是什么样子)

解析主要就是对代码进行拆分,一个关键字都通过词法分析、语法分析确定作用
(1)词法分析 词法分析主要通过分词器tokenizer,将源码拆分为tokens。例如:
(add 2 (subtract 4 2)) 进行词法分析后得到(如何判别是不是token:代码中两边可以用空格间隔的都可以看作一个token,例如看见一行语句就能知道有多少个token,说明你真正知道token的识别),token的表现形式是对象形式:{ type , value }

[
  { type: "paren", value: "(" }, // 左括号,左括号两边都可以用空格间隔
  { type: "name", value: "add" }, // add变量,注意这里不能把add拆成a d d 三个字符,这是整体,代码中两边可以用空格间隔的都可以看作一个token
  { type: "number", value: "2" },
  { type: "paren", value: "(" },
  { type: "name", value: "subtract" },
  { type: "number", value: "4" },
  { type: "number", value: "2" },
  { type: "paren", value: ")" },
  { type: "paren", value: ")" },
];

(2)语法分析 语法分析就是将词法分析得到的一系列tokens,整理组合成与语法相关联的表达式形式,语法分析得到的产物就是AST树。例如(add 2 (subtract 4 2))得到上述的词法分析tokens数组,现在进行语法解析后得到的AST:

{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params:
      [{
        type: 'NumberLiteral',
        value: '2',
      },
      {
        type: 'CallExpression',
        name: 'subtract',
        params: [{
          type: 'NumberLiteral',
          value: '4',
        }, {
          type: 'NumberLiteral',
          value: '2',
        }]
      }]
  }]
}

1.2 修改

2.Babel插件的编写

我们在编写babel插件的时候一般是通过函数返回一个对象,但是这个对象必须存在一个visitor属性,因为babel插件的接口规范如下:

image.png

export default function(api, options, dirname) { // babel插件接受三个参数
// 这个函数接受三个参数
// 1. api types (@babel/types)、traverse (@babel/traverse)、template(@babel/template) 等实用方法
// 2.options:插件参数
// 3.dirname:目录名

  return {
    visitor: {
      StringLiteral(path, state) {},
    }
  };
};
// 在返回值对象中,visitor的对象的属性都是方法,并且方法名必须是AST的DOM节点类型,这样在遍历对应的节点类型时,就会调用visitor中对应节点类型的访问函数(这些访问函数就是我们需要加的处理逻辑)。
// 注意:visitor的函数名必须和节点类型相同,这是babel自定义插件的基本遵循规则

下面举一个例子,熟悉一下babel自定义插件的语法:
如最后附录所示,visitor中的 CallExpression 表示当遍历到AST中的调用表达式CallExpression节点时,使用visitor中的CallExpression处理函数处理节点(CallExpression表示嗲用表达式,通常为一个函数的调用,例如console.log())

const generate = require('@babel/generator').default;
const t = require('@babel/types')
export default function ({types}) {
  return {
    visitor: {
      // 我们的访问者代码将放在这里  
      // 这里的CallExpression就相当于一种类型的visit函数
      CallExpression(path, state) { // path表示当前节点的连接对象, state
        const calleeName = generate(path.node.callee).code;
        // path.node:访问当前节点
        // 或者:if(t.isMemberExpression(path.node.callee) && path.node.callee.object.name === 'console' && ['log', 'info', 'error', 'debug'].includes(path.node.callee.property.name)) {
        if (targetCalleeName.includes(calleeName)) {
          const { line, column } = path.node.loc.start;
          path.node.arguments.unshift(
            types.stringLiteral(`loc:[${line},${column}]`) // 增加打印行,列
          );
        }
      }
    },
  };
}
// 转化前
const str = "babel";
console.log(str);
// babel转换后
const str = "babel"; 
console.log("loc:[2,0]", str);

解析:
step1: 我们的目标是console.log,那么对应的AST树的节点是callExpression调用表达式,如console.log()/Math.random()/fn()等全局函数方法或者自定义函数的调用。
step2: 因此我们下一步就要从调用表达式的几种类型中,找出console类型。path.node表示当前遍历到的 node 节点,可通过它访问节点上的属性;在callExpression节点中存在callee 属性表示调用的函数名arguments 表示调用的函数参数
(如果callee函数名为MemberExpression(成员表达式),则表示调用的是console/Math这类的全局方法,并且成员表达式会存在object属性,object中的name为'console'或者'math',如果callee函数名为Identifier(标识符),则表示调用的是自定的函数名
step3:而在console.log()中增加行,列信息,也就是console.log(str),变为console.log(行,列,str),这就等价于在console.log()函数中增加两个参数,对应到AST树上就是在 arguments数组中新增两个节点,且节点类型为字面量类型

// console.log(str)的AST树的JSON格式
{
  "type": "Program",
  "start": 0,
  "end": 18,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 17,
      "expression": {
        "type": "CallExpression", // 函数调用表达式
        "start": 0,
        "end": 16,
        "callee": { // callee表示调用的函数名
          "type": "MemberExpression", // 成员表达式
          "start": 0,
          "end": 11,
          "object": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [ // arguments 表示调用的函数参数
          {
            "type": "Identifier",
            "start": 12,
            "end": 15,
            "name": "str"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

注意visit函数的两个参数path对象和state对象

(1) path对象
除了能在 Path 对象上访问到当前 AST 节点、父级 AST 节点、父级 Path 对象,还能访问到添加、更新、移动和删除节点等其他方法,这些方法提高了我们对 AST 增删改的效率

(2) state对象
state对象主要是解决AST各个节点类型相互独立,无法处理依赖关系的问题,而设立的参数。例如,当我们处理某一节点类型的时候需要依赖其他节点类型的处理结果,但由于 visitor 属性之间互不关联,因此需要 state 帮助我们在不同的 visitor 之间传递状态。
将状态直接设置到 this 上,Babel 会给 visitor 上的每个方法绑定 this。在 Babel 插件中,this 通常会被用于传递状态:从 pre 到 visitor 再到 post, 例如:

 export default function({ types: t }) {
    return {
       pre(state) {
         this.cache = new Map();
       },
       visitor: {
         StringLiteral(path) {
           this.cache.set(path.node.value1);
         }
       },
       post(state) {
         console.log(this.cache);
       }
     };
   }

3.Babel的配置

3.1 两种配置 babel 的文件

babel 有两种并行的配置文件:.babelrc和babel.config.js

1.项目范围的配置
babel.config.js 文件,具有不同的拓展名(json、js、html) babel.config.js 是按照 commonjs 导出对象,可以写js的逻辑。
2.相对文件的配置
.babelrc 文件,具有不同的拓展名 参考文献:juejin.cn/post/701967…

3.2 Babel 配置项

juejin.cn/post/684490…

附录:

image.png

image.png