Kurs Vue.js · Moduł 14: Dyrektywy i pluginy

Plugin Ecosystem i Integracja

4 min czytania
W tej lekcji5

W NOVA LAB każdy moduł stacji kosmicznej korzysta ze sprawdzonych, przetestowanych systemów - od nawigacji po komunikację. W ekosystemie Vue istnieją popularne pluginy, które rozwiązują typowe problemy. Poznajmy je i nauczmy się tworzyć zaawansowane pluginy.

Popularne pluginy Vue

vue-i18n - internacjonalizacja

vue-i18n to oficjalny plugin do wielojęzyczności w Vue:

1import { createI18n } from 'vue-i18n'
2
3const i18n = createI18n({
4  locale: 'pl',
5  fallbackLocale: 'en',
6  messages: {
7    pl: {
8      station: {
9        name: 'Stacja NOVA LAB',
10        status: 'Status: {status}',
11        crew: 'Załoga: {count} osób'
12      }
13    },
14    en: {
15      station: {
16        name: 'NOVA LAB Station',
17        status: 'Status: {status}',
18        crew: 'Crew: {count} members'
19      }
20    }
21  }
22})
23
24app.use(i18n)

W komponencie:

1<script setup>
2import { useI18n } from 'vue-i18n'
3
4const { t, locale } = useI18n()
5</script>
6
7<template>
8  <h1>{{ t('station.name') }}</h1>
9  <p>{{ t('station.status', { status: 'Operacyjny' }) }}</p>
10  <button @click="locale = 'en'">EN</button>
11  <button @click="locale = 'pl'">PL</button>
12</template>

VeeValidate - walidacja formularzy

VeeValidate to plugin do walidacji formularzy:

1<script setup>
2import { useForm, useField } from 'vee-validate'
3import * as yup from 'yup'
4
5const schema = yup.object({
6  missionName: yup.string().required('Nazwa misji jest wymagana'),
7  crewCount: yup.number().min(1, 'Minimum 1 osoba').required()
8})
9
10const { handleSubmit } = useForm({ validationSchema: schema })
11
12const { value: missionName, errorMessage: nameError } = useField('missionName')
13const { value: crewCount, errorMessage: crewError } = useField('crewCount')
14
15const onSubmit = handleSubmit(values => {
16  console.log('Misja:', values)
17})
18</script>
19
20<template>
21  <form @submit="onSubmit">
22    <input v-model="missionName" placeholder="Nazwa misji" />
23    <span v-if="nameError">{{ nameError }}</span>
24
25    <input v-model="crewCount" type="number" placeholder="Załoga" />
26    <span v-if="crewError">{{ crewError }}</span>
27
28    <button type="submit">Rozpocznij misję</button>
29  </form>
30</template>

vue-toastification - powiadomienia

vue-toastification to gotowy system powiadomień:

1import Toast from 'vue-toastification'
2import 'vue-toastification/dist/index.css'
3
4app.use(Toast, {
5  position: 'top-right',
6  timeout: 3000,
7  closeOnClick: true
8})

Tworzenie pluginu z provide/inject

Wzorzec provide/inject to najlepszy sposób udostępniania danych z pluginu:

1import { ref, readonly } from 'vue'
2
3const NotificationPlugin = {
4  install(app, options = {}) {
5    const notifications = ref([])
6    let nextId = 0
7
8    function notify(message, type = 'info') {
9      const id = nextId++
10      notifications.value.push({ id, message, type })
11      setTimeout(() => {
12        notifications.value = notifications.value.filter(n => n.id !== id)
13      }, options.duration || 4000)
14    }
15
16    app.provide('notifications', {
17      list: readonly(notifications),
18      notify,
19      success: (msg) => notify(msg, 'success'),
20      error: (msg) => notify(msg, 'error'),
21      warning: (msg) => notify(msg, 'warning')
22    })
23  }
24}

Kluczowy element - readonly() zapobiega przypadkowej modyfikacji listy powiadomień z zewnątrz pluginu.

Plugin Options Pattern

Dobry plugin akceptuje konfigurację z rozsądnymi domyślnymi wartościami:

1const ThemePlugin = {
2  install(app, options = {}) {
3    const defaults = {
4      primaryColor: '#00ff88',
5      darkMode: true,
6      fontFamily: 'monospace',
7      borderRadius: '8px'
8    }
9
10    const theme = ref({ ...defaults, ...options })
11
12    function setTheme(newOptions) {
13      theme.value = { ...theme.value, ...newOptions }
14    }
15
16    app.provide('theme', {
17      current: readonly(theme),
18      setTheme
19    })
20
21    // Globalna dyrektywa wykorzystująca theme
22    app.directive('themed', (el, binding) => {
23      const t = theme.value
24      el.style.color = t.primaryColor
25      el.style.fontFamily = t.fontFamily
26      if (binding.modifiers.bordered) {
27        el.style.border = '1px solid ' + t.primaryColor
28        el.style.borderRadius = t.borderRadius
29      }
30    })
31  }
32}

Instalacja:

1app.use(ThemePlugin, {
2  primaryColor: '#00b4d8',
3  darkMode: true
4})

Auto-Install Pattern

Pluginy przeznaczone do dystrybucji (np. jako pakiet npm) powinny wspierać auto-install:

1const MyPlugin = {
2  install(app, options) {
3    // logika pluginu
4  }
5}
6
7// Auto-install gdy Vue jest dostępne globalnie (CDN)
8if (typeof window !== 'undefined' && window.Vue) {
9  window.Vue.use(MyPlugin)
10}
11
12export default MyPlugin

Composable vs Plugin - kiedy co stosować?

CechaComposablePlugin
Rejestracjaimport w komponencieapp.use() w main.js
ZasięgLokalny (per komponent)Globalny (cała aplikacja)
Globalne komponentyNieTak (app.component)
Globalne dyrektywyNieTak (app.directive)
Provide/InjectMożliweNaturalne
Drzewo zależnościJawne importyNiejawne (inject)

Zasada: Jeżeli funkcjonalność jest potrzebna w wielu miejscach i wymaga globalnej konfiguracji (np. theme, i18n, toast) - użyj pluginu. Jeżeli to logika specyficzna dla kilku komponentów - composable.

