Статья, написанная несколько месяцев назад, до сих пор не опубликована в «Наггетс».Оригинальная ссылка
предисловие
обычно используетсяantd,elementКогда вы ждете библиотеку компонентов, вы будете использоватьBabelПлагин:babel-plugin-import, В этой статье кратко рассказывается о том, что делает этот плагин, на примерах и анализе исходного кода, а также реализуется минимальная используемая версия.
Адрес плагина:GitHub.com/Анта-дизайн/…
Введение в babel-plugin-import
Зачем: зачем вам этот плагин
antdа такжеelementЭти две библиотеки компонентов, посмотрите на их исходный код,index.jsОни следующие:
// antd
export { default as Button } from './button';
export { default as Table } from './table';
// element
import Button from '../packages/button/index.js';
import Table from '../packages/table/index.js';
export default {
Button,
Table,
};
antdа такжеelementна всем протяженииES6 Moduleизexportэкспортировать отдельные компоненты с именами.
Итак, мы можем пройтиES6изimport { } fromсинтаксис для импорта однокомпонентногоJSдокумент. Однако нам также необходимо вручную импортировать стили компонентов:
// antd
import 'antd/dist/antd.css';
// element
import 'element-ui/lib/theme-chalk/index.css';
если только одинButtonкомпонента, но вводятся все стили, что явно неразумно.
Конечно, вы сказали, что вы также можете использовать только один компонент, и вы также можете уменьшить размер кода:
import Button from 'antd/lib/button';
import 'antd/lib/button/style';
ПС: похожеantdБиблиотека компонентов предоставляетES ModuleПродукт сборки , напрямую черезimport {} fromтакже может быть в видеtree-shaking, это не в сегодняшней теме, поэтому не буду об этом говорить~
Да, в этом нет ничего плохого. Однако посмотрите, когда нам нужно несколько компонентов:
import { Affix, Avatar, Button, Rate } from 'antd';
import 'antd/lib/affix/style';
import 'antd/lib/avatar/style';
import 'antd/lib/button/style';
import 'antd/lib/rate/style';
Считаете ли вы такой код недостаточно элегантным? Если бы это был я, я бы даже хотел кого-нибудь ударить.
В это время следует подумать о том, как ввестиButtonПри автоматическом импорте файлов стилей.
Что: что делает этот плагин
Проще говоря,babel-plugin-importОн предназначен для решения вышеуказанной проблемы, реализации загрузки отдельных компонентов для библиотеки компонентов по требованию и автоматического введения ее стилей, таких как:
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style');
Вам нужно заботиться только о том, какие компоненты нужно импортировать. Мне не нужно заботиться о внутренних стилях. Ничего, если вы поможете мне импортировать их автоматически.
Как: Как использовать этот плагин
Проще говоря, вам нужно заботиться о трех параметрах:
{
"libraryName": "antd", // 包名
"libraryDirectory": "lib", // 目录,默认 lib
"style": true, // 是否引入 style
}
Прочее см. документацию:GitHub.com/Анта-дизайн/…
Анализ исходного кода babel-plugin-import
В основном смотреть наbabel-plugin-importкак загрузитьJavaScriptкод и стиль.
В качестве примера возьмем следующий код:
import { Button, Rate } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);
сбор зависимостей первого шага
babel-plubin-importБудет вImportDeclarationЛи будет всеspecifierсобрал.
Первый взглядastБар:
доступно из этогоImportDeclarationИз заявления извлечены несколько ключевых моментов:
- source.value: antd
- specifier.local.name: Button
- specifier.local.name: Rate
Что нужно сделать тоже просто:
-
importпакет неantd, то есть,libraryName - Пучок
Buttonа такжеRateсобрал
Посмотрите на код:
ImportDeclaration(path, state) {
const { node } = path;
if (!node) return;
// 代码里 import 的包名
const { value } = node.source;
// 配在插件 options 的包名
const { libraryName } = this;
// babel-type 工具函数
const { types } = this;
// 内部状态
const pluginState = this.getPluginState(state);
// 判断是不是需要使用该插件的包
if (value === libraryName) {
// node.specifiers 表示 import 了什么
node.specifiers.forEach(spec => {
// 判断是不是 ImportSpecifier 类型的节点,也就是是否是大括号的
if (types.isImportSpecifier(spec)) {
// 收集依赖
// 也就是 pluginState.specified.Button = Button
// local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton
// imported.name 是真实导出的变量名
pluginState.specified[spec.local.name] = spec.imported.name;
} else {
// ImportDefaultSpecifier 和 ImportNamespaceSpecifier
pluginState.libraryObjs[spec.local.name] = true;
}
});
pluginState.pathsToRemove.push(path);
}
}
обращатьсяbabelпрошел всеImportDeclarationПосле того, как узлы типа собраны, собираются зависимости, и следующим шагом является их загрузка.
Второй шаг – определить, следует ли использовать
После сбора зависимостей должен определить, что этоimportИспользуется ли переменная, здесь речь идет о ситуации.
мы знаем,JSXв конце концов статьReact.createElement()реализовано:
ReactDOM.render(<Button>Hello</Button>);
↓ ↓ ↓ ↓ ↓ ↓
React.createElement(Button, null, "Hello");
Вот так,createElementПервый параметр — это то, что мы ищем, нам нужно определить, являются ли собранные зависимостиcreateElementиспользовать.
Анализировать эту линию кодаast, этот узел легко найти:
Посмотрите на код:
CallExpression(path, state) {
const { node } = path;
const file = (path && path.hub && path.hub.file) || (state && state.file);
// 方法调用者的 name
const { name } = node.callee;
// babel-type 工具函数
const { types } = this;
// 内部状态
const pluginState = this.getPluginState(state);
// 如果方法调用者是 Identifier 类型
if (types.isIdentifier(node.callee)) {
if (pluginState.specified[name]) {
node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
}
}
// 遍历 arguments 找我们要的 specifier
node.arguments = node.arguments.map(arg => {
const { name: argName } = arg;
if (
pluginState.specified[argName] &&
path.scope.hasBinding(argName) &&
path.scope.getBinding(argName).path.type === 'ImportSpecifier'
) {
// 找到 specifier,调用 importMethod 方法
return this.importMethod(pluginState.specified[argName], file, pluginState);
}
return arg;
});
}
КромеReact.createElement(Button)Кроме того, естьconst btn = Button / [Button]...и многие другие ситуации будут использоватьсяButton, в исходниках есть соответствующие методы обработки, если интересно, можете сами глянуть:GitHub.com/Анта-дизайн/…, тут особо нечего сказать.
Третий шаг — сгенерировать код импорта (ядро)
Основная работа первого и второго шагов заключается в поиске зависимостей, которые должны обрабатываться плагином, таких как:
import { Button, Rate } from 'antd';
ReactDOM.render(<Button>Hello</Button>);
Buttonиспользуются компоненты,RateНе используется в коде. Так что плагин должен автоматически импортироватьButtonкод и стиль.
Давайте сначала подведем итоги, когда мыimportПри создании компонента вы хотите, чтобы он:
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style');
И снова вспомнить конфигурацию плагинаoptions, просто изменитеlibraryDirectoryтак же какstyleПросто подождите, пока конфигурация будет использована.
Дети, у вас есть несколько вопросительных знаков? Как сделать здесьbabelЧтобы изменить код и сгенерировать новыйimportи стильimportЧто ж, не паникуйте, просто посмотрите на код, и вы поймете:
import { addSideEffect, addDefault, addNamed } from '@babel/helper-module-imports';
importMethod(methodName, file, pluginState) {
if (!pluginState.selectedMethods[methodName]) {
// libraryDirectory:目录,默认 lib
// style:是否引入样式
const { style, libraryDirectory } = this;
// 组件名转换规则
// 优先级最高的是配了 camel2UnderlineComponentName:是否使用下划线作为连接符
// camel2DashComponentName 为 true,会转换成小写字母,并且使用 - 作为连接符
const transformedMethodName = this.camel2UnderlineComponentName
? transCamel(methodName, '_')
: this.camel2DashComponentName
? transCamel(methodName, '-')
: methodName;
// 兼容 windows 路径
// path.join('antd/lib/button') == 'antd/lib/button'
const path = winPath(
this.customName
? this.customName(transformedMethodName, file)
: join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName),
);
// 根据是否有导出 default 来判断使用哪种方法来生成 import 语句,默认为 true
// addDefault(path, 'antd/lib/button', { nameHint: 'button' })
// addNamed(path, 'button', 'antd/lib/button')
pluginState.selectedMethods[methodName] = this.transformToDefaultImport
? addDefault(file.path, path, { nameHint: methodName })
: addNamed(file.path, methodName, path);
// 根据不同配置 import 样式
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) {
addSideEffect(file.path, `${path}/style`);
} else if (style === '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] };
}
addSideEffect, addDefaultа такжеaddNamedда@babel/helper-module-importsВсе три метода создаютimportметод, удельная производительность:
addSideEffect
addSideEffect(path, 'source');
↓ ↓ ↓ ↓ ↓ ↓
import "source"
addDefault
addDefault(path, 'source', { nameHint: "hintedName" })
↓ ↓ ↓ ↓ ↓ ↓
import hintedName from "source"
addNamed
addNamed(path, 'named', 'source', { nameHint: "hintedName" });
↓ ↓ ↓ ↓ ↓ ↓
import { named as _hintedName } from "source"
больше о@babel/helper-module-importsВидеть:@babel/helper-module-imports
Суммировать
Считаем 1 2 3 вместе,babel-plugin-importЧто нужно сделать, то сделано.
Подведем итоги,babel-plugin-importи общийbabelКак и плагин, он будет проходить кодast, затем вastсделал несколько вещей:
-
Собрать зависимости:оказаться
importDeclaration, проанализируйте пакетaи зависимостиb,c,d....,еслиaа такжеlibraryNameсогласен, будетb,c,d...собранный внутри -
определить, использовать ли: в различных ситуациях (например, упомянутых в
CallExpression) судить о собранномb,c,d...Используется ли он в коде, если используется, то называетсяimportMethodгенерировать новыеimpportутверждение -
Сгенерировать код импорта: создание кода и стилей на основе элементов конфигурации.
importутверждение
Однако здесь не упомянуты некоторые детали, например, как удалить старый.importПодождите... Если вам интересно, вы можете прочитать исходный код самостоятельно.
Прочитав исходный код, вы нашли что-нибудь?antdа такжеelementВ дополнение к большим библиотекам компонентов можно использовать любую библиотеку компонентов.babel-plugin-importДля достижения загрузки по требованию и стилей автоматической загрузки.
Да, такие как наши обычно используемыеlodash, вы также можете использоватьbabel-plugin-importРазличные способы загрузки, вы можете попробовать.
Практическая реализация babel-plugin-import
Прочитав так много, я сам придумал упрощенную версиюbabel-plugin-importБар.
Если вы не знаете, как реализоватьBabelплагин, могу читать[Введение в плагин Babel] Как использовать Babel для автоматического добавления зависимостей в код
Простейшая реализация функции
Согласно вышеизложенному, наиболее важными элементами конфигурации являются три:
{
"libraryName": "antd",
"libraryDirectory": "lib",
"style": true,
}
Поэтому мы реализуем только эти три элемента конфигурации.
И, как упоминалось выше, есть много способов вызвать компонент в реальных ситуациях, мы не рассматриваем здесь эти сложные ситуации, а реализуем только самые распространенные.<Button />передача.
входной файл
Роль входного файла состоит в том, чтобы получить элементы конфигурации, переданные пользователем, и применить основной код подключаемого модуля кastначальство.
import Plugin from './Plugin';
export default function ({ types }) {
let plugins = null;
// 将插件作用到节点上
function applyInstance(method, args, context) {
for (const plugin of plugins) {
if (plugin[method]) {
plugin[method].apply(plugin, [...args, context]);
}
}
}
const Program = {
// ast 入口
enter(path, { opts = {} }) {
// 初始化插件实例
if (!plugins) {
plugins = [
new Plugin(
opts.libraryName,
opts.libraryDirectory,
opts.style,
types,
),
];
}
applyInstance('ProgramEnter', arguments, this);
},
// ast 出口
exit() {
applyInstance('ProgramExit', arguments, this);
},
};
const ret = {
visitor: { Program },
};
// 插件只作用在 ImportDeclaration 和 CallExpression 上
['ImportDeclaration', 'CallExpression'].forEach(method => {
ret.visitor[method] = function () {
applyInstance(method, arguments, ret.visitor);
};
});
return ret;
}
основной код
реальная модификацияastКодpluginРеализовано:
import { join } from 'path';
import { addSideEffect, addDefault } from '@babel/helper-module-imports';
/**
* 转换成小写,添加连接符
* @param {*} _str 字符串
* @param {*} symbol 连接符
*/
function transCamel(_str, symbol) {
const str = _str[0].toLowerCase() + _str.substr(1);
return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`);
}
/**
* 兼容 Windows 路径
* @param {*} path
*/
function winPath(path) {
return path.replace(/\\/g, '/');
}
export default class Plugin {
constructor(
libraryName, // 需要使用按需加载的包名
libraryDirectory = 'lib', // 按需加载的目录
style = false, // 是否加载样式
types, // babel-type 工具函数
) {
this.libraryName = libraryName;
this.libraryDirectory = libraryDirectory;
this.style = style;
this.types = types;
}
/**
* 获取内部状态,收集依赖
* @param {*} state
*/
getPluginState(state) {
if (!state) {
state = {};
}
return state;
}
/**
* 生成 import 语句(核心代码)
* @param {*} methodName
* @param {*} file
* @param {*} pluginState
*/
importMethod(methodName, file, pluginState) {
if (!pluginState.selectedMethods[methodName]) {
// libraryDirectory:目录,默认 lib
// style:是否引入样式
const { style, libraryDirectory } = this;
// 组件名转换规则
const transformedMethodName = transCamel(methodName, '');
// 兼容 windows 路径
// path.join('antd/lib/button') == 'antd/lib/button'
const path = winPath(join(this.libraryName, libraryDirectory, transformedMethodName));
// 生成 import 语句
// import Button from 'antd/lib/button'
pluginState.selectedMethods[methodName] = addDefault(file.path, path, { nameHint: methodName });
if (style) {
// 生成样式 import 语句
// import 'antd/lib/button/style'
addSideEffect(file.path, `${path}/style`);
}
}
return { ...pluginState.selectedMethods[methodName] };
}
ProgramEnter(path, state) {
const pluginState = this.getPluginState(state);
pluginState.specified = Object.create(null);
pluginState.selectedMethods = Object.create(null);
pluginState.pathsToRemove = [];
}
ProgramExit(path, state) {
// 删除旧的 import
this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove());
}
/**
* ImportDeclaration 节点的处理方法
* @param {*} path
* @param {*} state
*/
ImportDeclaration(path, state) {
const { node } = path;
if (!node) return;
// 代码里 import 的包名
const { value } = node.source;
// 配在插件 options 的包名
const { libraryName } = this;
// babel-type 工具函数
const { types } = this;
// 内部状态
const pluginState = this.getPluginState(state);
// 判断是不是需要使用该插件的包
if (value === libraryName) {
// node.specifiers 表示 import 了什么
node.specifiers.forEach(spec => {
// 判断是不是 ImportSpecifier 类型的节点,也就是是否是大括号的
if (types.isImportSpecifier(spec)) {
// 收集依赖
// 也就是 pluginState.specified.Button = Button
// local.name 是导入进来的别名,比如 import { Button as MyButton } from 'antd' 的 MyButton
// imported.name 是真实导出的变量名
pluginState.specified[spec.local.name] = spec.imported.name;
} else {
// ImportDefaultSpecifier 和 ImportNamespaceSpecifier
pluginState.libraryObjs[spec.local.name] = true;
}
});
// 收集旧的依赖
pluginState.pathsToRemove.push(path);
}
}
/**
* React.createElement 对应的节点处理方法
* @param {*} path
* @param {*} state
*/
CallExpression(path, state) {
const { node } = path;
const file = (path && path.hub && path.hub.file) || (state && state.file);
// 方法调用者的 name
const { name } = node.callee;
// babel-type 工具函数
const { types } = this;
// 内部状态
const pluginState = this.getPluginState(state);
// 如果方法调用者是 Identifier 类型
if (types.isIdentifier(node.callee)) {
if (pluginState.specified[name]) {
node.callee = this.importMethod(pluginState.specified[name], file, pluginState);
}
}
// 遍历 arguments 找我们要的 specifier
node.arguments = node.arguments.map(arg => {
const { name: argName } = arg;
if (
pluginState.specified[argName] &&
path.scope.hasBinding(argName) &&
path.scope.getBinding(argName).path.type === 'ImportSpecifier'
) {
// 找到 specifier,调用 importMethod 方法
return this.importMethod(pluginState.specified[argName], file, pluginState);
}
return arg;
});
}
}
При этом достигается простейшееbabel-plugin-importПлагин, который может автоматически загружать отдельные пакеты и стили.
Полный код:GitHub.com/Axue Temple/Папа…
Суммировать
Благодаря анализу исходного кода и практическим занятиям в этой статье представлены подробные объяснения простым языком.babel-plugin-importПринцип работы плагина, надеюсь, прочитав эту статью, вы сможете четко понять, что делает этот плагин.
Для получения дополнительных статей вы можете обратить внимание на публичный аккаунт «Front-end Trial» и делиться ежедневными избранными статьями.
оBabelНекоторые ссылки, которые вы будете использовать: