Статья, написанная несколько месяцев назад, до сих пор не опубликована в «Наггетс».Оригинальная ссылка
предисловие
обычно используется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
Некоторые ссылки, которые вы будете использовать: