Предисловие: вЗагрузка по требованию с помощью Babel-плагинаВ этой статье автор реализует загрузку компонентов по запросу, используя идею цепочки областей видимости. Эта идея представляет собой единую трактовку, входящую
ImportDeclaration
После этого соберите зависимости, сгенерируйте новые узлы и, наконец, используйтеЦепочка охвата (область)непосредственно заменить модифицированныйspecifiers[]
Все ссылочные имена, которые связаны. Также с помощью цепочки областей можно узнать, упоминается ли узел в контексте, и если ссылки нет, удалить недопустимый узел. Даже окончательная замена исходного узла завершает загрузку по требованию. Эта статья побудит вас проанализироватьbabel-plugin-import
Разблокируйте проверенный в отрасли подключаемый модуль babel с полным потоком загрузки по запросу.
первая подачаbabel-plugin-import
Адрес плагина:ant-design/babel-plugin-import
Поскольку автор статьи был введен в Babel Babel-плагин, так что это не пойдет непосредственно в тему. Для деталей щелкните одноклассникиэта ссылка. 众所周知,庖丁解牛分为三个阶段:
- Второй этап, три года спустя, он увидел внутреннюю текстуру костей коровы, больше не видел всю голову крупного рогатого скота.
- Третий этап, когда бойня сейчас, просто войти в контакт с духом тела крупного рогатого скота на нем, без глаз, чтобы видеть.
Теперь действуйте шаг за шагом по этим трем этапам.babel-plugin-import
Исходный код плагина
Шаг 1: Когда первые министры разгадывали быка, они не видели ничего, кроме быков.
первыйbabel-plugin-import
Именно для решения проблемы внешние компоненты или библиотеки функций, на которые есть ссылки в проекте, полностью упаковываются в процессе упаковки, что приводит к чрезмерному объему пакета после компиляции, как показано на следующем рисунке:
babel-plugin-import
Исходный код плагина состоит из двух файлов
- Файл индекса — это файл для инициализации записи плагина, а также файл, который автор подчеркнул в шаге 1.
- Файл плагина содержит набор методов для обработки различных узлов AST, которые экспортируются в виде класса
Сначала перейдите к входному файлу Index плагина:
import Plugin from './Plugin';
export default function({ types }) {
let plugins = null;
/**
* Program 入口初始化插件 options 的数据结构
*/
const Program = {
enter(path, { opts = {} }) {
assert(opts.libraryName, 'libraryName should be provided');
plugins = [
new Plugin(
opts.libraryName,
opts.libraryDirectory,
opts.style,
opts.styleLibraryDirectory,
opts.customStyleName,
opts.camel2DashComponentName,
opts.camel2UnderlineComponentName,
opts.fileName,
opts.customName,
opts.transformToDefaultImport,
types,
),
];
applyInstance('ProgramEnter', arguments, this);
},
exit() {
applyInstance('ProgramExit', arguments, this);
},
};
const ret = {
visitor: { Program }, // 对整棵AST树的入口进行初始化操作
};
return ret;
}
Во-первых, файл индекса импортирует плагин и имеет функцию экспорта по умолчанию Параметром функции является деконструированное имя.typesпараметр, который деконструируется из объекта babel, полное имя типов@babel/types
, набор методов обработки узлов AST. После импорта таким образом нам не нужно импортировать вручную@babel/types
.
После входа в функцию вы можете увидеть观察者( visitor )
Узел AST инициализируется вProgram
Здесь дляProgram
Обработка узла использует полную структуру плагина, есть события входа (enter) и выхода (exit), и нужно обратить внимание на:
Некоторые студенты могут спросить здесьProgram
Что такое узел? См. дерево AST, соответствующее const a = 1 ниже (сокращенная часть параметров)
{
"type": "File",
"loc": {
"start":... ,
"end": ...
},
"program": {
"type": "Program", // Program 所在位置
"sourceType": "module",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "NumericLiteral",
"value": 1
}
}
],
"kind": "const"
}
],
"directives": []
},
"comments": [],
"tokens": [
...
]
}
Программа эквивалентнакорневой узел, полное исходное дерево. Как правило, такие операции, как инициализация данных, выполняются при входе в узел. Также можно понять, что узел выполняется раньше других узлов, и это также узел, который выполняет выход позже всех. выход. теперь, когдаbabel-plugin-import
изProgram
Полная структура написана в узле, и должны быть очень важные вещи, с которыми нужно иметь дело при выходе.Мы обсудим, что делает выход позже.
Сначала рассмотрим ввод Здесь мы сначала используем состояние параметра ввода для построения параметров подключаемого модуля, указанных пользователем, и проверяем обязательные поля.libraryName
Существует ли [имя библиотеки]. Плагин, представленный файлом индекса, представляет собой структуру класса, поэтому плагин должен быть создан, и все параметры плагина должны быть согласованы с@babel/types
Все передается, и класс Plugin будет объяснен ниже.
Тогда позвониapplyInstance
функция:
export default function({ types }) {
let plugins = null;
/**
* 从类中继承方法并利用 apply 改变 this 指向,并传递 path , state 参数
*/
function applyInstance(method, args, context) {
for (const plugin of plugins) {
if (plugin[method]) {
plugin[method].apply(plugin, [...args, context]);
}
}
}
const Program = {
enter(path, { opts = {} }) {
...
applyInstance('ProgramEnter', arguments, this);
},
...
}
}
Основная цель этой функции — наследовать метод класса Plugin и требует три параметра.
- метод (строка): имя метода, которое необходимо наследовать от класса плагина.
- аргументы: (массив): [путь, состояние]
- PluginPass ( Object ): содержимое совпадает с состоянием, чтобы гарантировать, что передаваемое содержимое является последним состоянием.
Основная цель - сделатьProgram
Enter наследуется от класса PluginProgramEnter
метод и передать параметры пути и состояния вProgramEnter
.Program
По той же причине выхода наследованиеProgramExit
метод.
Теперь в классе Plugin:
export default class Plugin {
constructor(
libraryName,
libraryDirectory,
style,
styleLibraryDirectory,
customStyleName,
camel2DashComponentName,
camel2UnderlineComponentName,
fileName,
customName,
transformToDefaultImport,
types, // babel-types
index = 0, // 标记符
) {
this.libraryName = libraryName; // 库名
this.libraryDirectory = typeof libraryDirectory === 'undefined' ? 'lib' : libraryDirectory; // 包路径
this.style = style || false; // 是否加载 style
this.styleLibraryDirectory = styleLibraryDirectory; // style 包路径
this.camel2DashComponentName = camel2DashComponentName || true; // 组件名是否转换以“-”链接的形式
this.transformToDefaultImport = transformToDefaultImport || true; // 处理默认导入
this.customName = normalizeCustomName(customName); // 处理转换结果的函数或路径
this.customStyleName = normalizeCustomName(customStyleName); // 处理转换结果的函数或路径
this.camel2UnderlineComponentName = camel2UnderlineComponentName; // 处理成类似 time_picker 的形式
this.fileName = fileName || ''; // 链接到具体的文件,例如 antd/lib/button/[abc.js]
this.types = types; // babel-types
this.pluginStateKey = `importPluginState${index}`;
}
...
}
При создании экземпляра плагина в файле записи переданы параметры плагинаconstructor
После инициализации, кромеlibraryName
Все остальные значения имеют соответствующие значения по умолчанию.Стоит отметить, что customeName и customStyleName в списке параметров могут получить функцию или импортированный путь, поэтому вам нужно передатьnormalizeCustomName
функции унифицированы.
function normalizeCustomName(originCustomName) {
if (typeof originCustomName === 'string') {
const customeNameExports = require(originCustomName);
return typeof customeNameExports === 'function'
? customeNameExports
: customeNameExports.default;// 如果customeNameExports不是函数就导入{default:func()}
}
return originCustomName;
}
Эта функция используется для обработки, когда параметр является путем, преобразования его и извлечения соответствующей функции. Если после обработкиcustomeNameExports
Все еще не функция для импортаcustomeNameExports.default
, который включает в себя экспорт по умолчанию, — это немного знаний о синтаксическом сахаре.
export default something() {}
// 等效于
function something() {}
export ( something as default )
Коды возврата, шаг 1 в файле вводаProgram
ProgramEnter
метод
export default class Plugin {
constructor(...) {...}
getPluginState(state) {
if (!state[this.pluginStateKey]) {
// eslint-disable-next-line no-param-reassign
state[this.pluginStateKey] = {}; // 初始化标示
}
return state[this.pluginStateKey]; // 返回标示
}
ProgramEnter(_, state) {
const pluginState = this.getPluginState(state);
pluginState.specified = Object.create(null); // 导入对象集合
pluginState.libraryObjs = Object.create(null); // 库对象集合 (非 module 导入的内容)
pluginState.selectedMethods = Object.create(null); // 存放经过 importMethod 之后的节点
pluginState.pathsToRemove = []; // 存储需要删除的节点
/**
* 初始化之后的 state
* state:{
* importPluginState「Number」: {
* specified:{},
* libraryObjs:{},
* select:{},
* pathToRemovw:[]
* },
* opts:{
* ...
* },
* ...
* }
*/
}
...
}
ProgramEnter
прошедшийgetPluginState
**Инициализировать состояние в структуреimportPluginState
объект,getPluginState
Функция появляется очень часто в последующих операциях, и читатели должны обратить внимание на эту функцию, и она будет описана позже.
Но зачем вам инициализировать такую структуру? Это включает в себя идею плагина. Как упоминалось во вступительной блок-схеме,babel-plugin-import
Конкретная реализация загрузки по требованию выглядит следующим образом: после прохождения узла импорта собрать данные узла, а затем выполнить метод преобразования загрузки по требованию со всех узлов, которые могут быть указаны в привязке импорта. Состояние является ссылочным типом, и операции над ним будут влиять на начальное значение состояния последующих узлов, поэтому с узлом Program объект, собирающий зависимости, инициализируется при входе для облегчения последующих операций. Метод, отвечающий за инициализацию структуры узла состояния и выборку данных, точноgetPluginState
.
Эта идея очень важна, и она проходит через все последующие коды и цели. Пожалуйста, убедитесь, что вы поняли и читаете дальше.
Шаг 2: Спустя три года я не видел целую корову
На шаге 1 мы узнали, что подключаемый модуль используетProgram
унаследовано как отправная точкаProgramEnter
И инициализируется зависимость плагина.Если у читателя есть какие-то части, которые не были отсортированы, пожалуйста, вернитесь к шагу 1, чтобы тщательно переварить содержимое, прежде чем продолжить чтение.
Во-первых, вернемся к периферийному индексному файлу, который до этого был зарегистрирован только в режиме наблюдателя.Program
Других записей узлов AST не существует, поэтому, по крайней мере, тип узла AST из оператора импорта должен быть введен вImportDeclaration
export default function({ types }) {
let plugins = null;
function applyInstance(method, args, context) {
...
}
const Program = {
...
}
const methods = [ // 注册 AST type 的数组
'ImportDeclaration'
]
const ret = {
visitor: { Program },
};
// 遍历数组,利用 applyInstance 继承相应方法
for (const method of methods) {
ret.visitor[method] = function() {
applyInstance(method, arguments, ret.visitor);
};
}
}
создать массив иImportDeclaration
Вставка, вызов после обходаapplyInstance
_ _ то же самое, что и введение Шага 1. После выполнения посетитель станет следующей структурой
visitor: {
Program: { enter: [Function: enter], exit: [Function: exit] },
ImportDeclaration: [Function],
}
Теперь вернитесь к плагину и введитеImportDeclaration
export default class Plugin {
constructor(...) {...}
ProgramEnter(_, state) { ... }
/**
* 主目标,收集依赖
*/
ImportDeclaration(path, state) {
const { node } = path;
// path 有可能被前一个实例删除
if (!node) return;
const {
source: { value }, // 获取 AST 中引入的库名
} = node;
const { libraryName, types } = this;
const pluginState = this.getPluginState(state); // 获取在 Program 处初始化的结构
if (value === libraryName) { // AST 库名与插件参数名是否一致,一致就进行依赖收集
node.specifiers.forEach(spec => {
if (types.isImportSpecifier(spec)) { // 不满足条件说明 import 是名称空间引入或默认引入
pluginState.specified[spec.local.name] = spec.imported.name;
// 保存为:{ 别名 : 组件名 } 结构
} else {
pluginState.libraryObjs[spec.local.name] = true;// 名称空间引入或默认引入的值设置为 true
}
});
pluginState.pathsToRemove.push(path); // 取值完毕的节点添加进预删除数组
}
}
...
}
ImportDeclaration
Будут собраны зависимые поля в импорте.Если он импортируется по пространству имен или импортируется по умолчанию, для него будет установлено значение {псевдоним: true}, а для деструктурированного импорта будет установлено значение {псевдоним: имя компонента}.getPluginState
Метод был объяснен в шаге 1. Структура узла AST для импортаЗагрузка по требованию с помощью Babel-плагинаЕсть подробная инструкция, повторяться не буду. После реализации следующей структуры pluginState
// 例: import { Input, Button as Btn } from 'antd'
{
...
importPluginState0: {
specified: {
Btn : 'Button',
Input : 'Input'
},
pathToRemove: {
[NodePath]
}
...
}
...
}
этот разstate.importPluginState
В структуре уже собрана вся информация о зависимостях, которая поможет узлам в последующих преобразованиях.
На данный момент все готово, остался только восточный ветер. Что такое Дунфэн? это действие, которое запускает импорт преобразования. существуетЗагрузка по требованию с помощью Babel-плагинаПреобразование узлов и удаление старых узлов также выполняется при сборе зависимостей в . все работаетImportDeclaration
происходит в узле. а такжеbabel-plugin-import
Идея состоит в том, чтобы найти все узлы AST, которые могут ссылаться на Import, и обработать их все. **Некоторые читатели могут подумать о преобразовании и ссылке на импортсвязыватьУзел JSX, но нет смысла преобразовывать узел JSX, потому что существует достаточно типов узла AST ( type ), на которые можно ссылаться привязке импорта, поэтому область действия типа узла AST, который необходимо преобразовать, следует сузить. как можно больше. Более того, другие плагины babel будут конвертировать наши узлы JSX в другие типы AST, поэтому мы можем игнорировать дерево AST типа JSX и ждать, пока другие плагины babel преобразуются, прежде чем заменять их. На самом деле есть много записей, которые можно запустить на следующем шаге, но начнем с наиболее знакомого React.createElement.
class Hello extends React.Component {
render() {
return <div>Hello</div>
}
}
// 转换后
class Hello extends React.Component {
render(){
return React.createElement("div",null,"Hello")
}
}
Тип AST после преобразования JSXCallExpression
(выражение выполнения функции), структура выглядит следующим образом, знакомство со структурой может помочь учащимся глубже понять следующие шаги.
{
"type": "File",
"program": {
"type": "Program",
"body": [
{
"type": "ClassDeclaration",
"body": {
"type": "ClassBody",
"body": [
{
"type": "ClassMethod",
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "CallExpression", // 这里是处理的起点
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"identifierName": "React"
},
"name": "React"
},
"property": {
"type": "Identifier",
"loc": {
"identifierName": "createElement"
},
"name": "createElement"
}
},
"arguments": [
{
"type": "StringLiteral",
"extra": {
"rawValue": "div",
"raw": "\"div\""
},
"value": "div"
},
{
"type": "NullLiteral"
},
{
"type": "StringLiteral",
"extra": {
"rawValue": "Hello",
"raw": "\"Hello\""
},
"value": "Hello"
}
]
}
],
"directives": []
}
}
]
}
}
]
}
}
Итак, переходим к узлу CallExpression и продолжаем процесс трансформации.
export default class Plugin {
constructor(...) {...}
ProgramEnter(_, state) { ... }
ImportDeclaration(path, state) { ... }
CallExpression(path, state) {
const { node } = path;
const file = path?.hub?.file || state?.file;
const { name } = node.callee;
const { types } = this;
const pluginState = this.getPluginState(state);
// 处理一般的调用表达式
if (types.isIdentifier(node.callee)) {
if (pluginState.specified[name]) {
node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
}
}
// 处理React.createElement
node.arguments = node.arguments.map(arg => {
const { name: argName } = arg;
// 判断作用域的绑定是否为import
if (
pluginState.specified[argName] &&
path.scope.hasBinding(argName) &&
types.isImportSpecifier(path.scope.getBinding(argName).path)
) {
return this.importMethod(pluginState.specified[argName], file, pluginState); // 替换了引用,help/import插件返回节点类型与名称
}
return arg;
});
}
...
}
Вы можете увидеть вызов исходного кодаimportMethod
Дважды функция этой функции состоит в том, чтобы инициировать импорт, чтобы преобразовать действие в режим загрузки по требованию и вернуть совершенно новый узел AST. Потому что после преобразования импорта имя компонента, который мы вручную ввели ранее, будет отличаться от преобразованного имени, поэтомуimportMethod
Нам нужно вернуть преобразованное новое имя (структура AST) в соответствующее положение нашего соответствующего узла AST, заменяя имя старого компонента. Исходный код функции будет подробно проанализирован позже.
Возвращаясь к оригинальному вопросу, почемуCallExpression
нужно позвонитьimportMethod
функция? Поскольку значения этих двух выражений различны,CallExpression
Есть два случая узлов:
- Это было проанализировано только что, первый случай — React.createElement после преобразования кода JSX.
- AST нашего кода операции, такого как вызовы функций, также
CallExpression
Тип, например:
import lodash from 'lodash'
lodash(some values)
Таким образом, вCallExpression
сначала определит, является ли значение node.calleeIdentifier
, если правильно описан второй случай, конвертировать напрямую. Если нет, то это в форме React.createElement, пройдитесь по трем параметрам React.createElement, чтобы извлечь имя, а затем решите, является ли имя именем импорта, собранным предыдущим состоянием.pluginState, и, наконец, проверьте область имени и отследить имя имени.связыватьЭто заявление об импорте. Все эти условия оценки предназначены для того, чтобы избежать неправильного изменения исходной семантики функции и предотвратить неправильное изменениеОбласть блокировки для таких функций, как замыканияЕсть переменная с таким же именем. Если вышеуказанные условия соблюдены, это должен быть импорт, который необходимо обработать.Цитировать. пусть это продолжаетсяimportMethod
функция преобразования,importMethod
Необходимо передать три параметра: имя компонента, файл (path.sub.file), pluginState
import { join } from 'path';
import { addSideEffect, addDefault, addNamed } from '@babel/helper-module-imports';
export default class Plugin {
constructor(...) {...}
ProgramEnter(_, state) { ... }
ImportDeclaration(path, state) { ... }
CallExpression(path, state) { ... }
// 组件原始名称 , sub.file , 导入依赖项
importMethod(methodName, file, pluginState) {
if (!pluginState.selectedMethods[methodName]) {
const { style, libraryDirectory } = this;
const transformedMethodName = this.camel2UnderlineComponentName // 根据参数转换组件名称
? transCamel(methodName, '_')
: this.camel2DashComponentName
? transCamel(methodName, '-')
: methodName;
/**
* 转换路径,优先按照用户定义的customName进行转换,如果没有提供就按照常规拼接路径
*/
const path = winPath(
this.customName
? this.customName(transformedMethodName, file)
: join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName), // eslint-disable-line
);
/**
* 根据是否是默认引入对最终路径做处理,并没有对namespace做处理
*/
pluginState.selectedMethods[methodName] = this.transformToDefaultImport // eslint-disable-line
? addDefault(file.path, path, { nameHint: methodName })
: addNamed(file.path, methodName, path);
if (this.customStyleName) { // 根据用户指定的路径引入样式文件
const stylePath = winPath(this.customStyleName(transformedMethodName));
addSideEffect(file.path, `${stylePath}`);
} else if (this.styleLibraryDirectory) { // 根据用户指定的样式目录引入样式文件
const stylePath = winPath(
join(this.libraryName, this.styleLibraryDirectory, transformedMethodName, this.fileName),
);
addSideEffect(file.path, `${stylePath}`);
} else if (style === true) { // 引入 scss/less
addSideEffect(file.path, `${path}/style`);
} else if (style === 'css') { // 引入 css
addSideEffect(file.path, `${path}/style/css`);
} else if (typeof style === 'function') { // 若是函数,根据返回值生成引入
const stylePath = style(path, file);
if (stylePath) {
addSideEffect(file.path, stylePath);
}
}
}
return { ...pluginState.selectedMethods[methodName] };
}
...
}
После входа в функцию не торопитесь смотреть код, обратите внимание, что здесь представлены два пакета: path.join и@babel/helper-module-imports, соединение введено для удовлетворения потребности в быстром соединении путей загрузки по требованию.Что касается преобразования оператора импорта, определенно необходимо создать новый узел AST импорта для достижения загрузки по требованию и, наконец, удалить старый оператор импорта. . Новый узел импорта официально поддерживается Babel.@babel/helper-module-imports
генерировать. Теперь продолжите процесс, сначала игнорируя начальный условный оператор if, который будет объяснен позже. Давайте взглянем на несколько ссылок, которые необходимо обработать в функции обработки импорта:
- Измените имя импортированного компонента, преобразование по умолчанию осуществляется в виде объединенных слов "-", например: DatePicker преобразуется в средство выбора даты, а преобразование обработки функции - transCamel.
function transCamel(_str, symbol) {
const str = _str[0].toLowerCase() + _str.substr(1); // 先转换成小驼峰,以便正则获取完整单词
return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`);
// 例 datePicker,正则抓取到P后,在它前面加上指定的symbol符号
}
- Преобразование в определенный путь, по которому находится компонент.Если пользователь плагина указывает собственный путь, используйте customName для обработки,
babel-plugin-import
Почему бы не предоставить форму объекта в качестве параметра? Поскольку модификация customName основана на значении transformMethodName и передается потребителю подключаемого модуля, дизайн может более точно соответствовать пути, который необходимо загрузить по требованию. Функция withPath, которая обрабатывает эти действия, в основном совместима с операционными системами Linux и преобразует '', поддерживаемый файловой системой Windows, в '/'.
function winPath(path) {
return path.replace(/\\/g, '/');
// 兼容路径: windows默认使用‘\’,也支持‘/’,但linux不支持‘\’,遂统一转换成‘/’
}
-
Судя по transformToDefaultImport, эта опция по умолчанию имеет значение true, а преобразованный узел AST находится в виде экспорта по умолчанию.Если вам не нужен экспорт по умолчанию, вы можете установить для transformToDefaultImport значение false, а затем использовать его позже.
@babel/helper-module-imports
Генерируется новый узел импорта, и возвращаемое значение конечной функции ** является идентификатором по умолчанию нового узла импорта, который заменяет узел, вызывающий функцию importMethod, тем самым заменяя все узлы, ссылающиеся на старую привязку импорта, вновь сгенерированным импортом. узел АСТ. -
Наконец, в зависимости от того, включил ли пользователь стиль, импорт по запросу и имеет ли customStyleName дополнительную обработку пути стиля, а также обработку параметров, например styleLibraryDirectory (путь пакета стилей), или создать соответствующий узел загрузки css по запросу.
До сих пор один из самых основных линий преобразования был преобразован. Я считаю, что вы уже поняли основной процесс преобразования нагрузки по требованию. Вернитесь к началу функции ImportMethod.если оператор, что тесно связано с задачей, которую мы будем выполнять на шаге 3. Теперь давайте перейдем к шагу 3 вместе.
Шаг 3: В это время министры встречаются с богами, а не смотрят на них, чиновники знают, что нужно остановиться, и боги хотят действовать.
На шаге 3 выполняются последние два шага преобразования нагрузки по требованию:
- Ссылки, введенные в привязку импорта, определенно относятся не только к синтаксису JSX, но и к другим типам, таким как троичные выражения, наследование классов, операции, операторы оценки, синтаксис возврата и т. д. Мы должны иметь дело с ними, чтобы гарантировать, что все ссылки привязаны к последний импорт, который также приводит кфункция importMethodЗаписано, но мы не должны хотеть, чтобы функцию импорта ссылаться на N раз, генерируют n новых отчетов о импорте, поэтому будет предыдущее заявление о суждении.
- Вошел
ImportDeclaration
При сборе информации мы собираем только зависимости от нее, а не удаляем узлы. и мы не добавили действие, выполняемое выходом узла Program
Далее мы перечислим все узлы AST, которые необходимо обработать, и дадим каждому узлу соответствующий интерфейс (Interface) и примеры (не зацикливайтесь на семантике):
MemberExpression
MemberExpression(path, state) {
const { node } = path;
const file = (path && path.hub && path.hub.file) || (state && state.file);
const pluginState = this.getPluginState(state);
if (!node.object || !node.object.name) return;
if (pluginState.libraryObjs[node.object.name]) {
// antd.Button -> _Button
path.replaceWith(this.importMethod(node.property.name, file, pluginState));
} else if (pluginState.specified[node.object.name] && path.scope.hasBinding(node.object.name)) {
const { scope } = path.scope.getBinding(node.object.name);
// 全局变量处理
if (scope.path.parent.type === 'File') {
node.object = this.importMethod(pluginState.specified[node.object.name], file, pluginState);
}
}
}
MemberExpression (выражение члена атрибута), интерфейс выглядит следующим образом
interface MemberExpression {
type: 'MemberExpression';
computed: boolean;
object: Expression;
property: Expression;
}
/**
* 处理类似:
* console.log(lodash.fill())
* antd.Button
*/
Если в настройках плагина не отключена функция transformToDefaultImport, здесь будет вызываться метод importMethod и возвращаться@babel/helper-module-imports
Задано новое значение узла. В противном случае он будет судить, является ли текущее значение частью собранной информации об импорте и является ли оно глобальной переменной в области файла.Получив область для проверки того, является ли тип ее родительского узла файлом, вы можете избежать ошибочной замены других переменные с тем же именем, например, сцена закрытия.
VariableDeclarator
VariableDeclarator(path, state) {
const { node } = path;
this.buildDeclaratorHandler(node, 'init', path, state);
}
VariableDeclarator (объявление переменной), очень удобно понимать сцену обработки, в основном имеющую дело с операторами объявления const/let/var
interface VariableDeclaration : Declaration {
type: "VariableDeclaration";
declarations: [ VariableDeclarator ];
kind: "var" | "let" | "const";
}
/**
* 处理类似:
* const foo = antd
*/
В этом примере появляется метод BuildDeClaratorHandler, главным образом, чтобы убедиться, что пропущенный атрибут является основным типом идентификатора и представляет собой ссылку, связанную с помощью импорта, а затем входит в ImportMethod для преобразования, а затем возвращает новый узел для перезаписи исходного атрибута.
buildDeclaratorHandler(node, prop, path, state) {
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { types } = this;
const pluginState = this.getPluginState(state);
if (!types.isIdentifier(node[prop])) return;
if (
pluginState.specified[node[prop].name] &&
path.scope.hasBinding(node[prop].name) &&
path.scope.getBinding(node[prop].name).path.type === 'ImportSpecifier'
) {
node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState);
}
}
ArrayExpression
ArrayExpression(path, state) {
const { node } = path;
const props = node.elements.map((_, index) => index);
this.buildExpressionHandler(node.elements, props, path, state);
}
ArrayExpression (выражение массива), интерфейс, как показано ниже
interface ArrayExpression {
type: 'ArrayExpression';
elements: ArrayExpressionElement[];
}
/**
* 处理类似:
* [Button, Select, Input]
*/
Обработка этого примера отличается от обработки других узлов, потому что сам элемент массива является формой массива, а ссылки, которые нам нужно преобразовать, — это все элементы массива, поэтому переданные здесь реквизиты аналогичны [0, 1 , 2, 3] Чистый массив из , что удобно для последующего извлечения данных из элементов. Конкретный метод преобразования здесь — buildExpressionHandler,Будет часто появляться при последующей обработке узлов AST.
buildExpressionHandler(node, props, path, state) {
const file = (path && path.hub && path.hub.file) || (state && state.file);
const { types } = this;
const pluginState = this.getPluginState(state);
props.forEach(prop => {
if (!types.isIdentifier(node[prop])) return;
if (
pluginState.specified[node[prop].name] &&
types.isImportSpecifier(path.scope.getBinding(node[prop].name).path)
) {
node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState);
}
});
}
Сначала просмотрите реквизиты, также убедитесь, что переданные свойства являются базовыми.Identifier
После типа и ссылки, связанной импортом, он входит в importMethod для преобразования, который аналогичен предыдущему методу buildDeclaratorHandler, за исключением того, что props имеет форму массива
LogicalExpression
LogicalExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['left', 'right'], path, state);
}
LogicalExpression (выражение логического оператора)
interface LogicalExpression {
type: 'LogicalExpression';
operator: '||' | '&&';
left: Expression;
right: Expression;
}
/**
* 处理类似:
* antd && 1
*/
В основном извлеките переменные в левой и правой частях выражения логического оператора и используйте метод buildExpressionHandler для преобразования
ConditionalExpression
ConditionalExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['test', 'consequent', 'alternate'], path, state);
}
Содержащая передача (условный оператор)
interface ConditionalExpression {
type: 'ConditionalExpression';
test: Expression;
consequent: Expression;
alternate: Expression;
}
/**
* 处理类似:
* antd ? antd.Button : antd.Select;
*/
В основном извлекайте элементы, похожие на троичные выражения, и используйте метод buildExpressionHandler для их преобразования.
IfStatement
IfStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['test'], path, state);
this.buildExpressionHandler(node.test, ['left', 'right'], path, state);
}
IfStatement (оператор if)
interface IfStatement {
type: 'IfStatement';
test: Expression;
consequent: Statement;
alternate?: Statement;
}
/**
* 处理类似:
* if(antd){ }
*/
Этот узел относительно особенный, но я не понимаю, почему мне нужно дважды вызывать buildExpressionHandler, потому что есть вероятность, о которой я думаю, есть и другие записи AST, которые можно обработать. Читатели, которые хотят знать, могут заниматься наукой.
ExpressionStatement
ExpressionStatement(path, state) {
const { node } = path;
const { types } = this;
if (types.isAssignmentExpression(node.expression)) {
this.buildExpressionHandler(node.expression, ['right'], path, state);
}
}
ВыражениеУтверждение
interface ExpressionStatement {
type: 'ExpressionStatement';
expression: Expression;
directive?: string;
}
/**
* 处理类似:
* module.export = antd
*/
ReturnStatement
ReturnStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['argument'], path, state);
}
RETURNSTATEMENT (оператор RETURN)
interface ReturnStatement {
type: 'ReturnStatement';
argument: Expression | null;
}
/**
* 处理类似:
* return lodash
*/
ExportDefaultDeclaration
ExportDefaultDeclaration(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['declaration'], path, state);
}
ExportDefaultDeclaration (модуль экспорта по умолчанию)
interface ExportDefaultDeclaration {
type: 'ExportDefaultDeclaration';
declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;
}
/**
* 处理类似:
* return lodash
*/
BinaryExpression
BinaryExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['left', 'right'], path, state);
}
BinaryExpression (выражение бинарного оператора)
interface BinaryExpression {
type: 'BinaryExpression';
operator: BinaryOperator;
left: Expression;
right: Expression;
}
/**
* 处理类似:
* antd > 1
*/
NewExpression
NewExpression(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['callee', 'arguments'], path, state);
}
NewExpression (новое выражение)
interface NewExpression {
type: 'NewExpression';
callee: Expression;
arguments: ArgumentListElement[];
}
/**
* 处理类似:
* new Antd()
*/
ClassDeclaration
ClassDeclaration(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['superClass'], path, state);
}
КлассДекларация
interface ClassDeclaration {
type: 'ClassDeclaration';
id: Identifier | null;
superClass: Identifier | null;
body: ClassBody;
}
/**
* 处理类似:
* class emaple extends Antd {...}
*/
Property
Property(path, state) {
const { node } = path;
this.buildDeclaratorHandler(node, ['value'], path, state);
}
Свойство (значение свойства объекта)
/**
* 处理类似:
* const a={
* button:antd.Button
* }
*/
После обработки узла AST удаляем исходный импортный импорт, т.к. у нас есть путь для сохранения старого импорта в pluginState.pathsToRemove, лучшее время удаляетсяProgramExit
Использование path.remove () удалено.
ProgramExit(path, state) {
this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove());
}
Поздравляем всех, кто настоял на том, чтобы увидеть текущих читателей, он достиг последнего шага, зарегистрировав все типы узлов AST, с которыми мы имеем дело, в наблюдателе.
export default function({ types }) {
let plugins = null;
function applyInstance(method, args, context) { ... }
const Program = { ... }
// 补充注册 AST type 的数组
const methods = [
'ImportDeclaration'
'CallExpression',
'MemberExpression',
'Property',
'VariableDeclarator',
'ArrayExpression',
'LogicalExpression',
'ConditionalExpression',
'IfStatement',
'ExpressionStatement',
'ReturnStatement',
'ExportDefaultDeclaration',
'BinaryExpression',
'NewExpression',
'ClassDeclaration',
]
const ret = {
visitor: { Program },
};
for (const method of methods) { ... }
}
На данный момент полностью проанализированоbabel-plugin-import
Весь процесс, читатель может переоценить всю идею обработки загрузки по требованию, на самом деле основная логика относительно проста и понятна без подробностей.
думать
После прочтения исходного кода и модульного тестирования автор обнаружил, что плагин не конвертирует узел Switch, поэтому я отправил PR в официальный репозиторий, который теперь объединен с основной веткой.Если у читателей есть какие-либо идеи, пожалуйста, не стесняйтесь высказываться в области комментариев.
В основном автор добавилSwitchStatement
,SwitchCase
Обработка с двумя узлами AST.SwitchStatement
SwitchStatement(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['discriminant'], path, state);
}
SwitchCase
SwitchCase(path, state) {
const { node } = path;
this.buildExpressionHandler(node, ['test'], path, state);
}
Суммировать
Это первый раз, когда автор пишет статью об анализе исходного кода.Из-за ограниченных возможностей автора, если некоторые логические объяснения недостаточно ясны или в процессе интерпретации есть ошибки, читатели могут высказать предложения или исправьте ошибки в комментариях. Теперь у Babel действительно есть несколько API, которые можно упростить еще больше.babel-plugin-import
кода или логики, например: path.replaceWithMultiple, но некоторая, казалось бы, избыточная логика в исходном коде должна иметь соответствующие сцены, поэтому она будет сохранена. Этот плагин выдержал испытание временем и является отличным примером для читателей, которым необходимо разработать Babel-плагин. Не только это, но и детали маргинализации функций и совместимости операционной системы были полностью обработаны. Если вам просто нужно использоватьbabel-plugin-import
, в этой статье показаны некоторые изbabel-plugin-import
API, не представленный в документе, также может помочь пользователям подключаемых модулей реализовать более расширенные функции, поэтому автор запустил эту статью в надежде помочь студентам.