Cours Vue.js · Module 14: Directives et plugins

Écosystème de plugins et intégration

4 min de lecture
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 MyPlugin

Composable vs Plugin - lequel choisir ?

CaractéristiqueComposablePlugin
Enregistrementimport dans le composantapp.use() dans main.js
PortéeLocale (par composant)Globale (toute l'application)
Composants globauxNonOui (app.component)
Directives globalesNonOui (app.directive)
Provide/InjectPossibleNaturel
Arbre de dépendancesImports explicitesImplicite (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. 1. Lequel des éléments suivants est le plugin officiel de Vue pour l'internationalisation (i18n) ?

  2. 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 :