Урок 05

Плагины и миксины

Плагины Vue

Плагин — объект с методом install. Добавляет глобальные функции, компоненты, директивы.

Простой плагин

plugins/logger.js:

export default {
    install(app, options = {}) {
        const prefix = options.prefix || '[App]';

        app.config.globalProperties.$log = (message) => {
            console.log(`${prefix} ${message}`);
        };
    },
};

main.js:

import logger from './plugins/logger';

app.use(logger, { prefix: '[Skills]' });

Использование в компоненте:

<script setup>
import { getCurrentInstance } from 'vue';

const instance = getCurrentInstance();
instance.appContext.config.globalProperties.$log('Hello');
</script>

В Options API:

export default {
    mounted() {
        this.$log('Hello');
    },
};

Плагин с компонентом

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

export default {
    install(app) {
        app.component('MyButton', Button);
    },
};

Использование:

<MyButton>Клик</MyButton>

Плагин с provide

Лучшая практика:

export default {
    install(app, options) {
        const state = reactive({
            theme: 'dark',
        });

        const toggleTheme = () => {
            state.theme = state.theme === 'dark' ? 'light' : 'dark';
        };

        app.provide('theme', { state, toggleTheme });
    },
};

Потомок:

<script setup>
const { state, toggleTheme } = inject('theme');
</script>

Плагин с директивой

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

TypeScript

plugins/logger.ts:

import type { App } from 'vue';

interface Options {
    prefix?: string;
}

export default {
    install(app: App, options: Options = {}) {
        const prefix = options.prefix || '[App]';
        app.config.globalProperties.$log = (msg: string) => {
            console.log(`${prefix} ${msg}`);
        };
    },
};

Расширение типов:

declare module '@vue/runtime-core' {
    interface ComponentCustomProperties {
        $log: (message: string) => void;
    }
}

Популярные плагины

  • vue-router — роутинг
  • pinia — состояние
  • @vueuse/core — composables (много)
  • vue-i18n — локализация
  • vee-validate — формы
  • floating-vue — tooltip, dropdown

Миксины (Options API)

Миксин — переиспользуемая логика для Options API компонентов.

⚠️ Устарели. Для Vue 3 — composables. Но знать стоит.

mixins/logger.js:

export default {
    data() {
        return { logCount: 0 };
    },
    methods: {
        log(msg) {
            this.logCount++;
            console.log(msg);
        },
    },
    mounted() {
        console.log('Mixin mounted');
    },
};

Компонент:

<script>
import logger from '@/mixins/logger';

export default {
    mixins: [logger],
    mounted() {
        this.log('Component mounted');
    },
};
</script>

Проблемы миксинов

1. Коллизии имён. Если два миксина имеют data.logCount — конфликт.

2. Неявные зависимости. Не видно, откуда метод.

3. Сложно типизировать (TypeScript).

4. Каша при 5+ миксинах.

Composables вместо миксинов

composables/useLogger.js:

import { ref } from 'vue';

export function useLogger() {
    const logCount = ref(0);

    function log(msg) {
        logCount.value++;
        console.log(msg);
    }

    return { logCount, log };
}

Компонент:

<script setup>
import { useLogger } from '@/composables/useLogger';
import { onMounted } from 'vue';

const { logCount, log } = useLogger();

onMounted(() => log('Component mounted'));
</script>

Плюсы:

  • Явные зависимости
  • Нет коллизий
  • Работает с TS
  • Легко тестируется

Правило: вместо миксинов — composables.

Глобальный миксин

⚠️ Не делай так:

app.mixin({
    mounted() {
        console.log('Каждый компонент');
    },
});

Применяется ко всем компонентам. Отладка — ад.

Плагин-пример: уведомления

plugins/notifications.js:

import { reactive } from 'vue';
import Notification from '@/components/Notification.vue';

const state = reactive({
    items: [],
});

export function useNotifications() {
    function notify(message, type = 'info', timeout = 3000) {
        const id = Date.now();
        state.items.push({ id, message, type });

        setTimeout(() => {
            state.items = state.items.filter(n => n.id !== id);
        }, timeout);
    }

    return {
        items: state.items,
        notify,
        success: (msg) => notify(msg, 'success'),
        error: (msg) => notify(msg, 'error'),
    };
}

export default {
    install(app) {
        app.component('NotificationContainer', Notification);
    },
};

Использование:

<script setup>
import { useNotifications } from '@/plugins/notifications';

const { success, error } = useNotifications();

function save() {
    try {
        success('Сохранено');
    } catch {
        error('Ошибка');
    }
}
</script>

Плагин i18n вручную

import { ref, computed } from 'vue';

const locales = {
    ru: { hello: 'Привет' },
    en: { hello: 'Hello' },
};

const current = ref('ru');

export default {
    install(app) {
        app.provide('i18n', {
            locale: current,
            t: (key) => computed(() => locales[current.value]?.[key] ?? key),
            setLocale: (l) => current.value = l,
        });
    },
};

Порядок плагинов

app
    .use(createPinia())
    .use(router)
    .use(i18nPlugin, { locale: 'ru' })
    .use(notificationsPlugin);

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

Итог

  • Плагин — { install(app, options) }
  • app.use(plugin, options) — подключение
  • Внутри install: component, directive, provide, globalProperties
  • Миксины устарели — используй composables
  • TypeScript — расширяй ComponentCustomProperties

Дальше — рендер-функции и JSX.