- PVSM.RU - https://www.pvsm.ru -
Раньше использовал Vuetify [1] в качестве UI библиотеки. В связи с его сомнительной репутации [2], отказался от него, но пока что не нашел ни одной свободной библиотеки, что реализовала бы все его достоинства, одним из которых, является глобальная конфигурация [3].
Сейчас использую Element Plus [4], так как используется на основной работе и она на равных с другими схожими библиотеками. У него тоже есть глобальная конфигурация [5], но он очень кастрирован - я не могу глобально настроить конкретный компонент.
В начале разработки, были поставлены требование к проекту, чтобы определенные элементы по умолчанию вели себя одинакова.
Для примера возьмем таблицу: нужно чтобы все таблицы окрашивались через строку.
За это в компоненте ElTable [6] отвечает prop stripe [7]:
<template>
<el-table :data="tableData" stripe>
<!-- ... -->
</el-table>
</template>
Передавать в компонент в ручную. Очевидное и самое простое решение. Но, это обязывает программиста каждый раз помнить, что при использование того, или иного компонента, нужно предавать определенные значение. Высокий риск человеческого фактора.
Создать некий wrapper, в которому будет находится наша таблица по умолчанию. Не лучше первого решение, так как программисту вместо очевидного ElTable, нужно использовать некий TableWrapper. В добавок, это решение ломает подсказки в текстовых редакторах и IDE-шках.
Мы перехватывает импортируемый компонент, изменяем default в нужном props и после этого импортируем его. Так, мы используем лишь открытый Api компонента, сохраняем подсказки в текстовых редакторах и пользователь может переопределить значение. Этот способ и будет использован.
Если создается props, только с типом, без указание default, то он он сведется к функции, а не к объекту
const props = defineProps({ border: { type: Boolean } });
будет преобразован в
border: ƒ Boolean()
тогда как нам нужно:
border: {default: true, type: ƒ Boolean()}
Создадим функцию, который будет принимать компонент и объект prop-сов, который хотим изменить:
const setDefaultProps = (component: any, defaultProps: Record<string, any>) =>
Object.entries(defaultProps).forEach(([propName, propValue]) => {
// если компонент не содержит заданный props, идем дальше
if (!Object.prototype.hasOwnProperty.call(component.props, propName))
return;
// если prop компонента является функцией, преобразовываем в объект, в ином случай возвращаем его
const propBody = Object.prototype.hasOwnProperty.call(
component.props[propName],
"type"
)
? component.props[propName]
: { type: component.props[propName] };
component.props[propName] = {
...propBody,
default: propValue, // задаем по умолчанию нужное значение
};
});
У аргумента component указан тип any вместо DefineComponent, потому-что element-plus использует свои тип SFCWithInstall, который не совместим с ним, но по итогу, все равно сводится к нему.
<template>
<el-table-custom :data="list">
<el-table-column prop="name" />
</el-table-custom>
</template>
<script setup lang="ts">
import { ElTable as ElTableCustom } from "element-plus";
setDefaultProps(ElTableCustom, { stripe: true });
const name = ref([
{ name: "foo" },
{ name: "bar" },
{ name: "hello" },
{ name: "world" },
]);
</script>
Все прекрасно работает... Хотел бы я сказать, но если захотим изменить компонент ElTooltip, то ничего не получится.
Как писал ранее, компоненты element-plus реализуют свой тип SFCWithInstall, который изнутри не напрямую используют props, из-за чего изменение default не приведут к желаемому результату.
Есть еще одно решение, более грубое: изменение props перед запуском метода setup:
const setPropsUnsafe = (component: any, defaultProps: Record<string, any>) => {
const setup = component.setup!; // сохраняем, чтобы не получить рекурсию
component.setup = (props: any, ctx: any) => {
const newProps = { ...props }; // снимаем защиту readonly Proxy
Object.entries(defaultProps).forEach(([propName, propValue]) => {
// если компонент не содержить заданный props, идем дальше
if (!Object.prototype.hasOwnProperty.call(component.props, propName))
return;
if (
!Object.prototype.hasOwnProperty.call(component.props[propName], "default") ||
component.props[propName]["default"] === newProps[propName]
)
newProps[propName] = propValue;
});
return setup(shallowReadonly(shallowReactive(newProps)), ctx);
};
};
Меняем только в тому случай, если prop равен значению default
Мы полностью снимаем Proxy у props, так как она не позволяет его изменять, а после оборачиваем обратно. Из-за этого, возможны непредсказуемые поведение. Теперь все должно работать... Но даже так, изменение некоторых prop-сов не приводит ни к чему (В случай с ElTooltip - это content и enterable). Но пока что, хватает того, что есть.
Создаем интерфейс настроек
export interface SettingComponent {
component: Record<string, any>;
props: Record<string, any>;
unsafeProps?: boolean;
}
Так как в Resolver нам нужно указывать, что нам нужно импортировать, указываем у component тип Record<string, any>, чтобы воспользоваться хитростью сокращенной записью - { hello } будет преобразован в { 'hello': hello }, и мы сможем достать название компонента.
Создадим функцию settingRun который запускает ранее описанные функции.
export function settingRun(settings: SettingComponent[]) {
for (const setting of settings) {
const [[_, component]] = Object.entries(setting.component);
if (setting.unsafeProps) setPropsUnsafe(component, setting.props);
else setDefaultProps(component, setting.props);
}
}
Создадим 2 файла
globalSetting.ts - будет хранит сами настройки
globalSettingLib.ts - будет хранит всю логику
Будем использовать unplugin-vue-components который создан для автоматического импорта компонентов в template.
Element plus рекомендует использовать Auto import [8], так что у нас уже имеется пакеты unplugin-vue-components.
По сути, resolver [9], является функции, который на вход принимает имя компонента в CapitalCase и возвращает объект который указывает что и откуда импортировать:
Для этого нам и нужен файл globalSetting.ts, где мы импортируем компоненты, который хотим изменить, меняем их и экспортируем в вне.
// globalSetting.ts
import { ElTable, ElTooltip } from "element-plus";
import { SettingComponent, settingRun } from "./globalSettingLib";
export const settings: SettingComponent[] = [
{
component: { ElTable },
props: { border: true, stripe: true, size: "small", tableLayout: "auto" },
},
{
component: { ElTooltip },
props: { showAfter: 500 },
unsafeProps: true,
},
];
settingRun(settings); // меняем компонент
export { ElTable, ElTooltip, ElDialog };
Резолвер
// globalSettingLib.ts
export function SettingComponentsResolver(
settings: SettingComponent[],
from: string
): ComponentResolverFunction {
const names = settings.map((i) => Object.keys(i.component)[0]);
return (name: string) => {
if (names.includes(name)) {
return {
name: name,
from: from,
};
}
};
}
По итогу globalSettingLib.ts получается:
import { ComponentResolverFunction } from "unplugin-vue-components/types";
const setDefaultProps = (component: any, defaultProps: Record<string, any>) =>
Object.entries(defaultProps).forEach(([propName, propValue]) => {
if (!Object.prototype.hasOwnProperty.call(component.props, propName))
return;
const propBody = Object.prototype.hasOwnProperty.call(
component.props[propName],
"type"
)
? component.props[propName]
: { type: component.props[propName] };
component.props[propName] = {
...propBody,
default: propValue,
};
});
const setPropsUnsafe = (component: any, defaultProps: Record<string, any>) => {
const setup = component.setup!;
component.setup = (props: any, ctx: any) => {
const newProps = { ...props };
Object.entries(defaultProps).forEach(([propName, propValue]) => {
if (!Object.prototype.hasOwnProperty.call(component.props, propName))
return;
if (
!Object.prototype.hasOwnProperty.call(
component.props[propName],
"default"
) ||
component.props[propName]["default"] === newProps[propName]
)
newProps[propName] = propValue;
});
return setup(shallowReadonly(shallowReactive(newProps)), ctx);
};
};
export interface SettingComponent {
component: Record<string, any>;
props: Record<string, any>;
unsafeProps?: boolean;
}
export function settingRun(settings: SettingComponent[]) {
const names: string[] = [];
for (const setting of settings) {
const [[name, component]] = Object.entries(setting.component);
names.push(name);
if (setting.unsafeProps) setPropsUnsafe(component, setting.props);
else setDefaultProps(component, setting.props);
}
return names;
}
export function SettingComponentsResolver(
settings: SettingComponent[],
from: string
): ComponentResolverFunction {
const names = settings.map((i) => Object.keys(i.component)[0]);
return (name: string) => {
if (names.includes(name)) {
return {
name: name,
from: from,
};
}
};
}
Остается лишь передать resolver внутри vite.config.ts
export default defineConfig({
// ...other code
plugins: [
// ...other code
Components({
resolvers: [
SettingComponentsResolver(settings, "@/plugins/globalSetting"),
// ...other resolvers
],
}),
],
});
Автор: new_error
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/global-naya-nastrojka-komponenta/400968
Ссылки в тексте:
[1] Vuetify: https://vuetifyjs.com/en
[2] сомнительной репутации: https://habr.com/ru/articles/709492/
[3] глобальная конфигурация: https://vuetifyjs.com/en/features/global-configuration/
[4] Element Plus: https://element-plus.org/en-US/
[5] глобальная конфигурация: https://element-plus.org/en-US/guide/quickstart.html#global-configuration
[6] ElTable: https://element-plus.org/en-US/component/table.html
[7] stripe: https://element-plus.org/en-US/component/table.html#striped-table
[8] Auto import: https://element-plus.org/en-US/guide/quickstart.html#auto-import-recommend
[9] resolver: https://github.com/unplugin/unplugin-vue-components?tab=readme-ov-file#importing-from-ui-libraries
[10] Источник: https://habr.com/ru/articles/854308/?utm_source=habrahabr&utm_medium=rss&utm_campaign=854308
Нажмите здесь для печати.