Kod do tej lekcji: App.vue
1<script setup>
2import { inject, ref, readonly } from 'vue'
3
4const theme = inject('theme')
5const notifications = inject('notifications')
6const { t, locale, setLocale } = inject('i18n')
7
8function testNotification(type) {
9  if (type === 'success') {
10    notifications.success(t('notifications.saved'))
11  } else if (type === 'error') {
12    notifications.error(t('notifications.connectionError'))
13  } else {
14    notifications.notify(t('notifications.scanStarted'))
15  }
16}
17</script>
18
19<template>
20  <div class="nova-lab">
21    <h1>Plugin Ecosystem i Integracja</h1>
22
23    <section class="section">
24      <h2>Theme Plugin (provide/inject + readonly)</h2>
25      <div class="theme-info">
26        <p>Primary: <span :style="{ color: theme.current.value.primaryColor }">{{ theme.current.value.primaryColor }}</span></p>
27        <p>Dark Mode: {{ theme.current.value.darkMode ? 'TAK' : 'NIE' }}</p>
28        <p>Font: {{ theme.current.value.fontFamily }}</p>
29      </div>
30      <div class="btn-row">
31        <button @click="theme.setTheme({ primaryColor: '#00ff88' })" class="btn">Zielony</button>
32        <button @click="theme.setTheme({ primaryColor: '#00b4d8' })" class="btn cyan">Cyan</button>
33        <button @click="theme.setTheme({ primaryColor: '#e74c3c' })" class="btn red">Czerwony</button>
34      </div>
35    </section>
36
37    <section class="section">
38      <h2>i18n Plugin</h2>
39      <p>{{ t('station.name') }}</p>
40      <p>{{ t('station.status') }}</p>
41      <div class="btn-row">
42        <button @click="setLocale('pl')" :class="['btn', { active: locale.value === 'pl' }]">PL</button>
43        <button @click="setLocale('en')" :class="['btn', { active: locale.value === 'en' }]">EN</button>
44      </div>
45    </section>
46
47    <section class="section">
48      <h2>Notification Plugin</h2>
49      <div class="btn-row">
50        <button @click="testNotification('success')" class="btn success">Sukces</button>
51        <button @click="testNotification('error')" class="btn error">Blad</button>
52        <button @click="testNotification('info')" class="btn info">Info</button>
53      </div>
54    </section>
55
56    <div class="notif-container">
57      <div
58        v-for="n in notifications.list.value"
59        :key="n.id"
60        :class="['notif', n.type]"
61      >{{ n.message }}</div>
62    </div>
63  </div>
64</template>
65
66<style scoped>
67.nova-lab {
68  background: #0a0e27;
69  color: #c0c0c0;
70  min-height: 100vh;
71  padding: 20px;
72  font-family: monospace;
73}
74h1 { color: #00ff88; margin-bottom: 20px; }
75h2 { color: #00b4d8; font-size: 16px; margin-bottom: 10px; }
76.section {
77  background: rgba(0,180,216,0.05);
78  border: 1px solid rgba(0,180,216,0.2);
79  padding: 15px;
80  border-radius: 8px;
81  margin-bottom: 15px;
82}
83.theme-info { margin-bottom: 10px; }
84.theme-info p { margin: 4px 0; font-size: 13px; }
85.btn-row { display: flex; gap: 8px; }
86.btn {
87  background: rgba(0,255,136,0.15);
88  color: #00ff88;
89  border: 1px solid #00ff88;
90  padding: 6px 14px;
91  border-radius: 6px;
92  cursor: pointer;
93  font-family: monospace;
94}
95.btn.active { background: #00ff88; color: #0a0e27; }
96.btn.cyan { color: #00b4d8; border-color: #00b4d8; }
97.btn.red { color: #e74c3c; border-color: #e74c3c; }
98.btn.success { color: #2ecc71; border-color: #2ecc71; }
99.btn.error { color: #e74c3c; border-color: #e74c3c; }
100.btn.info { color: #00b4d8; border-color: #00b4d8; }
101.notif-container {
102  position: fixed; top: 20px; right: 20px;
103  display: flex; flex-direction: column; gap: 8px; z-index: 1000;
104}
105.notif {
106  padding: 10px 18px; border-radius: 8px;
107  font-family: monospace; font-size: 13px;
108  animation: slideIn 0.3s ease;
109}
110.notif.success { background: rgba(46,204,113,0.9); color: white; }
111.notif.error { background: rgba(231,76,60,0.9); color: white; }
112.notif.info { background: rgba(0,180,216,0.9); color: white; }
113@keyframes slideIn {
114  from { transform: translateX(100%); opacity: 0; }
115  to { transform: translateX(0); opacity: 1; }
116}
117</style>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Który z poniższych jest oficjalnym pluginem Vue do wielojęzyczności (i18n)?

  2. 2. Po co używać readonly() przy udostępnianiu danych z pluginu?

To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Zaimplementuj ThemePlugin z domyślnymi opcjami, metodą setTheme i globalną dyrektywą v-themed.

  • Układanie w poziomie

    Ułóż poprawne przekazanie readonly ref przez provide:

  • Klikanie w kolejności

    Ułóż poprawną instalację ThemePlugin z opcjami:

  • Układanie w pionie

    Ułóż kroki implementacji Plugin Options Pattern:

  • Układanie w poziomie

    Ułóż poprawne łączenie domyślnych opcji z przekazanymi:

  • Układanie w pionie

    Ułóż kroki wzorca auto-install pluginu:

  • Klikanie w kolejności

    Ułóż poprawną rejestrację globalnej dyrektywy w pluginie:

  • Edytor kodu

    Zaimplementuj plugin powiadomień z reactive array, setTimeout do auto-remove i provide/inject.

  • Układanie w pionie

    Ułóż warstwy architektury aplikacji Vue z pluginami od najniższej do najwyższej:

  • Układanie w poziomie

    Ułóż użycie dyrektywy v-themed z modyfikatorem bordered:

  • Klikanie w kolejności

    Ułóż poprawną rejestrację globalnego komponentu w pluginie:

Przydatne artykuły