предисловие
.在开发插件之前,有些内容还是要了解一下的,已经熟悉的大佬们可以直接跳过。
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(), вы фактически посещаете путь, а не узел. Таким образом, вы манипулируете реактивным представлением узла, а не самим узлом.
Написать плагин
Впереди приведены некоторые необходимые знания, в этой статье упоминаются только некоторые относительно важные знания. Для получения более подробной информации, пожалуйста, обратитесь к руководству по разработке. Лично, если вы разрабатываете плагин, у вас должны быть следующие три шага:
- Анализ абстрактного синтаксического дерева исходного файла AST
- Разбор файла объекта абстрактное синтаксическое дерево
- Строительство посетителя
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 и не знаю, как создавать узлы.После того, как я овладел навыками, это должно быть намного лучше. Эта статья представляет собой краткое изложение руководства по подключаемому модулю и обобщает идеи моей собственной реализации. Я надеюсь немного помочь нуждающимся студентам.Подробности смотрите в моем блоге