Навигация

Плагины и app-level API во Vue

Экземпляр приложения (app) — это точка входа, через которую Vue-приложение настраивается до первого рендера. Здесь регистрируют глобальные компоненты и директивы, подключают роутер и Pinia, настраивают обработку ошибок, предоставляют сервисы всему дереву компонентов. Понимание app-level API объясняет, почему app.use(router) работает именно так и где вообще уместно писать «глобальный» код.

Плагины — основной способ упаковать такие расширения: один раз при старте выполнить инициализацию и отдать результат всему приложению. В этой статье разберём функцию install и app.use, глобальные сущности app.component/app.directive/app.provide, настройки app.config, жизненный цикл app.mount/app.unmount, несколько приложений на странице, а также сравним плагин с composable и provide/inject.

Содержание

  1. Что такое Vue-плагин
  2. Создание плагина: install и app.use
  3. Глобальные сущности: component, directive, provide
  4. app.config
  5. app.mount и app.unmount
  6. Несколько приложений на странице
  7. Плагин, composable или provide/inject
  8. Типизация app-level API
  9. Лучшие практики и антипаттерны
  10. Ключевые тезисы для интервью
  11. Заключение
  12. Полезные ссылки

Что такое Vue-плагин

Плагин — это самодостаточное расширение, которое устанавливается один раз на уровне приложения и глобально меняет его поведение. Типичные задачи плагинов:

  • регистрация глобальных компонентов или директив (дизайн-система, v-tooltip);
  • инициализация и настройка сторонних библиотек (роутер, сторы, i18n, аналитика, Sentry);
  • настройка HTTP-клиента или SDK, используемых во всех компонентах;
  • добавление глобальных свойств и сервисов ($http, $toast);
  • централизованная настройка обработки ошибок и предупреждений.

Ключевое отличие от компонентов и composables: плагин не привязан к конкретному компоненту и обычно имеет побочные эффекты при старте. Компонент рендерит разметку в своём месте, composable возвращает реактивную логику в компонент, а плагин один раз настраивает всё приложение целиком.

Пример: вместо того чтобы в каждом компоненте создавать свой экземпляр HTTP-клиента, плагин создаёт его один раз, добавляет интерсепторы и предоставляет компонентам через app.provide или app.config.globalProperties.


Создание плагина: install и app.use

Плагин — это объект с методом install либо просто функция. Vue вызывает install в момент установки:

const myPlugin = {
  install(app, options) {
    // app — экземпляр приложения
    // options — второй и последующие аргументы app.use
  },
};

export default myPlugin;

Функциональная форма — сокращённый вариант того же:

const myPlugin = (app, options) => {
  // тело install
};

Установка выполняется через app.use:

import { createApp } from 'vue';
import App from './App.vue';
import myPlugin from './plugins/myPlugin';

const app = createApp(App);

app.use(myPlugin, { prefix: 'api' }); // options попадут в install
app.mount('#app');

Важные свойства app.use:

  • Цепочка вызовов. Метод возвращает сам app, поэтому вызовы можно объединять: app.use(router).use(pinia).use(i18n).mount('#app').
  • Защита от повторной установки. Vue запоминает установленные плагины и не вызывает install дважды для одного и того же объекта плагина.
  • Порядок. Плагины должны быть установлены до app.mount. После монтирования app.use не окажет влияния на уже созданное дерево, поэтому Vue выдаёт предупреждение.
  • Дополнительные аргументы. Всё, что передано после плагина, приходит в install как второй и последующие параметры.

Практический пример плагина, который создаёт сервис уведомлений и предоставляет его приложению:

// plugins/toast.ts
import type { App } from 'vue';
import { reactive } from 'vue';

interface ToastOptions {
  duration?: number;
}

const TOAST_KEY = Symbol('toast');

export interface ToastService {
  items: { id: number; text: string }[];
  show(text: string): void;
  clear(): void;
}

export const toastPlugin = {
  install(app: App, options: ToastOptions = {}) {
    const duration = options.duration ?? 3000;
    let id = 0;

    const service: ToastService = {
      items: reactive([]),
      show(text) {
        const item = { id: id++, text };
        this.items.push(item);
        setTimeout(() => this.clear(), duration);
      },
      clear() {
        service.items.length = 0;
      },
    };

    // предоставляем сервис всему дереву компонентов
    app.provide(TOAST_KEY, service);
    // и как глобальное свойство для шаблонов и Options API
    app.config.globalProperties.$toast = service;
  },
};
import { createApp } from 'vue';
import App from './App.vue';
import { toastPlugin } from './plugins/toast';

const app = createApp(App);

