предисловие
Так как в заголовке сказано углубиться в Babel, давайте не будем говорить о различных вариантах использования Babel, что такое babel-core, babel-runtime, babel-loader... Если вы хотите понять эту часть контента, таких много. статьи, рекомендуется посмотреть недавно Одна из:Познакомьтесь с Babel на одном (долгом) дыхании, можно сказать, достаточно подробный и полный.
Ближе к делу, эта статья в основном предназначена для того, чтобы понять, как работает Babel, как работают плагины Babel и как писать плагины Babel Я думаю, вы что-то поймете после ее прочтения.
Итак, приступим!
Абстрактное синтаксическое дерево (AST)
Чтобы понять, как работает Babel, вам сначала нужно понять абстрактные синтаксические деревья, потому что плагины Babel работают с абстрактными синтаксическими деревьями. Сначала написанный нами код анализируется в абстрактное синтаксическое дерево (AST) на этапе компиляции, а затем после серии обходов и преобразований преобразованное абстрактное синтаксическое дерево генерируется в обычный код js. Картинка ниже(источник) может представлять рабочий процесс Babel:
Давайте сначала поговорим о AST.Цель разбора кода в AST состоит в том, чтобы помочь компьютеру лучше понять наш код. Здесь мы сначала напишем кусок кода:function add(x, y) {
return x + y;
}
add(1, 2);
Затем код анализируется в абстрактное синтаксическое дерево (онлайн-инструменты), выраженный в формате JSON следующим образом:
{
"type": "Program",
"start": 0,
"end": 52,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 40,
"id": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"expression": false,
"generator": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "x"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "y"
}
],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 40,
"body": [
{
"type": "ReturnStatement",
"start": 25,
"end": 38,
"argument": {
"type": "BinaryExpression",
"start": 32,
"end": 37,
"left": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "x"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 36,
"end": 37,
"name": "y"
}
}
}
]
}
},
{
"type": "ExpressionStatement",
"start": 42,
"end": 52,
"expression": {
"type": "CallExpression",
"start": 42,
"end": 51,
"callee": {
"type": "Identifier",
"start": 42,
"end": 45,
"name": "add"
},
"arguments": [
{
"type": "Literal",
"start": 46,
"end": 47,
"value": 1,
"raw": "1"
},
{
"type": "Literal",
"start": 49,
"end": 50,
"value": 2,
"raw": "2"
}
]
}
}
],
"sourceType": "module"
}
Здесь вы найдете похожие структуры на разных уровнях абстрактного синтаксического дерева, например:
{
"type": "Program",
"start": 0,
"end": 52,
"body": [...]
}
{
"type": "FunctionDeclaration",
"start": 0,
"end": 40,
"id": {...},
"body": {...}
}
{
"type": "BlockStatement",
"start": 19,
"end": 40,
"body": [...]
}
Такая структура называетсяУзел. AST состоит из нескольких или отдельных таких узлов, и внутри узла может быть несколько таких подузлов, образующих синтаксическое дерево, чтобы можно было описать синтаксис программы для статического анализа.
Поле типа в узле указывает тип узла, например «Program», «FunctionDeclaration», «ExpressionStatement» и т. д. в приведенном выше AST. Конечно, каждый тип узла будет иметь некоторые дополнительные атрибуты для дальнейшего описания узла. тип.
Рабочий процесс Бабеля
На картинке выше уже описан рабочий процесс Babel, и мы опишем его подробно ниже. Три основных этапа обработки Babel: анализ, преобразование и генерация.
-
Разобрать
Разобрать код в абстрактное синтаксическое дерево (AST), каждый движок js (например, движок V8 в браузере Chrome) имеет свой собственный синтаксический анализатор AST, и Babel передаетсяBabylonосуществленный. Процесс парсинга состоит из двух этапов:лексический анализа такжеРазбор, на этапе лексического анализа код в строковой форме преобразуется вжетон(токены), токен аналогичен узлу в AST; на этапе синтаксического анализа поток токенов преобразуется в форму AST, а на этом этапе информация в токене преобразуется в структуру представления AST.
-
конвертировать
На этом этапе Babel принимает AST и выполняет его обход в глубину через babel-traverse, добавляя, обновляя и удаляя узлы в процессе. Эта часть также является частью, в которой задействован плагин Babel.
-
генерировать
Преобразованный AST преобразуется в js-код с помощью babel-генератора Процесс заключается в том, чтобы сначала пройти весь AST в глубину, а затем создать строку, которая может представлять преобразованный код.
Эту часть можно посмотреть подробнееВавилонское руководство. Стоит отметить, что есть два типа плагинов babel, один из нихплагин синтаксиса, этот тип плагина является вспомогательным парсером (Babylon) на этапе парсинга, другой тип плагина —Плагин перевода, этот тип подключаемого модуля участвует в переводе кода на этапе преобразования, что также является наиболее распространенным и важным требованием для использования Babel. Основное внимание в этой статье уделяется также плагину транспиляции для babel.
Чтобы понять конкретный процесс обработки AST в Babel при обходе, нам также необходимо понять следующие важные моменты знаний.
Visitor
Когда Babel обрабатывает узел, он получает информацию об узле в виде посетителя и выполняет связанные операции.Этот метод выполняется через объект посетителя.Объект посетителя определяет функции доступа для различных узлов, так что различная обработка может выполняться для разных узлов. узлы. Написанный нами подключаемый модуль Babel фактически завершает наши операции по модификации кода, определяя экземпляр объекта посетителя для обработки ряда узлов AST. Возьмите каштан:
Мы хотим обработать оператор команды импорта, используемый в коде для загрузки модуля.
import { Ajax } from '../lib/utils';
Затем наш плагин Babel должен определить такой объект посетителя:
visitor: {
Program: {
enter(path, state) {
console.log('start processing this module...');
},
exit(path, state) {
console.log('end processing this module!');
}
},
ImportDeclaration (path, state) {
console.log('processing ImportDeclaration...');
// do something
}
}
Когда этот плагин используется в обходе, всякий раз, когда обрабатывается оператор импорта, то есть узел ImportDeclaration, будет автоматически вызываться метод ImportDeclaration(), который определяет конкретную операцию обработки оператора импорта. ImportDeclaration() вызывается при входе в узел ImportDeclaration, и мы также можем позволить плагину вызывать метод для обработки при выходе из узла.
visitor: {
ImportDeclaration: {
enter(path, state) {
console.log('start processing ImportDeclaration...');
// do something
},
exit(path, state) {
console.log('end processing ImportDeclaration!');
// do something
}
},
}
Метод enter() вызывается при входе в узел ImportDeclaration, а метод exit() вызывается при выходе из узла ImportDeclaration. То же самое верно для узла Program выше (узел Program можно в просторечии интерпретировать как узел модуля). Стоит отметить, что обход AST использует обход в глубину, поэтому процесс обхода AST вышеуказанного блока кода импорта выглядит следующим образом:
─ Program.enter()
─ ImportDeclaration.enter()
─ ImportDeclaration.exit()
─ Program.exit()
Таким образом, при создании посетителя у вас есть два шанса посетить узел.
ps: Определения различных типов узлов в AST можно найти в руководстве по Babylon:GitHub.com/Babel/детка сейчас…
Path
Из приведенного выше объекта посетителя вы можете видеть, что каждый раз, когда вы обращаетесь к методу узла, передается параметр пути, и этот параметр пути содержит информацию об узле и узле и его местоположении для работы на конкретном узле. В частности, Path — это объект, представляющий соединение между двумя узлами. Этот объект содержит не только информацию о текущем узле, но и информацию о родительском узле текущего узла, а также множество других методов, связанных с добавлением, обновлением, перемещением и удалением узлов. В частности, атрибуты и методы, содержащиеся в объекте Path, в основном следующие:
── 属性
- node 当前节点
- parent 父节点
- parentPath 父path
- scope 作用域
- context 上下文
- ...
── 方法
- get 当前节点
- findParent 向父节点搜寻节点
- getSibling 获取兄弟节点
- replaceWith 用AST节点替换该节点
- replaceWithMultiple 用多个AST节点替换该节点
- insertBefore 在节点前插入节点
- insertAfter 在节点后插入节点
- remove 删除节点
- ...
Конкретные можно посмотретьbabel-traverse.
Здесь мы продолжаем приведенный выше пример, чтобы увидеть, какую информацию содержит атрибут узла параметра пути:
visitor: {
ImportDeclaration (path, state) {
console.log(path.node);
// do something
}
}
Результат печати следующий:
Node {
type: 'ImportDeclaration',
start: 5,
end: 41,
loc:
SourceLocation {
start: Position { line: 2, column: 4 },
end: Position { line: 2, column: 40 } },
specifiers:
[ Node {
type: 'ImportSpecifier',
start: 14,
end: 18,
loc: [SourceLocation],
imported: [Node],
local: [Node] } ],
source:
Node {
type: 'StringLiteral',
start: 26,
end: 40,
loc: SourceLocation { start: [Position], end: [Position] },
extra: { rawValue: '../lib/utils', raw: '\'../lib/utils\'' },
value: '../lib/utils'
}
}
Можно обнаружить, что в дополнение к общим полям type, start, end и loc узел ImportDeclaration также имеет два специальных поля, спецификаторы и источник. Давайте поговорим о полях import и local в спецификаторе. Imported означает переменные, экспортированные из модуля экспорта, а local означает переменные текущего модуля после импорта. Это все еще немного запутанно. Давайте изменим оператор команды import:
import { Ajax as ajax } from '../lib/utils';
Затем продолжайте и напечатайте локальные и импортированные поля первого элемента спецификаторов:
Node {
type: 'Identifier',
start: 22,
end: 26,
loc:
SourceLocation {
start: Position { line: 2, column: 21 },
end: Position { line: 2, column: 25 },
identifierName: 'ajax' },
name: 'ajax' }
Node {
type: 'Identifier',
start: 14,
end: 18,
loc:
SourceLocation {
start: Position { line: 2, column: 13 },
end: Position { line: 2, column: 17 },
identifierName: 'Ajax' },
name: 'Ajax' }
Это довольно очевидно. Если ключевое слово as не используется, тогда import и local являются узлами, представляющими одну и ту же переменную.
State
Состояние — это второй параметр, который передается каждый раз при доступе к методу узла в объекте посетителя. Если вы посмотрите на объяснение в руководстве по Babel, вы все еще можете быть немного сбиты с толку.Короче говоря, состояние — это набор состояний, включая такую информацию, как информация о текущем плагине, информация о параметрах конфигурации, переданная плагином, и даже информация о пути текущего узла. Конечно, вы также можете сохранить пользовательское состояние во время обработки плагина babel в объекте состояния.
Сферы
Область действия здесь фактически такая же, как и область действия, упомянутая в js, а это значит, что Babel также должен учитывать область действия при обработке AST. Например, нужно различать переменные с одинаковыми именами внутри и вне функции. Здесь мы напрямую берем один из руководства Babel.Пример для объяснения. Рассмотрим следующий код:
function square(n) {
return n * n;
}
Давайте напишем посетителя, который переименовывает n в x.
visitor: {
FunctionDeclaration(path) {
const param = path.node.params[0];
paramName = param.name;
param.name = "x";
},
Identifier(path) {
if (path.node.name === paramName) {
path.node.name = "x";
}
}
}
Этот код посетителя может работать для приведенного выше примера кода, но его легко взломать:
function square(n) {
return n * n;
}
var n = 1;
Приведенный выше посетитель заменит переменную n вне квадрата функции на x, что, очевидно, не соответствует нашим ожиданиям. Лучший способ справиться с этим — использовать рекурсию и поместить одного посетителя внутрь другого.
visitor: {
FunctionDeclaration(path) {
const updateParamNameVisitor = {
Identifier(path) {
if (path.node.name === this.paramName) {
path.node.name = "x";
}
}
};
const param = path.node.params[0];
paramName = param.name;
param.name = "x";
path.traverse(updateParamNameVisitor, { paramName });
},
}
Теперь, когда у нас есть некоторое представление о рабочем процессе Babel, давайте поговорим о наборе инструментов Babel.
Набор инструментов Бабеля
Babel на самом деле представляет собой набор модулей, которые также упоминались во введении к рабочему процессу Babel выше.
Babylon
«Babylon — это синтаксический анализатор для Babel. Первоначально созданный из проекта Acorn. Acorn очень быстр, прост в использовании и имеет архитектуру на основе плагинов, разработанную для нестандартных функций (и тех будущих стандартных функций)». Вот прямая ссылка на инструкции в мануале, можно сказать, что Babylon определяет набор спецификаций для парсинга кода в AST. Чтобы привести пример:
import * as babylon from "babylon";
const code = `function square(n) {
return n * n;
}`;
babylon.parse(code);
// Node {
// type: "File",
// start: 0,
// end: 38,
// loc: SourceLocation {...},
// program: Node {...},
// comments: [],
// tokens: [...]
// }
babel-traverse
Babel-обход используется для поддержания состояния операции AST, определяя методы операции для обновления, добавления и удаления узлов. Как упоминалось ранее, свойства и методы в параметре пути определены в babel-traverse. Вот пример совместного использования babel-traverse и Babylon для обхода и обновления узлов:
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {
path.node.name = "x";
}
}
});
babel-types
babel-types — это мощная библиотека инструментов для обработки узлов AST: «Она содержит методы для создания, проверки и преобразования узлов AST. Библиотека инструментов содержит продуманные методы инструментов, полезные для написания логики для обработки AST». библиотеку инструментов, пожалуйста, обратитесь к официальному сайту Babel:Вавилон — это .IO/docs/en/daddy…
Здесь мы все еще используем команду импорта для демонстрации примера.Например, если мы хотим определить, какой тип импорта является импортом, вот три формы импорта:
import { Ajax } from '../lib/utils';
import utils from '../lib/utils';
import * as utils from '../lib/utils';
Узлы, используемые для представления трех переменных, импортированных выше в AST, отличаются друг от друга и называются ImportSpecifier, ImportDefaultSpecifier и ImportNamespaceSpecifier. Для получения подробной информации см.здесь. Если мы обработаем только оператор команды импорта, который импортирует указанную переменную, тогда наш плагин для Babel можно будет написать так:
function plugin () {
return ({ types }) => ({
visitor: {
ImportDeclaration (path, state) {
const specifiers = path.node.specifiers;
specifiers.forEach((specifier) => {
if (!types.isImportDefaultSpecifier(specifier) && !types.isImportNamespaceSpecifier(specifier)) {
// do something
}
})
}
}
}
На этом принципы Babel почти закончены, давайте попробуем написать плагин Babel с конкретными функциями.
Практика плагина Babel
Здесь мы пытаемся реализовать такую функцию: при использовании библиотеки компонентов пользовательского интерфейса мы часто используем только некоторые компоненты в библиотеке компонентов, например:
import { Select, Pagination } from 'xxx-ui';
Но это вводит всю библиотеку компонентов, поэтому код всей библиотеки компонентов также будет упакован при упаковке, что явно неразумно, поэтому мы надеемся, что сможем упаковать только те компоненты, которые нам нужны при упаковке.
Let's do it!
Во-первых, нам нужно сообщить Babel, как найти путь к соответствующему компоненту, то есть нам нужно настроить правило, чтобы указать Babel загрузить соответствующий компонент в соответствии с указанным именем.Здесь мы определяем метод:
"customSourceFunc": componentName =>(`./xxx-ui/src/components/ui-base/${componentName}/${componentName}`)}
Этот метод используется как параметр конфигурации этого плагина, который можно настроить в .babelrc (точнее, в .babelrc.js) или в babel-loader. Далее нам нужно определить объект посетителя.С предыдущим предзнаменованием, вот непосредственно код:
visitor: {
ImportDeclaration (path, { opts }) {
const specifiers = path.node.specifiers;
const source = path.node.source;
// 判断传入的配置参数是否是数组形式
if (Array.isArray(opts)) {
opts.forEach(opt => {
assert(opt.libraryName, 'libraryName should be provided');
});
if (!opts.find(opt => opt.libraryName === source.value)) return;
} else {
assert(opts.libraryName, 'libraryName should be provided');
if (opts.libraryName !== source.value) return;
}
const opt = Array.isArray(opts) ? opts.find(opt => opt.libraryName === source.value) : opts;
opt.camel2UnderlineComponentName = typeof opt.camel2UnderlineComponentName === 'undefined'
? false
: opt.camel2UnderlineComponentName;
opt.camel2DashComponentName = typeof opt.camel2DashComponentName === 'undefined'
? false
: opt.camel2DashComponentName;
if (!types.isImportDefaultSpecifier(specifiers[0]) && !types.isImportNamespaceSpecifier(specifiers[0])) {
// 遍历specifiers生成转换后的ImportDeclaration节点数组
const declarations = specifiers.map((specifier) => {
// 转换组件名称
const transformedSourceName = opt.camel2UnderlineComponentName
? camel2Underline(specifier.imported.name)
: opt.camel2DashComponentName
? camel2Dash(specifier.imported.name)
: specifier.imported.name;
// 利用自定义的customSourceFunc生成绝对路径,然后创建新的ImportDeclaration节点
return types.ImportDeclaration([types.ImportDefaultSpecifier(specifier.local)],
types.StringLiteral(opt.customSourceFunc(transformedSourceName)));
});
// 将当前节点替换成新建的ImportDeclaration节点组
path.replaceWithMultiple(declarations);
}
}
}
Среди них opts представляет параметры конфигурации, переданные ранее в .babelrc.js или babel-loader.camel2UnderlineComponentName и camel2DashComponentName в коде можно игнорировать, но вы можете буквально догадаться, что это за функция. Этот посетитель в основном обходит все узлы ImportDeclaration в модуле, находит узел, спецификатором которого является тип ImportSpecifier, использует входящий customSourceFunc для получения метода импорта его абсолютного пути, а затем заменяет исходный узел ImportDeclaration, так что компонент может быть загружается по требованию.
Давайте проверим эффект,
const babel = require('babel-core');
const types = require('babel-types');
const plugin = require('./../lib/index.js');
const visitor = plugin({types});
const code = `
import { Select as MySelect, Pagination } from 'xxx-ui';
import * as UI from 'xxx-ui';
`;
const result = babel.transform(code, {
plugins: [
[
visitor,
{
"libraryName": "xxx-ui",
"camel2DashComponentName": true,
"customSourceFunc": componentName =>(`./xxx-ui/src/components/ui-base/${componentName}/${componentName}`)}
}
]
]
});
console.log(result.code);
// import MySelect from './xxx-ui/src/components/ui-base/select/select';
// import Pagination from './xxx-ui/src/components/ui-base/pagination/pagination';
// import * as UI from 'xxx-ui';
Этот плагин Babel был опубликован в npm, адрес плагина:woohoo. эта лошадь plus.com/package/daddy…
Если вам интересно, вы также можете просмотреть исходный код плагина:GitHub.com/Ху Диньюй/Поместите…В исходниках есть тестовые примеры, можете клонировать и запускать самостоятельно, не забудьте сначала собрать.
На самом деле, этот плагин является нищенской версией плагина загрузки по запросу, плагина загрузки по запросу ant-design.babel-plugin-importРеализовано более полное решение и сделаны специальные оптимизации для React.Я проведу анализ исходного кода этого плагина, когда у меня будет время в будущем.
Вот об этом.
над.