предисловие
Когда я на выходных радостно писал буклет, я случайно опрокинул напиток и пролил его на клавиатуру, хотя я быстро вымыл его, компьютер внезапно выключился. Я попытался перезапустить и обнаружил, что он не может запуститься, и, наконец, подтвердил, что он сломан.
Неисправность компьютера не самая большая моя тревога, в основном потому, что я пообещал многим читателям, что буклет будет выпущен на следующей неделе, и я больше не могу этого делать, но теперь я должен это сделать, потому что весь код находится на том компьютере.
Думал, как лучше наверстать. Вспомнил, что многие с нетерпением ждали финального кейса "Рукописного простого Вавилона". Так получилось, что я тоже недавно писал этот кейс. Думал заранее поделиться мыслями . Это некоторая компенсация (а также объявил новости о голубе в течение нескольких дней).
вся идея
Процесс компиляции babel
Мы знаем, что основной процесс компиляции babel — это синтаксический анализ, преобразование и генерация.
- parse должен преобразовать исходный код в AST
- Преобразование заключается в добавлении, удалении или изменении AST.
- generate должен напечатать AST в объектный код и сгенерировать исходную карту
встроенный пакет babel7
Babel 7 помещает реализацию этих функций в разные пакеты:
-
@babel/parser
Разобрать исходный код в AST, соответствующий этапу разбора -
@babel/traverse
Пройдите AST и вызовите функцию посетителя, соответствующую этапу преобразования. -
@babel/generate
Распечатайте AST, сгенерируйте объектный код и исходную карту, соответствующие этапу генерации
Среди них AST необходимо создать в процессе обхода, который будет использоваться:
-
@babel/types
Создавайте и оценивайте AST -
@babel/template
Массовое создание AST из модулей
Выше приведена функция каждого этапа, запись общей функции Babel находится по адресу:
-
@babel/core
Разберите конфигурацию, примените плагин, предустановку и общий процесс компиляции.
Есть некоторые общие функции между плагинами и плагинами, они находятся в:
-
@babel/helpers
Используется для преобразования AST, созданного шаблоном, требуемым кодом es next, например _typeof, _defineProperties и т. д. -
@babel/helper-xxx
Общие функции для управления AST, используемые другими плагинами.
Конечно, помимо публичных функций при конвертации во время компиляции есть еще и функции во время выполнения.Эта часть размещена в:
-
@babel/runtime
В основном он включает три части: corejs, помощники и регенератор:- helper: версия вспомогательной функции во время выполнения (не внедряется через AST, а импортируется во время выполнения)
- corejs: реализация es next api, corejs 2 поддерживает только статические методы, corejs 3 также поддерживает методы экземпляра
- регенератор: реализация асинхронного ожидания, поддерживаемая facebook
(Babel — реализованный сам по себе хелпер для преобразования синтаксиса, но он не реализует полифиллы сам по себе, а полагается на сторонние corejs и регенератор)
Какие пакеты будем внедрять
Выше приведены некоторые из встроенных пакетов, которые завершает Babel.Если мы хотим написать простой Babel, мы должны реализовать эти пакеты, но мы можем сделать некоторые упрощения.
-
parser 包
Это должно быть реализовано, парсер Babel основан на acorn fork, и мы также основаны на acorn с небольшим расширением. Завершите преобразование исходного кода в AST. -
traverse 包
Это обход AST. Необходимо знать, какие ключи проходят разные типы AST. Они определены в пакете @babel/types. Мы также используем аналогичный метод реализации и будем вызывать соответствующего посетителя для реализации пути и path.scope Затем передаются некоторые API. -
generate 包
Это напечатать AST в объектный код и сгенерировать исходную карту. Чтобы распечатать каждый тип AST в этой части, необходимо написать соответствующую функцию для его обработки.Чтобы сгенерировать исходную карту, используйте пакет source-map для генерации каждого сопоставления, связывая loc, записанный во время синтаксического анализа, с позицией, вычисленной во время печати. -
types 包
Используемый для создания AST, он будет поддерживать API для создания и оценки различных AST, а также предоставлять свойства, которые должен пройти каждый AST для процесса обхода. -
template 包
AST создается партиями, здесь мы реализуем простую версию, передаем строку, анализируем ее в AST и возвращаем. -
core 包
Это объединение всего процесса, поддержка плагинов и пресетов, вызов плагинов, слияние с конечными посетителями, а затем обход. -
helper 包
Мы также реализуем один, поскольку плагин поддерживается, тогда есть некоторые общедоступные функции, которые можно использовать повторно. -
runtime 包
Мы также предоставляем его, но просто добавляем несколько вспомогательных функций для преобразования синтаксиса.
Это то, что мы, вероятно, сделаем. В процессе реализации мы можем углубить наше понимание Babel и транслятора, а не только освоить сам Babel.
Давайте подробно проанализируем конкретные идеи каждого шага ниже:
Код
(Поскольку код нельзя извлечь из сломанного компьютера, и его нет в буклете, он будет давать только идеи, а полный исходный код будет предоставлен, когда буклет появится в сети)
В целях упрощения мы не занимаемся субподрядом, а помещаем код в один пакет.
parser
Основные парсеры включают esprima, acorn и т. д. Acorn — самый популярный, а парсер babel — ответвление от acorn с множеством модификаций. Нам не нужен форк, мы можем сделать некоторые расширения на основе механизма плагинов Acorn.
Например, AST, проанализированный acorn, имеет только литеральный (литеральный) тип и не различает литералы, такие как строки, числа или логические значения, в то время как синтаксический анализатор babel уточняет их в AST, такие как StringLiteral, NumericLiteral и BooleanLiteral.
Давайте реализуем синтаксический анализатор, расширяющий AST таким образом.
Давайте сначала воспользуемся оригинальным парсером acorn:
const acorn = require("acorn");
const Parser = acorn.Parser;
const ast = Parser.parse(`
const a = 1;
`);
console.log(JSON.stringify(ast, null, 2));
Он печатается следующим образом:
Видно, что результатом синтаксического анализа числового литерала является Literal, поэтому, чтобы судить о типе, вам нужно посмотреть на тип значения, чтобы определить, что такое литерал, что более проблематично. Вот почему Babel совершенствует их.
Также уточним:
Способ расширения acorn — это наследование + переписывание, наследование предыдущего синтаксического анализатора, переопределение некоторых методов и возврат нового синтаксического анализатора.
const acorn = require("acorn");
const Parser = acorn.Parser;
var literalExtend = function(Parser) {
return class extends Parser {
parseLiteral (...args) {
const node = super.parseLiteral(...args);
switch(typeof node.value) {
case 'number':
node.type = 'NumericLiteral';
break;
case 'string':
node.type = 'StringLiteral';
break;
}
return node;
}
}
}
const newParser = Parser.extend(literalExtend);
const ast = newParser.parse(`
const a = 1;
`);
console.log(JSON.stringify(ast, null, 2));
Когда мы анализируем, мы оцениваем тип литерала, а затем устанавливаем тип.
Попробуйте эффект:
Таким образом, мы реализовали аналогичное расширение парсера babel для acorn.
Конечно, есть много расширений парсера babel, здесь мы просто реализуем его просто и проясним идеи.
traverse
Обход AST – это процесс поиска в глубину. При обработке определенного узла AST нам нужно знать, как продолжить обход дочерних узлов AST.
Пакет типов babel определяет, как проходятся различные AST (посетитель), как создавать (построитель), как судить (fidelds.validate) и псевдонимы (псевдоним).
Здесь нам также необходимо вести данные о том, как проходится каждый AST:
const AST_DEFINATIONS_MAP = new Map();
AST_DEFINATIONS_MAP.set('Program', {
visitor: ['body']
});
AST_DEFINATIONS_MAP.set('VariableDeclaration', {
visitor: ['declarations']
});
AST_DEFINATIONS_MAP.set('VariableDeclarator', {
visitor: ['id', 'init']
});
AST_DEFINATIONS_MAP.set('Identifier', {});
AST_DEFINATIONS_MAP.set('NumericLiteral', {});
Затем выполните обход AST в глубину на основе этих данных:
function traverse(node) {
const defination = astDefinationsMap.get(node.type);
console.log(node.type);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach(childNode => {
traverse(childNode);
})
} else {
traverse(prop);
}
})
}
}
Результат печати следующий:
По сравнению со структурой AST, только что реализованной, обход в глубину действительно достигается.
visitor
После обхода нам нужно реализовать функцию посетителей, а в процессе обхода добавлять, удалять и модифицировать AST. Это вызов соответствующей функции посетителя в соответствии с node.type во время процесса обхода:
function traverse(node, visitors) {
const defination = astDefinationsMap.get(node.type);
const visitorFunc = visitors[node.type];
if(visitorFunc && typeof visitorFunc === 'function') {
visitorFunc(node);
}
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach(childNode => {
traverse(childNode, visitors);
})
} else {
traverse(prop, visitors);
}
})
}
}
Давайте попробуем:
traverse(ast, {
Identifier(node) {
node.name = 'b';
}
});
Еще раз проверив AST, я обнаружил, что имя идентификатора изменилось с a на b.
Посетитель Babel также поддерживает указание ввода и выхода для выбора вызова до и после обхода дочерних узлов.Если функция передается, она рассматривается как ввод:
function traverse(node, visitors) {
const defination = astDefinationsMap.get(node.type);
let visitorFuncs = visitors[node.type] || {};
if(typeof visitorFuncs === 'function') {
visitorFuncs = {
enter: visitorFuncs
}
}
visitorFuncs.enter && visitorFuncs.enter(node);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach(childNode => {
traverse(childNode, visitors);
})
} else {
traverse(prop, visitors);
}
})
}
visitorFuncs.exit && visitorFuncs.exit(node);
}
Таким образом, посетитель, которого мы передаем, также может быть записан так:
traverse(ast, {
Identifier: {
exit(node) {
node.name = 'b';
}
}
});
Будет вызываться после обхода дочерних узлов.
path
Посетитель, который мы реализовали, — это узел, который передается напрямую, но в AST нет информации о родительском узле, поэтому мы также должны передать родительский узел.
Babel предоставляет функцию пути, которая представляет собой путь от текущего узла к корневому узлу, соединенный через родителя.
Мы инкапсулируем класс NodePath:
class NodePath {
constructor(node, parent, parentPath) {
this.node = node;
this.parent = parent;
this.parentPath = parentPath;
}
}
При вызове посетителя создайте объект пути и передайте:
function traverse(node, visitors, parent, parentPath) {
const defination = astDefinationsMap.get(node.type);
let visitorFuncs = visitors[node.type] || {};
if(typeof visitorFuncs === 'function') {
visitorFuncs = {
enter: visitorFuncs
}
}
const path = new NodePath(node, parent, parentPath);
visitorFuncs.enter && visitorFuncs.enter(path);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach(childNode => {
traverse(childNode, visitors, node, path);
})
} else {
traverse(prop, visitors, node, path);
}
})
}
visitorFuncs.exit && visitorFuncs.exit(path);
}
Таким образом мы можем получить родительский узел в визитере, родительский узел родительского узла, попробуем:
traverse(ast, {
Identifier: {
exit(path) {
path.node.name = 'b';
let curPath = path;
while (curPath) {
console.log(curPath.node.type);
curPath = curPath.parentPath;
}
}
}
});
Результат печати следующий:
Можно получить AST от текущего узла до корневого узла.
API для пути
Можно сохранить родителя, а также брата и сестру, то есть мы можем получить весь AST через путь. Но манипулирование AST напрямую немного громоздко, поэтому мы должны предоставить некоторый API для упрощения операции.
Во-первых, нам нужно сохранить ключ, соответствующий пройденному атрибуту AST, и соответствующий listKey, если это массив.
class NodePath {
constructor(node, parent, parentPath, key, listKey) {
this.node = node;
this.parent = parent;
this.parentPath = parentPath;
this.key = key;
this.listKey = listKey;
}
}
function traverse(node, visitors, parent, parentPath, key, listKey) {
const defination = astDefinationsMap.get(node.type);
let visitorFuncs = visitors[node.type] || {};
if(typeof visitorFuncs === 'function') {
visitorFuncs = {
enter: visitorFuncs
}
}
const path = new NodePath(node, parent, parentPath, key, listKey);
visitorFuncs.enter && visitorFuncs.enter(path);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach((childNode, index) => {
traverse(childNode, visitors, node, path, key, index);
})
} else {
traverse(prop, visitors, node, path, key);
}
})
}
visitorFuncs.exit && visitorFuncs.exit(path);
}
Затем реализуйте replaceWith и удалите API на основе ключа и listKey:
class NodePath {
constructor(node, parent, parentPath, key, listKey) {
this.node = node;
this.parent = parent;
this.parentPath = parentPath;
this.key = key;
this.listKey = listKey;
}
replaceWith(node) {
if (this.listKey) {
this.parent[this.key].splice(this.listKey, 1, node);
}
this.parent[this.key] = node
}
remove () {
if (this.listKey) {
this.parent[this.key].splice(this.listKey, 1);
}
this.parent[this.key] = null;
}
}
Проверьте эффект:
traverse(ast, {
NumericLiteral(path) {
path.replaceWith({ type: 'Identifier', name: 'bbbbbbb' });
}
});
Результат:
NumericLiteral был заменен идентификатором. Мы успешно реализовали path.replaceWith.
path.scope
path.scope — это информация о области, записывающая привязки объявленных переменных, их ссылки, где они были изменены (constantViolations), родительские области и т. д. является реализацией статической цепочки областей видимости.
Идеи реализации:
Прежде всего, функции, блоки и модули будут генерировать области видимости.При обработке этих AST должен быть создан объект Scope, который имеет атрибут привязки, и каждое объявление будет создавать привязку (например, оператор объявления переменной VariableDeclaration, объявление функции заявление FuncitonDeclaration и параметры, импорт Подождите)
При обращении к привязкам в этих областях через идентификатор будут записаны ссылки.В случае изменения будет записан путь, соответствующий AST измененного оператора, такого как оператор присваивания.
Также необходимо предоставить ряд API-интерфейсов для упрощения анализа области и операций, таких как поиск getBinding, удаление removeBinding, переименование rename и т. д.
Из-за пространственного соотношения здесь мы его реализовывать не будем, полная реализация будет в буклете "Кодирование плагинов Babel".
types
В обходе мы реализовали API path.replaceWith, который используется для замены AST на новый AST, мы напрямую передали литеральный объект, что более проблематично. Babel предоставляет возможность создавать AST через пакет types.Проанализируем идеи реализации:
На самом деле создание узла AST тоже рекурсивный процесс.Нам нужно следить за правильностью каждой части.Сохраняем ключ посетителя при обходе.При создании мы все равно создаем AST соответствующий этим ключам,но нам нужно введите параметры.Проведите тест.
defineType("BinaryExpression", {
builder: ["operator", "left", "right"],
fields: {
operator: {
validate: assertOneOf(...BINARY_OPERATORS),
},
left: {
validate: assertNodeType("Expression"),
},
right: {
validate: assertNodeType("Expression"),
},
},
visitor: ["left", "right"],
aliases: ["Binary", "Expression"],
});
Babel внутренне определяет логику создания типа AST с помощью метода defineType.Атрибут fileds содержит атрибуты, необходимые этому AST, и способ проверки каждого атрибута. После прохождения проверки будет создан AST по соответствующим параметрам.
template
шаблон babel предназначен для создания AST пакетами через строки, мы можем реализовать простой шаблон на основе синтаксического анализатора
function template(code) {
return parse(code);
}
template.expression = function(code) {
return template(code).body[0].expression;
}
Затем приведенный выше код может стать:
traverse(ast, {
NumericLiteral(path) {
path.replaceWith(template.expression('bbb'));
}
});
generate
Выше приведены все добавления, удаления и модификации AST. Далее давайте реализуем создание и печать AST в качестве целевого кода.
По сути, это процесс объединения строк:
class Printer {
constructor () {
this.buf = '';
}
space() {
this.buf += ' ';
}
nextLine() {
this.buf += '\n';
}
Program (node) {
node.body.forEach(item => {
this[item.type](item) + ';';
this.nextLine();
});
}
VariableDeclaration(node) {
this.buf += node.kind;
this.space();
node.declarations.forEach((declaration, index) => {
if (index != 0) {
this.buf += ',';
}
this[declaration.type](declaration);
});
this.buf += ';';
}
VariableDeclarator(node) {
this[node.id.type](node.id);
this.buf += '=';
this[node.init.type](node.init);
}
Identifier(node) {
this.buf += node.name;
}
NumericLiteral(node) {
this.buf += node.value;
}
}
class Generator extends Printer{
generate(node) {
this[node.type](node);
return this.buf;
}
}
function generate (node) {
return new Generator().generate(node);
}
Давайте попробуем:
const sourceCode = `
const a = 1,b=2,c=3;
const d=4,e=5;
`;
ast = parse(sourceCode);
traverse(ast, {
NumericLiteral(path) {
if (path.node.value === 2) {
path.replaceWith(template.expression('aaaaa'));
}
}
})
console.log(generate(ast));
Результат печати следующий:
const a=1,b=aaaaa,c=3;
const d=4,e=5;
Мы успешно реализовали метод генерации.
sourcemap
В дополнение к печати объектного кода генератор также генерирует исходную карту, что является очень важной функцией транслятора.
Идея реализации sourcemap тоже относительно проста:
Информация о расположении (номер строки и столбца) в исходном коде сохраняется в AST после синтаксического анализа, а новый номер строки и столбца вычисляется при печати в качестве целевого кода, так что со старыми и новыми номерами строк и столбцов , исходную карту можно создать с помощью API пакета исходной карты.
var map = new SourceMapGenerator({
file: "source-mapped.js"
});
map.addMapping({
generated: {
line: 10,
column: 35
},
source: "foo.js",
original: {
line: 33,
column: 2
},
name: "christopher"
});
console.log(map.toString());
// '{"version":3,"file":"source-mapped.js",
// "sources":["foo.js"],"names":["christopher"],"mappings":";;;;;;;;;mCAgCEA"}'
core
Мы реализовали функцию всего процесса выше, но мы редко используем API, а чаще используем пакет @babel/core всего процесса, поэтому нам нужно реализовать пакет ядра на основе вышеуказанного пакета, а затем поддерживать плагин и предустановленные.
function transformSync(code, options) {
const ast = parse(code);
const pluginApi = {
template
}
const visitors = {};
options.plugins.forEach(([plugin, options]) => {
const res = plugin(pluginApi, options);
Object.assign(visitors, res.visitor);
})
traverse(ast, visitors);
return generate(ast);
}
Плагин поддерживает входящие параметры, и вы можете получить API и параметры в плагине, а возвращаемое значение — это функция посетителя:
const sourceCode = `
const a = 1;
`;
const code = transformSync(sourceCode, {
plugins: [
[
function plugin1(api, options) {
return {
visitor: {
Identifier(path) {
// path.node.value = 2222;
path.replaceWith(api.template.expression(options.replaceName));
}
}
}
},
{
replaceName: 'ddddd'
}
]
]
});
console.log(code);
Результат:
const ddddd=1;
На данный момент мы завершили упрощенную версию всех встроенных функций babel. (помощник — это пакет, который ставит общедоступные функции, а среда выполнения — это API, внедряемый во время выполнения. Эти два пакета относительно просты и не будут реализованы. Подробно они будут реализованы в буклете «Коды для очистки плагинов Babel»)
Суммировать
Мы разобрали процесс компиляции babel и соответствующие функции встроенных пакетов, а затем уточнили, какие пакеты мы хотим реализовать: parser, traverse, generate, types, template, core. Далее по очереди осуществлялась реализация или прочесывание идей реализации.
Пакет синтаксического анализатора основан на Acorn, babel — это форк Acorn, и мы модифицируем AST непосредственно на основе плагина Acorn. Мы внедрили расширение для AST от Literal.
Пакет traverse отвечает за обход AST.Мы реализуем обход AST в глубину, записывая ключ посетителя и вызывая посетителя в процессе обхода, а также поддерживаем двухфазные вызовы входа и выхода. Параметры, передаваемые по пути поддержки посетителей, вы можете получить родительский, и вы можете вызывать такие API, как replaceWith и remove. Также мы разобрались с идеей реализации scope.
Для создания AST используются как типы, так и шаблоны.Мы разобрали идею реализации типов, то есть рекурсивно создать AST и потом его собрать, реализовать простой шаблон, и использовать метод разбора прямо из строки .
Пакет Generate отвечает за печать модифицированного AST в целевой код и генерирование SourceMap. Мы реализуем печать кода. Отсортировал идею SourceMap.
Основной пакет представляет собой интеграцию всего процесса компиляции и поддерживает плагины и пресеты.Мы реализуем API-интерфейс transformSync и поддерживаем вызовы плагинов.
Вышеупомянутая идея реализации Babel, Если вы доработаете ее, она может реализовать полностью функциональный Babel.
Это последний случай "Проверки плагинов Babel". Идеи реализации и коды в буклете будут более понятными, а также будет предоставлен исходный код.
Компьютер внезапно сломался в эти выходные, и код мог быть утерян, поэтому мне пришлось сделать это на некоторое время. Но многие люди с нетерпением ждут выхода этого буклета, и мне очень жаль, поэтому я поделился интересующими всех идеями реализации «Простого рукописного Вавилонского перевода», надеясь помочь вам лучше освоить Babel и ему подобные переводчики. (Буклет будет написан как можно скорее после ремонта моего компьютера)