Плагины и 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.
Содержание
- Что такое Vue-плагин
- Создание плагина: install и app.use
- Глобальные сущности: component, directive, provide
- app.config
- app.mount и app.unmount
- Несколько приложений на странице
- Плагин, composable или provide/inject
- Типизация app-level API
- Лучшие практики и антипаттерны
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое 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
Три механизма перекрываются, но решают разные задачи. Выбор между ними — частый вопрос на интервью.
| Критерий | Плагин | Composable | provide/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 и доступные компоненты.