app.use(toastPlugin, { duration: 5000 });
app.mount('#app');

Внутри компонента сервис получают через inject(TOAST_KEY) (см. раздел «Плагин, composable или provide/inject»), а в шаблоне можно использовать $toast.


Глобальные сущности: component, directive, provide

Экземпляр приложения даёт несколько методов для регистрации глобальных сущностей.

app.component

Регистрирует компонент, доступный в шаблонах всего приложения:

import BaseButton from './components/BaseButton.vue';

app.component('BaseButton', BaseButton);

После этого <BaseButton /> доступен без импорта в любом SFC. Глобальная регистрация удобна для базовых компонентов дизайн-системы, но у неё есть цена: компонент не tree-shakable, его сложнее найти по коду, а имя становится «зарезервированным». Для прикладных компонентов предпочтительнее обычный импорт.

app.directive

Регистрирует глобальную директиву:

app.directive('focus', {
  mounted(el) {
    el.focus();
  },
});

Механика директив детально разобрана в статье Кастомные директивы во Vue — здесь важно, что плагин это естественное место для регистрации директив, которые нужны всему приложению.

app.provide

Кладёт значение в корень дерева компонентов. Любой потомок получит его через inject:

app.provide('appName', 'Prepra');
<script setup>
import { inject } from 'vue';

const appName = inject('appName');
</script>

Разница между тремя методами — в том, что именно становится глобальным:

МетодЧто регистрируетКак используется
app.componentКомпонент<Comp /> в шаблонах
app.directiveДирективаv-xxx на элементах
app.provideЗначение уровня приложенияinject(key) в компонентах

app.provide — самый «мягкий» способ: он не занимает глобальное имя в шаблонах, не конфликтует с разметкой и хорошо типизируется через InjectionKey. Именно его стоит предпочитать для предоставления сервисов.


app.config

app.config — объект настроек приложения. Большая часть опций нужна в специфичных сценариях.

errorHandler

Глобальный обработчик ошибок рендера, хуков и вотчеров. Обычно используется для логирования в production:

app.config.errorHandler = (err, instance, info) => {
  reportError(err, { component: instance?.$options?.name, info });
};

Детально errorHandler, всплытие ошибок и error boundaries разобраны в статье Обработка ошибок.

warnHandler

Перехватывает предупреждения Vue. Работает только в development, в production игнорируется:

app.config.warnHandler = (msg, instance, trace) => {
  console.warn('[Vue warn]', msg, trace);
};

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

performance

Включает трассировку производительности компонентов (тоже только в development):

app.config.performance = true;

Включает замеры init, render, patch и делает их видимыми в Performance-панели DevTools.

globalProperties

Добавляет свойства, доступные во всех компонентах через this (Options API и шаблоны):

app.config.globalProperties.$appName = 'Prepra';
<script setup>
import { getCurrentInstance } from 'vue';
</script>

<template>
  <p>{{ $appName }}</p>
</template>

Стоит быть осторожным: globalProperties — это глобальное мутабельное пространство. Для сервисов и зависимостей предпочтительнее app.provide/composables, а globalProperties оставлять для мелочей и совместимости. Также помните, что в <script setup> прямой доступ к $appName не работает — нужен getCurrentInstance().

optionMergeStrategies

Задаёт, как сливаются одноимённые опции при использовании миксинов и наследования в Options API:

app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
  return toVal ? [...toVal, ...fromVal] : fromVal;
};

Нужна редко — в основном библиотекам и легаси-коду на Options API.

compilerOptions

Настройки компилятора шаблонов. Доступны, только если приложение использует runtime-компилятор (например, сборка без предкомпиляции шаблонов), и позволяют помечать нестандартные теги:

app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('my-');

В типичном Vite-проекте шаблоны компилируются заранее, поэтому настраивать это через app.config бессмысленно — при необходимости параметры задают в конфиге сборщика (@vitejs/plugin-vue). Подробнее о сборке — в статье Webpack, Vite и CI/CD для фронтендера.

Прочие опции

В актуальных версиях Vue 3 добавлены idPrefix (префикс для SSR-safe id) и throwUnhandledErrorInProduction (поведение необработанных ошибок в production). Их назначение узкое, но полезно знать, что app.config продолжает расширяться.


app.mount и app.unmount

app.mount

Монтирует приложение в контейнер и возвращает корневой экземпляр компонента:

const root = app.mount('#app');
console.log(root.$el); // корневой DOM-элемент

Контейнером может быть CSS-селектор (строка) или реальный DOM-элемент:

app.mount(document.getElementById('app')!);

При монтировании Vue заменяет содержимое контейнера созданным деревом. Если контейнер содержал разметку (например, спиннер «Загрузка…»), она будет удалена — поэтому её часто переносят в <Suspense> или в шаблон приложения.

app.unmount

Демонтирует приложение: Vue запускает beforeUnmount/unmounted у всех компонентов, удаляет эффекты и слушатели, снимает приложение с контейнера:

app.unmount();

unmount нужен в сценариях, где приложение существует ограниченное время: микрофронтенды, виджеты, встраиваемые на чужую страницу, тесты. Важно понимать, что происходит при демонтировании:

  • срабатывают хуки очистки всех компонентов;
  • Vue снимает свои слушатели и реактивные эффекты;
  • внешние слушатели на window/document, созданные вручную (или плагином), Vue не уберёт — их надо снимать самостоятельно.

Несколько приложений на странице

Vue позволяет создать сколько угодно независимых приложений — каждое через свой createApp:

const header = createApp(HeaderWidget);
header.mount('#header');

const sidebar = createApp(SidebarWidget);
sidebar.mount('#sidebar');

Это базовый приём для виджетов и микрофронтендов: каждое приложение изолировано, имеет собственный корневой компонент и собственный набор глобальных сущностей. Компонент, зарегистрированный в header, недоступен в sidebar, а app.provide и app.config у них независимы.

Практические следствия:

  • Один и тот же компонент нельзя отрендерить в двух приложениях одновременно — экземпляр компонента принадлежит конкретному дереву. Нужны две независимые копии.
  • Общее состояние между приложениями организуется вне Vue: глобальная шина событий, window-свойства, отдельный модуль-стор. Vue-плагины и provide на уровень выше приложения не работают.
  • Каждому приложению — свои плагины. Если роутер нужен в одном приложении и не нужен в другом, устанавливайте его точечно.

Такая архитектура — основа встраивания Vue-виджетов в сторонние страницы и микрофронтендов.


Плагин, composable или provide/inject

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

КритерийПлагинComposableprovide/inject
ОбластьВсё приложениеКомпонент и его вызовыДерево от точки provide вниз
Когда выполняетсяОдин раз при установкеПри каждом вызове в setupПри рендере провайдера/потребителя
Побочные эффектыДа, при стартеВозможно, в компонентеНет
Регистрирует глобальные сущностиДаНетНет
Реактивное состояниеКосвенноДаДа (если передан ref)
ТипизацияРучная/через модулиЕстественнаяInjectionKey<T>

Практические ориентиры:

  • Плагин — когда есть глобальная инициализация и/или регистрация: подключить роутер, настроить i18n, проинициализировать SDK, зарегистрировать компоненты дизайн-системы. Плагин устанавливается один раз и обычно имеет побочные эффекты.
  • Composable — когда нужна переиспользуемая реактивная логика внутри компонентов: useFetch, useToggle, useMouse. Никакой глобальной инициализации, только возврат состояния и функций.
  • provide/inject — когда нужно передать зависимость по дереву без prop drilling, не занимая глобальное имя. app.provide — это provide на уровне корня приложения.

Механизмы часто комбинируют: плагин создаёт и настраивает сервис и отдаёт его через app.provide, а публичный composable его инжектит и добавляет удобства:

// composables/useToast.ts
import { inject } from 'vue';
import { TOAST_KEY } from '../plugins/toast';

export function useToast() {
  const toast = inject(TOAST_KEY);
  if (!toast) {
    throw new Error('toastPlugin не установлен: app.use(toastPlugin)');
  }
  return toast;
}
<script setup>
import { useToast } from '@/composables/useToast';

const toast = useToast();
</script>

<template>
  <button @click="toast.show('Готово!')">Показать</button>
</template>

Такой паттерн даёт лучшее из двух миров: сервис создаётся один раз (плагин), но доступ к нему — через понятный типизированный composable, который легко тестировать и мокать.


Типизация app-level API

Глобальные свойства и ключи provide/inject выглядят «нетипизированными», если не расширить типы Vue.

globalProperties

Чтобы TypeScript знал о $toast из плагина, расширьте ComponentCustomProperties:

// types/vue.d.ts
import type { ToastService } from '@/plugins/toast';

declare module 'vue' {
  interface ComponentCustomProperties {
    $toast: ToastService;
  }
}

export {};

После этого this.$toast и $toast в шаблонах типизированы. Важно: файл должен быть модулем — наличие import/export обязательно, иначе declare module перезапишет типы Vue.

InjectionKey

Ключи provide/inject лучше делать через InjectionKey<T> и Symbol, а не строки: это исключает коллизии имён и даёт полную типизацию значения:

import type { InjectionKey } from 'vue';
import type { ToastService } from './toast';

export const TOAST_KEY: InjectionKey<ToastService> = Symbol('toast');

Теперь inject(TOAST_KEY) возвращает ToastService | undefined, а TypeScript подсказывает поля сервиса. В Nuxt аналогичная типизация глобальных значений плагинов ($name) делается через расширение NuxtApp, о чём — в статье Жизненный цикл приложения в Nuxt 3.


Лучшие практики и антипаттерны

Делайте

  • Устанавливайте плагины до app.mount. После монтирования глобальные регистрации не подействуют на уже созданное дерево.
  • Предпочитайте app.provide + composable, а не globalProperties. Сервис с типизированным геттером (useToast) удобнее и безопаснее глобального this.$toast.
  • Делайте плагины идемпотентными. Vue не вызовет install дважды для одного объекта, но при ручной передаче разных экземпляров плагина защита не сработает — позаботьтесь об этом сами.
  • Проверяйте SSR. В универсальном коде обращайтесь к window/document только внутри проверки import.meta.client или в клиентских хуках.
  • Снимайте внешние ресурсы в unmount. Если плагин регистрирует глобальные слушатели или таймеры, предусмотрите очистку.
  • Типизируйте глобальные свойства и ключи. Расширяйте ComponentCustomProperties и используйте InjectionKey.

Не делайте

  • Не злоупотребляйте globalProperties. Раздутое глобальное пространство конфликтует с именами и плохо отслеживается.
// ❌ Свалить всё в глобальные свойства
app.config.globalProperties.$api = api;
app.config.globalProperties.$toast = toast;
app.config.globalProperties.$auth = auth;
app.config.globalProperties.$logger = logger;
  • Не выполняйте побочные эффекты с DOM на сервере. Обращение к document в install упадёт при SSR:
// ❌ Падает на сервере
const plugin = {
  install() {
    document.body.classList.add('app-ready');
  },
};
  • Не регистрируйте глобально то, что нужно одному компоненту. Глобальные компоненты и директивы оправданы для инфраструктуры, а не для прикладного кода.
  • Не устанавливайте плагин после mount. Регистрация окажется частичной и предсказуемо сломается.
  • Не дублируйте функциональность плагина и composable. Если логика реактивна и нужна в компонентах — это composable; если это глобальная инициализация — плагин.
  • Не забывайте про несколько приложений. Глобальные сущности одного app не видны в другом — не рассчитывайте на общий provide.

Ключевые тезисы для интервью

  • Vue-плагин — объект с install(app, options) или функция; он один раз при старте выполняет глобальную инициализацию и часто имеет побочные эффекты.
  • app.use вызывает install, возвращает app для цепочки, не устанавливает один плагин дважды и должен вызываться до app.mount.
  • app.component и app.directive регистрируют глобальные сущности по имени, а app.provide кладёт значение в корень дерева и даёт внедрение через inject без глобального имени — предпочтительный способ отдавать сервисы.
  • app.config настраивает errorHandler, warnHandler, globalProperties, optionMergeStrategies и compilerOptions; warnHandler и performance работают только в development.
  • app.mount заменяет содержимое контейнера и возвращает корневой экземпляр, app.unmount запускает очистку компонентов, но внешние слушатели и таймеры нужно снимать самому.
  • Несколько приложений создают отдельными createApp с независимыми глобальными сущностями; плагин выбирают для глобальной инициализации, composable — для логики компонентов, provide/inject — для передачи зависимости по дереву.
  • Глобальные свойства типизируют через ComponentCustomProperties, а ключи provide/inject — через InjectionKey<T>; типичные ошибки — установка плагина после mount, раздутый globalProperties и обращение к DOM при SSR.

Заключение

App-level API — это слой настройки приложения: плагины, глобальные сущности, app.config и управление жизненным циклом через mount/unmount. Плагин здесь — основная единица расширения: он один раз выполняет инициализацию и делает результат доступным всему приложению.

Ключевое правило выбора: глобальная инициализация и регистрация — плагин, реактивная логика в компонентах — composable, передача зависимости по дереву — provide/inject. На практике их объединяют: плагин создаёт и настраивает сервис, отдаёт его через app.provide, а компоненты получают сервис через типизированный composable.

Попробуйте написать собственный плагин — например, toast или HTTP-клиент с интерсепторами, — отдайте его через app.provide и оберните в useXxx composable. Так вы на практике увидите, где заканчивается зона ответственности app-level API и начинается обычная логика компонента. В следующих статьях раздела мы разберём стилизацию во Vue и доступные компоненты.

Полезные ссылки