- PVSM.RU - https://www.pvsm.ru -

Глобальная настройка любого компонента в Vue

Введение

Раньше использовал Vuetify [1] в качестве UI библиотеки. В связи с его сомнительной репутации [2], отказался от него, но пока что не нашел ни одной свободной библиотеки, что реализовала бы все его достоинства, одним из которых, является глобальная конфигурация [3].

Сейчас использую Element Plus [4], так как используется на основной работе и она на равных с другими схожими библиотеками. У него тоже есть глобальная конфигурация [5], но он очень кастрирован - я не могу глобально настроить конкретный компонент.

Проблема

В начале разработки, были поставлены требование к проекту, чтобы определенные элементы по умолчанию вели себя одинакова.

Для примера возьмем таблицу: нужно чтобы все таблицы окрашивались через строку.

За это в компоненте ElTable [6] отвечает prop stripe [7]:

<template>
  <el-table :data="tableData" stripe>
    <!-- ... -->
  </el-table>
</template>

Варианты решение

Решение 1 - ручная передача

Передавать в компонент в ручную. Очевидное и самое простое решение. Но, это обязывает программиста каждый раз помнить, что при использование того, или иного компонента, нужно предавать определенные значение. Высокий риск человеческого фактора.

Решение 2 - обертка

Создать некий wrapper, в которому будет находится наша таблица по умолчанию. Не лучше первого решение, так как программисту вместо очевидного ElTable, нужно использовать некий TableWrapper. В добавок, это решение ломает подсказки в текстовых редакторах и IDE-шках.

Решение 3 - перехват компонентов перед импортом

Мы перехватывает импортируемый компонент, изменяем default в нужном props и после этого импортируем его. Так, мы используем лишь открытый Api компонента, сохраняем подсказки в текстовых редакторах и пользователь может переопределить значение. Этот способ и будет использован.

Особенности Vue props

Если создается props, только с типом, без указание default, то он он сведется к функции, а не к объекту

const props = defineProps({ border: { type: Boolean } });

будет преобразован в

border: ƒ Boolean()

тогда как нам нужно:

border: {default: true, type: ƒ Boolean()}

Реализация

Изменение default props

Создадим функцию, который будет принимать компонент и объект 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 получается:

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