Kurs Vue.js · Moduł 14: Dyrektywy i pluginy
Plugin Ecosystem i Integracja
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 MyPluginComposable vs Plugin - kiedy co stosować?
| Cecha | Composable | Plugin |
|---|---|---|
| Rejestracja | import w komponencie | app.use() w main.js |
| Zasięg | Lokalny (per komponent) | Globalny (cała aplikacja) |
| Globalne komponenty | Nie | Tak (app.component) |
| Globalne dyrektywy | Nie | Tak (app.directive) |
| Provide/Inject | Możliwe | Naturalne |
| Drzewo zależności | Jawne importy | Niejawne (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. Który z poniższych jest oficjalnym pluginem Vue do wielojęzyczności (i18n)?
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: