Загружайте более элегантные решения по запросу - vite-plugin-components

Vite
Загружайте более элегантные решения по запросу - vite-plugin-components

Это третий день моего участия в Gengwen Challenge, смотрите подробности мероприятияОбновить вызов

Загружать самое раннее использование по запросу

и загружает по запросу

Конфигурация плагина

в vite.config.ts

import usePluginImport from "vite-plugin-importer";

export default defineConfig({
   plugins: [
      usePluginImport({
           libraryName: "ant-design-vue",
           libraryDirectory: "es",
           style: "css",
      }),
   ]
 })

использовать

import { Button, List, Checkbox, Popconfirm, Input } from "ant-design-vue";

 components: {
    [Checkbox.name]: Checkbox,
    [Input.name]: Input,
    [List.name]: List,
    [List.Item.name]: List.Item,
     AListItemMeta: List.Item.Meta, //这里用框架List.Item.Meta.name注册不上只能写死,可能使用displayName可以
    [Button.name]: Button,
    [Popconfirm.name]: Popconfirm,
 },

Болевые точки:

  • Поскольку библиотека компонентов antd использует множество подкомпонентов, таких какListкомпоненты подListItem, если вы зарегистрируете на один меньше, шаблон не будет парситься
  • Нужно ввести его снова, а потом зарегистрироватьсяkeyНапишите еще раз, снова напишите значение, один и тот же код нужно написать три раза, и обратите внимание на взаимосвязь между дочерним компонентом и родительским компонентом.
  • Некоторые компоненты не представленыnameсвойства, такие какAListItemMetaПоэтому его нужно писать до смерти, что приводит к несовместимости стилей.

элемент-плюс нагрузки по запросу

Конфигурация плагина

в vite.config.ts

import styleImport from "vite-plugin-style-import";

export default defineConfig({
   plugins: [
      styleImport({
      libs: [
        {
          libraryName: "element-plus",
          esModule: true,
          ensureStyleFile: true,
          resolveStyle: name => {
            return `element-plus/lib/theme-chalk/${name}.css`;
          },
          resolveComponent: name => {
            return `element-plus/lib/${name}`;
          },
        },
      ],
    }),
   ]
 })

Как использовать

import { ElForm, ElButton, ElFormItem, ElInput } from "element-plus";
 
components: {
   ElForm,
   ElButton,
   ElInput,
   ElFormItem,
},

Болевые точки:

  • Один и тот же компонент родитель-потомок должен быть введен дважды, напримерFormа такжеFormItem,а такжеantdразница в томelementРодительский и дочерний компоненты должны быть представлены отдельно и могут использоваться толькоcomponentsметод регистрации, ноantdКроме того, он также поддерживает регистрацию подключаемых модулей, используяapp.use(xxx)

Улучшать

Чтобы решить проблему, вызванную компонентами «родитель-потомок», представленными компонентами «родитель-потомок» antd,antdпредоставленapp.use(xx)То, как этот плагин зарегистрирован,antdПроблема зависимостей между подкомпонентами автоматически решается внутри: например, чтобы использовать компонент List, нужно всего лишьapp.use(List)готов использоватьList和ListItem,ListItemMeta, что гораздо удобнее

Поэтому я написалuseCompметод с использованием app.use(comp);зарегистрироваться В vue3 вам сначала нужно получить экземпляр приложения, vue3 предоставляетgetCurrentInstanceЭтот метод может получить экземпляр текущего компонента, а затем получить объект приложения в глобальном контексте через текущий экземпляр,instance?.appContext.app;, чтобы для регистрации можно было использовать app.use.Также следует отметить, что один и тот же компонент позволяет избежать повторной регистрации, и нужно записывать уже зарегистрированные компоненты.

Код useAntd.ts выглядит следующим образом:

import { Plugin, getCurrentInstance } from "vue";

interface Registed {
  [key: string]: boolean;
}
let registed: Registed = {};

type Comp = {
  displayName?: string;
  name?: string;
} & Plugin;

type RegisteComps = (...comps: Comp[]) => void;

export const useComp: RegisteComps = (...comps) => {
  comps.forEach(comp => {
    const name = comp.displayName || comp.name;

    if (name && !registed[name]) {
      const instance = getCurrentInstance();
     
      const app = instance?.appContext.app;

      if (app) {
        app.use(comp);
        registed[name] = true;
      }
    }
  });
};


Как использовать:


 import { List, Table, Button, Space } from "ant-design-vue";
 import { useComp } from "@/hooks/useAntd";
 //...略
 setup() {
     useComp(List, Table, Button, Space);
     return {} 
 }

Решить болевые точки:

  • Нет необходимости в зависимостях между родительскими и дочерними компонентами
  • уменьшатьcomponentsзарегистрированный код
  • Какие компоненты используются, непосредственноimportНазвание компонента в скобках, скопируйте и вставьте вuseCompВ методе это несложная операция.

Оставшиеся болевые точки:

  • element,naiveеще нужно использоватьcomponentsЗарегистрируйтесь по одному
  • По сравнению с tsx в настройке на одну строчку регистрационного кода больше.

Идеальная форма:

Используемые компоненты не должны заботиться об импорте и регистрации, как и полный импорт, их можно использовать непосредственно в шаблоне, который можно удобно написать, не беспокоясь о том, что вы забудете зарегистрировать компоненты.

Долго искал подобный метод, пока не зашел в сообщество и не нашел большого парняantfuс открытым исходным кодомvite-plugin-components, именно то, что я искал. Достигаемая им функция: автоматически анализировать компоненты, используемые в шаблоне, а затем автоматически вводить их по мере необходимости, и больше не требуется ручная регистрация.

Но в идеале очень пухленькая, в реальности очень худенькая, да и на некоторые ямы наступил

Конфигурация плагина:

import ViteComponents, {
  AntDesignVueResolver,
  ElementPlusResolver,
  ImportInfo,
  kebabCase,
  NaiveUiResolver,
  pascalCase,
} from "vite-plugin-components";

 
export default defineConfig({
  plugins: [
    ViteComponents({
      customComponentResolvers: [
        AntDesignVueResolver(),//官方插件提供
        ElementPlusResolver(),//官方插件提供
        NaiveUiResolver(),//官方插件提供
      ]
    })
  ]
})

Результат попытки:

  • naiveui идеальная поддержка, как!
  • Официальный scss, используемый elementui, должен установить зависимости scss
  • antdv имеет только несколько компонентов, которые можно разобрать, напримерlayout,list tableЭтот общий компонент не может быть решен

решить:

element-plus перезаписывает распознаватель:

Причина перезаписи: Поскольку проект не использует scss и использует меньше, поэтому конвертируйте scss в css для загрузки стилей.

Официальная формулировка выглядит следующим образом:

 const { importStyle = true } = options
  if (name.startsWith('El')) {
    const partialName = name[2].toLowerCase() + name.substring(3).replace(/[A-Z]/g, l => `-${l.toLowerCase()}`)
    return {
      path: `element-plus/es/el-${partialName}`,
      sideEffects: importStyle ? `element-plus/packages/theme-chalk/src/${partialName}.scss` : undefined,
    }
  }

после перезаписи: изменение пути, импортированного из каталога lib, прямое использование имени компонента в качестве имени каталога файлов для импорта и отказ от сложного преобразования имен компонентов. код показывает, как показано ниже:

  customComponentResolvers: [
        // AntDesignVueResolver(),
        // ElementPlusResolver(),
        NaiveUiResolver(),
        name => {
          if (name.startsWith("El")) {
            // Element UI
            const partialName = kebabCase(name); //ElButton->el-button
            return {
              path: `element-plus/lib/${partialName}`,
              sideEffects: `element-plus/lib/theme-chalk/${partialName}.css`,
            };
          } 
        }
     ]


antdv

Официальная практика такова:

export const AntDesignVueResolver = (): ComponentResolver => (name: string) => {
  if (name.match(/^A[A-Z]/))
    return { importName: name.slice(1), path: 'ant-design-vue/es' }
}

Существующие проблемы:

  • <a-list-item>Этот компонент недоступенant-design-vue/es/list-itemИщем этот каталог, такого каталога нет, на самом деле его настоящий каталогant-design-vue/es/list/Item.js

  • <a-layout-contentУ компонента нет соответствующего пути, он генерируется методом генератора в макете и привязывается к объекту макета, его фактический путь должен бытьant-design-vue/es/layout/layout.jsсвойство содержимого

  • <a-select-option>Этот компонент также должен быть выбран, но на самом деле он импортирован.vc-select/Option.jsПринадлежит к базовому компоненту

  • <a-menu-item>Компоненты — это подкомпоненты, принадлежащие меню, а каталог — этоant-design-vue/es/menu/MenuItem.js, Это отличается от предыдущих правил. Я думал, что это должно называться Item, но здесь оно отличается, поэтому требует особого отношения.

  • а также<a-tab-pane>Для такого компонента каталог стилей, который ему нужен, находится на вкладках, но на самом деле его файловый каталог находится вvc-tabs/src, он также требует специального лечения

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

разрешить путь к компонентуgetCompPathметод, код выглядит следующим образом:

function getCompPath(
  compName: string
): {
  dirName: string;
  compName: string;
  styleDir: string;
  importName?: string;
  sideEffects?: ImportInfo;
} {
  const hasSubComp = [
    "Menu",
    "Layout",
    "Form",
    "Table",
    "Modal",
    "Radio",
    "Button",
    "Checkbox",
    "List",
    "Collapse",
    "Descriptions",
    "Tabs",
    "Mentions",
    "Select",
    "Anchor",
    "Typography",
    // "TreeSelect",
  ]; //包含子组件的组件
  const keepSelf = [
    "MenuItem",
    "SubMenu",
    "FormItem",
    "RadioButton",
    "CollapsePanel",
    "TabPane",
    "AnchorLink",
  ]; //保留原子组件名称
  const keepFather = [
    "LayoutHeader",
    "LayoutContent",
    "LayoutFooter",
    "DescriptionsItem",
  ]; //需要使用父组件名称的子组件  LayoutFooter->''  之所以转成空是因为最后拼接的结果是dirName+compName,避免重复
  const rootName = hasSubComp.find((name: string) => compName.startsWith(name));
  const usePrevLevelName = ["ListItemMeta"]; //使用当前组件的上一级名称  ListItemMeta->Item
  const getPrevLevelName = () => {
    const split = kebabCase(compName).split("-");
    return pascalCase(split[split.length - 2]);
  };

  const fatherAlias = {
    TabPane: "vc-tabs/src",
    MentionsOption: "vc-mentions/src",
    SelectOption: "vc-select",
    TreeSelectNode: "vc-tree-select/src",
  };

  const compAlias = {
    TreeSelectNode: "SelectNode",
  };

  const styleMap = {
    TabPane: "tabs",
    MentionsOption: "mentions",
    SelectOption: "select",
    TreeSelectNode: "tree-select",
  };
  // const importNameMap = {
  //   LayoutContent: "Content",
  //   LayoutHeader: "Header",
  //   LayoutFooter: "Footer",
  // };

  let dirName = rootName?.toLowerCase() ?? kebabCase(compName);

  if (fatherAlias[compName]) {
    dirName = fatherAlias[compName];
  }

  let compNameStr = "";
  if (keepSelf.includes(compName)) {
    compNameStr = compName;
  } else if (keepFather.includes(compName)) {
    compNameStr = "";
  } else if (usePrevLevelName.includes(compName)) {
    compNameStr = getPrevLevelName();
  } else if (rootName) {
    compNameStr = compName.replace(rootName, "");
  }
  const compRequired = {
    TypographyTitle: "ant-design-vue/es/" + dirName + "/Base",
    TypographyText: "ant-design-vue/es/" + dirName + "/Base",
  };

  return {
    // importName: importNameMap[compName],
    dirName: fatherAlias[compName] ?? dirName,
    styleDir: `${styleMap[compName] ?? dirName}`,
    compName: compAlias[compName] ?? compNameStr,
    sideEffects: compRequired[compName]
      ? {
          path: compRequired[compName],
        }
      : undefined,
  };
}


Пользовательский преобразователь, код выглядит следующим образом

 ViteComponents({
      customComponentResolvers: [
    
        name => {
          if (name.match(/^A[A-Z]/)) {
            //ant-design-vue

            const importName = name.slice(1);
            const dirName = kebabCase(importName);
            const compName = pascalCase(importName); //AListItem->ListItem
            const compPath = getCompPath(compName);//这里解析组件的真实路径

            const sideEffects = [
              {
                path: `ant-design-vue/es/${compPath.styleDir}/style`,
              },
            ];
            if (compPath.sideEffects) {
              sideEffects.push(compPath.sideEffects);
            }
            return {
              path: `ant-design-vue/es/${compPath.dirName}/${compPath.compName}`,
              sideEffects,
            };
          }
          return null;
        },
      ],
      globalComponentsDeclaration: true,
    }),

После анализа большинство компонентов можно использовать, а некоторые оставшиеся компоненты нельзя использовать в обычном режиме.

Файлы объявлений некоторых компонентов, сгенерированных плагином, выглядят следующим образом:


declare module 'vue' {
  export interface GlobalComponents {
    AMenuItem: typeof import('ant-design-vue/es/menu/MenuItem')['default']
    AMenu: typeof import('ant-design-vue/es/menu/')['default']
    ALayoutHeader: typeof import('ant-design-vue/es/layout/')['default']
    ALayoutContent: typeof import('ant-design-vue/es/layout/')['default']
    ALayoutFooter: typeof import('ant-design-vue/es/layout/')['default']
    ALayout: typeof import('ant-design-vue/es/layout/')['default']
    AButton: typeof import('ant-design-vue/es/button/')['default']
    ADivider: typeof import('ant-design-vue/es/divider/')['default']
    AInput: typeof import('ant-design-vue/es/input/')['default']
    AFormItem: typeof import('ant-design-vue/es/form/FormItem')['default']
    ASpace: typeof import('ant-design-vue/es/space/')['default']
    AForm: typeof import('ant-design-vue/es/form/')['default']
    ACheckbox: typeof import('ant-design-vue/es/checkbox/')['default']
    AListItemMeta: typeof import('ant-design-vue/es/list/Item')['default']
    APopconfirm: typeof import('ant-design-vue/es/popconfirm/')['default']
    AListItem: typeof import('ant-design-vue/es/list/Item')['default']
    AList: typeof import('ant-design-vue/es/list/')['default']
    ATable: typeof import('ant-design-vue/es/table/')['default']
  }
}

Конкретные вопросы заключаются в следующем:

  • layoutкомпоненты ниже,Content,Header,Footer, так как по умолчанию только импортdefaultэкспортируется, если импортируется что-то вроде['default']['Content'], должно быть нормально, теперь проблема в том, что подкомпоненты типа Content разбираются как макеты, в результате чего проблемы со стилями
  • Импорт компонента ListItemMeta не является нормальным, и его можно импортировать только['default'], если можно ввести['default']['Meta']должны быть решены
  • Компонент типографики Внедрение компонента заголовка сообщит об ошибке:TypeError: baseProps is not a functionНо на самом деле компонент вводит этот метод, используя относительный путь

Я надеюсь, что большие ребята могут помочь нам увидеть, как решить вышеуказанные проблемы. Я чувствую, что текущий код немного проблематичен для написания. Если есть более элегантный метод, вы можете связаться со мной.

-------------обновление 20210625------------

Версия 2.2.0-бета.5:Официально поддерживает компоненты vite-plugin.

Небольшие проблемы, требующие исправления, в основном орфографические ошибки в именах.

  • Запись LayoutContent в LayouContent
  • Экспорт PassWord переименован в InputPassoword и другие имена компонентов.

Примечание: После проверки официалку починили, а версию еще не выпустили, ждем релиза beta.6~~~

После изменения исходного кода node_modules обнаруживается, что он нормально загружается. Тогда официальный резолвер vite-plugin-compoennts все еще не прост в использовании, его все равно нужно настраивать, код такой

       const importName = name.slice(1);
       return {
              importName: importName,
              path: `ant-design-vue/es`,
              sideEffects: "ant-design-vue/es/style",
       };

Гораздо удобнее, хвалите скорость отклика и поддержку босса команды antd-vue Тан! ! !

возобновить

---------20210626 обновление -------------

Версия 2.2.0-beta.6: Официально исправлена ​​орфографическая ошибка импорта, и ее можно использовать в обычном режиме в соответствии с описанным выше методом импорта.

предложение: Вышеупомянутый метод стиля стиля является полным импортом. Я не знаю, есть ли способ импорта по запросу. После импорта с использованием распознавателя, предоставленного vite-plugin, стиль отсутствует, но об ошибке не сообщается.

--------обновление 20210627-------------

vite-plugin-components: пр объединен и можно использовать, то есть стиль полностью импортирован

Все еще слишком медленно импортировать стили полностью, загружать много бесполезных стилей и пытаться загружать стили по запросу.

Поскольку antdv официально решил проблему импорта js, остальная часть импорта стилей не кажется такой уж хлопотной, пока вы найдете правильный каталог, Ссылаясь на предыдущий метод записи, таблица конфигурации по-прежнему пишется в соответствии с каталогом компонента.Идея заключается в следующем:

  • Подкомпоненты или компоненты, требующие специальной обработки, сопоставляются в соответствии с обычным шаблоном, и в случае совпадения возвращается сконфигурированный каталог.
  • Если нет подходящего компонента, непосредственно преобразуйте форму kebabCase (форма кебаба)

Интерфейс интерфейса выглядит следующим образом:

interface IMatcher {
  pattern: RegExp;
  styleDir: string;
}

Информация о компоненте, которую необходимо настроить после тестирования, выглядит следующим образом:

const matchComponents: IMatcher[] = [
  {
    pattern: /^Avatar/,
    styleDir: 'avatar',
  },
  {
    pattern: /^AutoComplete/,
    styleDir: 'auto-complete',
  },
  {
    pattern: /^Anchor/,
    styleDir: 'anchor',
  },

  {
    pattern: /^Badge/,
    styleDir: 'badge',
  },
  {
    pattern: /^Breadcrumb/,
    styleDir: 'breadcrumb',
  },
  {
    pattern: /^Button/,
    styleDir: 'button',
  },
  {
    pattern: /^Checkbox/,
    styleDir: 'checkbox',
  },
  {
    pattern: /^Card/,
    styleDir: 'card',
  },
  {
    pattern: /^Collapse/,
    styleDir: 'collapse',
  },
  {
    pattern: /^Descriptions/,
    styleDir: 'descriptions',
  },
  {
    pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
    styleDir: 'date-picker',
  },
  {
    pattern: /^Dropdown/,
    styleDir: 'dropdown',
  },

  {
    pattern: /^Form/,
    styleDir: 'form',
  },
  {
    pattern: /^InputNumber/,
    styleDir: 'input-number',
  },

  {
    pattern: /^Input|^Textarea/,
    styleDir: 'input',
  },
  {
    pattern: /^Statistic/,
    styleDir: 'statistic',
  },
  {
    pattern: /^CheckableTag/,
    styleDir: 'tag',
  },
  {
    pattern: /^Layout/,
    styleDir: 'layout',
  },
  {
    pattern: /^Menu|^SubMenu/,
    styleDir: 'menu',
  },

  {
    pattern: /^Table/,
    styleDir: 'table',
  },
  {
    pattern: /^Radio/,
    styleDir: 'radio',
  },

  {
    pattern: /^Image/,
    styleDir: 'image',
  },

  {
    pattern: /^List/,
    styleDir: 'list',
  },

  {
    pattern: /^Tab/,
    styleDir: 'tabs',
  },
  {
    pattern: /^Mentions/,
    styleDir: 'mentions',
  },

  {
    pattern: /^Mentions/,
    styleDir: 'mentions',
  },

  {
    pattern: /^Step/,
    styleDir: 'steps',
  },
  {
    pattern: /^Skeleton/,
    styleDir: 'skeleton',
  },

  {
    pattern: /^Select/,
    styleDir: 'select',
  },
  {
    pattern: /^TreeSelect/,
    styleDir: 'tree-select',
  },
  {
    pattern: /^Tree|^DirectoryTree/,
    styleDir: 'tree',
  },
  {
    pattern: /^Typography/,
    styleDir: 'typography',
  },
  {
    pattern: /^Timeline/,
    styleDir: 'timeline',
  },
]

Напишите цикл для прохождения, чтобы получить реальный путь стиля

          const importName = name.slice(1);
            let styleDir;
            const total = matchComponents.length;
            for (let i = 0; i < total; i++) {
              const matcher = matchComponents[i];
              if (importName.match(matcher.pattern)) {
                styleDir = matcher.styleDir;
                break;
              }
            }

            if (!styleDir) {
              styleDir = kebabCase(importName);
            }

            return {
              importName: importName,
              path: `ant-design-vue/es`,
              sideEffects: `ant-design-vue/es/${styleDir}/style`,
            };

Укажите пр, надеюсь смогу пройти как можно скорее:PR-ссылка

--------- 20210629 Обновление ------------

vite-plugin-components: antfu выпустил версию 0.11.4, чувствуйте себя комфортно~~~

добавить функцию:Увеличение загрузки CSS также вызвано этой проблемой. Я думал о загрузке index.css раньше, но я не думал, что смогу напрямую загрузить css.js. После загрузки в popconfirm он сообщит об ошибке, если индекс .css не найден, потому что самого стиля нет, все полагаются на стили двух других компонентов.Теперь загрузите css.js напрямую, чтобы решить эту проблему, просто подгрузите соответствующие стили в js по мере необходимости. Код меняется следующим образом:

const getSideEffects: (
  compName: string,
  opts: AntDesignVueResolverOptions
) => string | undefined = (compName, opts) => {
  const { importStyle = true, importCss = true, importLess = false } = opts

  if (importStyle) {
    if (importLess) {
      const styleDir = getStyleDir(compName)
      return `ant-design-vue/es/${styleDir}/style`
    }
    else if (importCss) {
      const styleDir = getStyleDir(compName)
      return `ant-design-vue/es/${styleDir}/style/css`
    }
  }
}

уже пр

Конфигурация использования плагина выглядит следующим образом:

Всего добавляется три параметра, по умолчанию ничего не передается, если параметры передаются, то они передаются в виде объектов. По умолчанию:

  • importStyle: true
  • importCss:true
  • importLess:false
  1. загружать только css
AntDesignVueResolver()
  1. только грузить меньше
AntDesignVueResolver({
    importLess: true
})

Описание: нужно открывать меньше javascriptEnabled: true,

  1. Не загружайте стили компонентов:
AntDesignVueResolver({
    importStyle: false
})

Параметры плагина определяются следующим образом:

export interface AntDesignVueResolverOptions {
  /**
   * import style along with components
   *
   * @default true
   */
  importStyle?: boolean
  /**
   * import css along with components
   *
   * @default true
   */
  importCss?: boolean
  /**
   * import less along with components
   *
   * @default false
   */
  importLess?: boolean
}

Тестирование производительности

Некоторые студенты предположили, что мой алгоритм цикла оптимизации должен оптимизировать временную сложность от O (n) до O (1), используя карту для отображения, Но у меня также есть свои компромиссы при использовании обычного сопоставления, потому что, если вы используете карту для хранения сопоставления компонентов и путей стиля, есть следующие недостатки.

  • Необходимо написать много конфигурации сопоставления, например, ListItem и ListItemMeta необходимо настроить на два
var map = {
ListItem: 'list',
ListItemMeta: 'list'
}

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

var reg = {
pattern: /^List/
dir: 'list'
}
  • Разница в производительности, чтобы досконально понять разницу между отображением и регуляризацией в этом небольшом количестве данных, я специально написал метод проверки производительности для проверки разницы, адрес кода:код sandbox.io/yes/regex pi…

Результат: В основном разница между ними составляет около 0,1 мс, и есть победители и проигравшие. Карта не имеет подавляющего преимущества. Возможно, объем данных небольшой, но может быть разница, когда объем данных велик. , но думаю разница в производительности не большая.В таком случае выбирайте более ремонтопригодное решение

Связанные проблемы и код

Проблема со стилями, загружаемыми по запросу

вопрос antdv о проблеме

vite-plugin-components issue о проблеме

Код:github — адрес проекта todo

разное

В последнее время поддерживаются некоторые библиотеки компонентов vue2 (представление пользовательского интерфейса и пользовательского интерфейса элемента). Эти две статьи резюмируются следующим образом:

Добро пожаловать в один клик и три ссылки~