Опыт разработки плагинов Babel

Node.js внешний интерфейс JavaScript Babel

предисловие

.在开发插件之前,有些内容还是要了解一下的,已经熟悉的大佬们可以直接跳过。

Babel использует модифицированный AST на основе ESTree, и спецификацию его ядра можно найти [здесь](https://github.com/babel/babel/blob/master/doc/ast/spec.md). Должно быть понятнее, если посмотреть непосредственно на пример:

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

Соответствующий объект AST (формат объекта предоставляется Babel)

{
  //代码块类别,函数声明
  type: "FunctionDeclaration",
  //变量标识
  id: {
    type: "Identifier",
    //变量名称
    name: "square"
  },
  //参数
  params: [{
    type: "Identifier",
    name: "n"
  }],
  //函数体
  body: {
     //块语句
    type: "BlockStatement",
    body: [{
       //return 语句
      type: "ReturnStatement",
      argument: {
        //二元表达式
        type: "BinaryExpression",
        //操作符
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

Вероятно вышеприведенная иерархическая связь, каждый слой называется узлом (Node), у js-объекта, соответствующего полному AST, может быть много узлов, в зависимости от конкретной ситуации. babel возвращает каждый узел как интерфейс. Включенные атрибуты, как показано в приведенном выше коде, такие как общие атрибуты, такие как type, start, end, loc, и частные атрибуты, соответствующие конкретным типам. Обработка наших более поздних плагинов также обрабатывается по разным типам.

AST Explorer для преобразования целевого кода в объект синтаксического дерева в сочетании с AST node typesдля просмотра конкретных свойств.

Шаги обработки Babel

Три основных этапа обработки Babel: анализ, преобразование и генерация.Я не хочу подробно описывать конкретный процесс, просто прочитайте официальное руководство.
Следует отметить, что плагин babel работает в процессе преобразования, то есть разобранный объект синтаксического дерева обрабатывается в соответствии с его собственным назначением, а затем выполняется этап генерации кода. Так что узнайте больше о конверсиях.

На этапе генерации кода окончательный (после ряда преобразований) AST преобразуется в код в строковой форме, а также создаются исходные карты для упрощения отладки.

Принцип генерации кода: сначала пройтись по всему AST в глубину, а затем построить строку, которая может представлять преобразованный код. При преобразовании это рекурсивный обход дерева.

конвертировать

Visitor

При конвертации плагин начинает работать, но как войти в этот процесс, Babel предоставляет нам спецификацию Visitor. Мы можем определить нашу логику доступа через посетителя. Наверное, вот так

const MyVisitor = {
  //这里对应上面node的type,所有type为Identifier的节点都会进入该方法中
  Identifier() {
    console.log("Called!");
  }
};
//以该方法为例 
function square(n) {
  return n * n;
} 
//会调用四次,因为
//函数名square
//形参 n
//函数体中的两个n,都是Identifier
path.traverse(MyVisitor); 
//  所以输出四个
Called!
Called!
Called!
Called!

Из-за алгоритма обхода в глубину после достижения конечного узла обнаруживается, что узлов-потомков нет, и необходимо вернуться к предыдущему уровню, чтобы продолжить обход следующего дочернего узла, поэтому каждый узел будет посещен. дважды.
Если не указано, вызов происходит при входе в узел, и, конечно же, метод посетителя может быть вызван и при выходе.

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
}; 

Есть также несколько советов:

Вы можете использовать | в имени метода для сопоставления нескольких разных типов, используя один и тот же обработчик.

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};

const MyVisitor = {
  Function(path) {}
};

Paths


{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}


Представляя идентификатор дочернего узла в виде пути, это выглядит так:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}

Когда вы передаете посетителя метода-члена Identifier(), вы фактически посещаете путь, а не узел. Таким образом, вы манипулируете реактивным представлением узла, а не самим узлом.

Написать плагин

Впереди приведены некоторые необходимые знания, в этой статье упоминаются только некоторые относительно важные знания. Для получения более подробной информации, пожалуйста, обратитесь к руководству по разработке. Лично, если вы разрабатываете плагин, у вас должны быть следующие три шага:

  1. Анализ абстрактного синтаксического дерева исходного файла AST
  2. Разбор файла объекта абстрактное синтаксическое дерево
  3. Строительство посетителя
    3.1 Определение условий доступа
    3.2 Определите логику преобразования

Основная часть плагина состоит из 3 шагов, но первые два шага очень важны. 3.1 и 3.2 зависят от результатов 1 и 2 соответственно. Только после четкого понимания структуры AST мы можем быть нацелены и достигать большего с меньшими усилиями. Например, следующий код:

var func = ()=>{
    console.log(this.b)
}; 

Цель состоит в том, чтобы преобразовать стрелочные функции в обычные объявления функций (здесь только преобразование этого формата, и другие части не будут задействованы в первую очередь). следующим образом:

var _this = this;
var func = function () {
    console.log(_this.b);
};

синтаксическое дерево исходного файла

Проанализируйте это простое объявление функции здесь и проанализируйте его в соответствии с приведенным выше определением, но все же рекомендуется здесьAST Explorer Мы можем ясно видеть наше синтаксическое дерево. Здесь перехватывается только полезная информация:

        "init": {
              "type": "ArrowFunctionExpression",
              /*...其他信息....*/
              "id": null,
              //形参
              "params": [],
              "body": {
                //函数体,this部分
                "arguments": [
                        {
                          "type": "MemberExpression",
                          "object": {
                             //this 表达式
                            "type": "ThisExpression",
                          },
                          "property": {
                             //b属性
                            "type": "Identifier",
                            "name": "b"
                          }
                        }
                      ]
             }
        }

Все, что мы хотим преобразовать, — это ArrowFunctionExpression, то есть функцию стрелки и часть ThisExpression этого выражения, а остальное пока остается без изменений.
Затем имена функций в нашем посетителе включают ArrowFunctionExpression и ThisExpression.

//visitor里面方法的key就对应我们要处理的node  type
const visitor = {
    //处理this表达式  
    ThisExpression(path){
        //将this转换成_this的形式
    },
    //处理箭头函数。
    ArrowFunctionExpression(path){
       //转换成普通的FunctionExpression
    }
}   

синтаксическое дерево объектного файла

Тот же метод, объект синтаксического дерева выглядит следующим образом:
Синтаксическое дерево слишком долго, мы посмотрим на изменения в месте лучше

    //转换之后的body由两个元素的数组,两个变量声明是统计关系
    "body": [
      //var _this = this;结构
      {
        "type": "VariableDeclaration",
        "kind": "var",
        "declarations": [
          {
            "type": "VariableDeclarator",
            //left为_this的标识
            "id": {
              "type": "Identifier",
              "name": "_this"
            },
            //right为this表达式
            "init": {
              "type": "ThisExpression"
              /***其他**/
            }
      },   
      // var func = function (b) {
      //      console.log(_this.b);
      //  };结构 只看关键的
      {
        "type": "VariableDeclaration",
        "kind": "var",
        "declarations": [
          {
            /*****省略*******/
            "arguments": [
                        {
                          "type": "MemberExpression",
                          //转换之后的_this.b
                          "object": {
                            "type": "Identifier",
                            "name": "_this"
                          },
                          "property": {
                            "type": "Identifier",
                            "name": "b"
                          }
                          ]
          }
      }
    ]

После сравнения определено, что наша операция должна заключаться в замене ArrowFunctionExpression на FunctionExpression.При встрече с этим выражением свяжите его и преобразуйте. При выполнении таких операций, как замена и добавление, используется API, предоставленный путем:

  • замена с заменой
  • Findparent() Находит родительский узел, отвечающий условиям
  • вставить перед вставкой родного брата
    Для получения дополнительной информации, пожалуйста, проверьте документацию, здесь перечислены только методы, которые мы используем.

построить узел

Эта операция вынесена сюда отдельно, описания API toFunctionExpression я так и не нашел. . . . Может я не там ищуFunctionExpression, я не могу найти его в исходном коде Babel:

//@src  /babel/packages/babel-types/src/definitions/core.js
defineType("FunctionExpression", {
  inherits: "FunctionDeclaration",
  //....
}
//又找到 FunctionDeclaration
defineType("FunctionDeclaration", {
  //这里才看到参数: id,params,body..
  builder: ["id", "params", "body", "generator", "async"],
  visitor: ["id", "params", "body", "returnType", "typeParameters"]
  //....  
}

В этом случае знают параметры, если есть четкая документация, пожалуйста, дайте мне знать. Вот прост.

Позже я искал его и, наконец, нашел соответствующий документ.портал

Идеальный посетитель

const Visitor = {
    //this表达式
    ThisExpression(path){
        //构建var _this = this
        let node = t.VariableDeclaration(
            'var',
            [
                t.VariableDeclarator(
                    t.Identifier('_this'),
                    t.Identifier('this')
                )
            ]
        ),
        //构建 _this标识符
        str = t.Identifier('_this'),
        //查找变量声明的父节点
        //这里只是针对例子的,真正转换需要考虑的情况很多
        parentPath = path.findParent((path) => path.isVariableDeclaration())
        //满足条件
        if(parentPath){
            //插入
            parentPath.insertBefore(node)
            path.replaceWith(
                str
            )
        }else{
            return
        }
    },
    //处理箭头函数。
    ArrowFunctionExpression(path){
        var node = path.node
        //构造一个t.FunctionExpression节点,将原有path替换掉即可
        path.replaceWith(t.FunctionExpression(
            node.id,
            node.params,
            node.body
          ))
    }
}     

Основной посетитель на этом заканчивается, конечно, если это плагин

//babel调用插件时会将babel-types作为参数传入 
export default function({ types: t }) {
  return {
    visitor:Visitor
  }

В местной отладке он может быть введен в баримо-ядро и варило, соответственно

var babel = require('babel-core');
var t = require('babel-types');
var code = `var func = ()=>{
    console.log(this.b)
  };`
const result = babel.transform(code, {
	plugins: [{
	  //前面的Visitor
		visitor: Visitor
	}]
});  
//输出转换之后的code
/**
 * var _this = this;
 * var func = function () {
 * console.log(_this.b);
 * }; 
 */
console.log(result.code);  

заключительные замечания

Справочная статья

Руководство по плагину Babel
Babel for ES6? And Beyond!

На бумаге это поверхностно, и я думал, что понял принцип и механизм плагинов babel, но я не ожидал, что написание небольшого демо будет таким трудоемким. В основном, я не знаком с соответствующими API и не знаю, как создавать узлы.После того, как я овладел навыками, это должно быть намного лучше. Эта статья представляет собой краткое изложение руководства по подключаемому модулю и обобщает идеи моей собственной реализации. Я надеюсь немного помочь нуждающимся студентам.Подробности смотрите в моем блоге