Cours Vue.js · Module 14: Directives et plugins
Écosystème de plugins et intégration
Dans cette leçon5
Dans NOVA LAB, chaque module de la station spatiale repose sur des systèmes éprouvés et testés - de la navigation à la communication. Dans l'écosystème Vue, il existe des plugins populaires qui résolvent des problèmes courants. Explorons-les et apprenons à créer des plugins avancés.
Plugins Vue populaires
vue-i18n - internationalisation
vue-i18n est le plugin officiel de Vue pour le support multilingue :
1import { createI18n } from 'vue-i18n'
2
3const i18n = createI18n({
4 locale: 'fr',
5 fallbackLocale: 'en',
6 messages: {
7 fr: {
8 station: {
9 name: 'Station NOVA LAB',
10 status: 'Status: {status}',
11 crew: 'Équipage : {count} personnes'
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)Dans un composant :
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: 'Operational' }) }}</p>
10 <button @click="locale = 'en'">EN</button>
11 <button @click="locale = 'fr'">FR</button>
12</template>VeeValidate - validation de formulaires
VeeValidate est un plugin de validation de formulaires :
1<script setup>
2import { useForm, useField } from 'vee-validate'
3import * as yup from 'yup'
4
5const schema = yup.object({
6 missionName: yup.string().required('Le nom de la mission est requis'),
7 crewCount: yup.number().min(1, 'Minimum 1 personne').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('Mission:', values)
17})
18</script>
19
20<template>
21 <form @submit="onSubmit">
22 <input v-model="missionName" placeholder="Nom de la mission" />
23 <span v-if="nameError">{{ nameError }}</span>
24
25 <input v-model="crewCount" type="number" placeholder="Équipage" />
26 <span v-if="crewError">{{ crewError }}</span>
27
28 <button type="submit">Démarrer la mission</button>
29 </form>
30</template>vue-toastification - notifications
vue-toastification est un système de notifications prêt à l'emploi :
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})Créer un plugin avec provide/inject
Le pattern provide/inject est la meilleure façon de partager des données depuis un plugin :
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}L'élément clé - readonly() empêche toute modification accidentelle de la liste de notifications depuis l'extérieur du plugin.
Pattern des options de plugin
Un bon plugin accepte une configuration avec des valeurs par défaut sensées :
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 // Directive globale utilisant le thème
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}Installation :
1app.use(ThemePlugin, {
2 primaryColor: '#00b4d8',
3 darkMode: true
4})Pattern auto-install
Les plugins destinés à la distribution (par ex. sous forme de package npm) devraient prendre en charge l'auto-installation :
1const MyPlugin = {
2 install(app, options) {
3 // logique du plugin
4 }
5}
6
7// Auto-installation quand Vue est disponible globalement (CDN)
8if (typeof window !== 'undefined' && window.Vue) {
9 window.Vue.use(MyPlugin)
10}
11
12export default MyPluginComposable vs Plugin - lequel choisir ?
| Caractéristique | Composable | Plugin |
|---|---|---|
| Enregistrement | import dans le composant | app.use() dans main.js |
| Portée | Locale (par composant) | Globale (toute l'application) |
| Composants globaux | Non | Oui (app.component) |
| Directives globales | Non | Oui (app.directive) |
| Provide/Inject | Possible | Naturel |
| Arbre de dépendances | Imports explicites | Implicite (inject) |
Règle générale : si une fonctionnalité est nécessaire à de nombreux endroits et requiert une configuration globale (par ex. thème, i18n, toast) - utilise un plugin. Si c'est une logique spécifique à quelques composants - utilise un composable.
Code de cette leçon : 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>Écosystème de plugins et intégration</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 ? 'OUI' : 'NON' }}</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">Vert</button>
32 <button @click="theme.setTheme({ primaryColor: '#00b4d8' })" class="btn cyan">Cyan</button>
33 <button @click="theme.setTheme({ primaryColor: '#e74c3c' })" class="btn red">Rouge</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('fr')" :class="['btn', { active: locale.value === 'fr' }]">FR</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">Success</button>
51 <button @click="testNotification('error')" class="btn error">Error</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>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Lequel des éléments suivants est le plugin officiel de Vue pour l'internationalisation (i18n) ?
2. Pourquoi utiliser readonly() lorsqu'on expose des données depuis un plugin ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Implémente un ThemePlugin avec des options par défaut, une méthode setTheme et une directive globale v-themed.
- Tri horizontal
Ordonne la façon correcte de passer un ref readonly via provide :
- Clique dans l'ordre
Ordonne l'installation correcte de ThemePlugin avec des options :
- Tri vertical
Ordonne les étapes d'implémentation du Plugin Options Pattern :
- Tri horizontal
Ordonne la fusion correcte des options par défaut avec celles fournies :
- Tri vertical
Ordonne les étapes du pattern auto-install d'un plugin :
- Clique dans l'ordre
Ordonne l'enregistrement correct d'une directive globale dans un plugin :
- Éditeur de code
Implémente un plugin de notifications avec un tableau réactif, un setTimeout pour l'auto-suppression et provide/inject.
- Tri vertical
Ordonne les couches d'une architecture d'application Vue avec plugins, de la plus basse à la plus haute :
- Tri horizontal
Ordonne l'utilisation de la directive v-themed avec le modificateur bordered :
- Clique dans l'ordre
Ordonne l'enregistrement correct d'un composant global dans un plugin :