Vue 3-plugin maken: install, provide, directives en tests

Vue 3-plugin maken: install, provide, directives en tests
Kort antwoord

Een Vue 3-plugin is een object met een methode install(app, options), dat je installeert met app.use(). Hij registreert componenten en directives, levert waarden via app.provide() en configureert de applicatie. Vue 2-code op basis van Vue.prototype en Vue.mixin werkt niet meer.

Een Vue-plugin voegt functionaliteit toe aan een hele applicatie: een component dat overal beschikbaar is, een directive, een injecteerbare waarde, gedeelde configuratie. Het is het mechanisme achter Vue Router, Pinia en de meeste libraries uit het ecosysteem.

Wat een plugin is

Een object met een methode install, of gewoon een functie. Vue roept hem aan met de applicatie-instantie en de opties die je aan app.use() meegeeft.

src/plugins/monitor.js
export default {
  install(app, options) {
    // hier gebeurt alles
  }
}
Vue 2-code werkt niet meer

In Vue 2 kreeg install de globale constructor Vue binnen, en sleutelde een plugin aan Vue.prototype, Vue.mixin of Vue.component. In Vue 3 bestaat die globale constructor niet meer: install krijgt de instantie die createApp heeft gemaakt, en alles loopt via die instantie. Een Vue 2-plugin die je klakkeloos overneemt, sneuvelt met Cannot set properties of undefined (setting '$api') of doet stilletjes niets.

De vertaaltabel:

javascript
// Vue 3
Vue.use(Plugin)                   app.use(Plugin)
Vue.component('X', X)             app.component('X', X)
Vue.directive('x', x)             app.directive('x', x)
Vue.mixin({ … })                  app.mixin({ … })
Vue.prototype.$api = api          app.config.globalProperties.$api = api
                                  app.provide('api', api)   // beter

Een volledige plugin

Hieronder een plugin die het mounten van componenten logt, dat logboek beschikbaar stelt en een directive levert. Hij laat de vier aanhaakpunten zien die je moet kennen.

src/plugins/monitor.js
export default {
  install(app, options = {}) {
    const prefixe = options.prefixe ?? '[monitor]'
    const journal = []

    // 1. globale property, bereikbaar via this in de Options API
    app.config.globalProperties.$journal = journal

    // 2. injection: de aanbevolen weg met <script setup>
    app.provide('journal', journal)

    // 3. globale directive
    app.directive('surligne', {
      mounted(el, binding) {
        el.style.backgroundColor = binding.value ?? '#fef08a'
      }
    })

    // 4. globale mixin, draait bij het mounten van elk component
    app.mixin({
      mounted() {
        journal.push(`${prefixe} ${this.$options.__name ?? 'anonyme'} monté`)
      }
    })
  }
}

Installeren, met of zonder opties:

src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import monitor from './plugins/monitor.js'

const app = createApp(App)

app.use(monitor, { prefixe: '[gekkode]' })
app.mount('#app')

Een plugin die twee keer geïnstalleerd wordt, wordt maar één keer toegepast: Vue houdt bij welke plugins al door app.use() zijn gegaan.

provide in plaats van globalProperties

app.config.globalProperties.$journal bootst de Vue 2-gewoonte van this.$ietsWat na. Dat werkt, maar this bestaat niet in <script setup>: de property is alleen bereikbaar vanuit het template of de Options API. Ze ontsnapt bovendien aan autocompletion en typing.

app.provide() is de juiste weg. Het component haalt de waarde op met inject:

src/components/Journal.vue
<script setup>
import { inject } from 'vue'

const journal = inject('journal')
</script>

<template>
  <p>{{ journal.length }} composants montés</p>
</template>

Gebruik een Symbol dat de plugin exporteert om botsingen tussen stringnamen te voorkomen:

src/plugins/monitor.js
export const cleJournal = Symbol('journal')

export default {
  install(app) {
    app.provide(cleJournal, [])
  }
}

Componenten distribueren

Het meest voorkomende geval: een bibliotheek componenten beschikbaar maken zonder import.

src/plugins/ui.js
import GkBouton from '../components/GkBouton.vue'
import GkCarte from '../components/GkCarte.vue'

export default {
  install(app, { prefixe = 'Gk' } = {}) {
    app.component(`${prefixe}Bouton`, GkBouton)
    app.component(`${prefixe}Carte`, GkCarte)
  }
}

Let op de afruil: een globaal geregistreerd component is overal beschikbaar, maar ontsnapt aan tree shaking. Het belandt in de bundle, ook als niemand het gebruikt. Houd globale registratie dus voor componenten die echt overal opduiken.

Een plugin mag ook gewoon een functie zijn

Vind je het object met een methode install te plechtig, dan volstaat een functie: Vue roept hem met dezelfde argumenten aan.

src/plugins/titre.js
export default function titrePlugin(app, options) {
  app.config.globalProperties.$titre = options.titre
}

Het publiceerbare package

Test hem eerst in een vers Vite-project. Wil je hem op npm zetten, exporteer hem dan als default en houd de dependency op Vue in peerDependencies, zodat het gastproject zijn eigen kopie levert:

package.json
{
  "name": "gekkode-monitor",
  "type": "module",
  "main": "./dist/index.js",
  "exports": { ".": "./dist/index.js" },
  "peerDependencies": { "vue": "^3.5.0" }
}
De auto-installatie heeft geen zin meer

Vue 2-plugins eindigden vaak met if (window.Vue) window.Vue.use(Plugin), om zichzelf te installeren wanneer de pagina Vue via een script-tag laadde. In Vue 3 bestaat er geen globale window.Vue meer: dat blok doet niets en hoort eruit.

De globale mixin, als laatste redmiddel

De voorbeeldplugin gebruikt app.mixin om elk mount-moment te observeren. Dat is het juiste gereedschap voor een dwarsdoorsnijdende behoefte als telemetrie, waarbij je echt alle componenten wilt raken.

Voor al het andere kies je een composable: een geëxporteerde functie die de betrokken componenten zelf aanroepen. Een globale mixin geldt overal, ook waar je hem niet verwacht, en maakt het lastig te achterhalen waar gedrag vandaan komt.

src/composables/useJournal.js
import { inject } from 'vue'

export function useJournal() {
  const journal = inject('journal')
  return {
    journal,
    ajouter: (message) => journal.push(message)
  }
}

Een plugin testen

Vue Test Utils accepteert plugins in de optie global, inclusief hun opties:

javascript
import { mount } from '@vue/test-utils'
import monitor from '@/plugins/monitor.js'

mount(MonComposant, {
  global: { plugins: [[monitor, { prefixe: '[test]' }]] }
})

Zonder opties volstaat de korte vorm: plugins: [monitor].

Verder lezen

Veel libraries uit het ecosysteem installeren zich op deze manier, van VueFire tot unhead voor de vindbaarheid.

Plugins komen pas tot hun recht zodra je componenten en injection onder de knie hebt. Zijn die begrippen nog vers, pak dan de Vue 3-tutorial voor beginners er weer bij, vooral de hoofdstukken over componenten en props. Voor gedeelde state is Pinia zelf een plugin, en de broncode ervan is goed leesvoer.

Veelgemaakte fouten

Een Vue 2-plugin klakkeloos overnemen install krijgt de applicatie-instantie, niet langer de globale constructor. Vue.prototype bestaat niet meer: gebruik app.provide() of app.config.globalProperties.
globalProperties met script setup this bestaat niet in <script setup>: de property is alleen leesbaar vanuit het template. Kies liever provide en inject.
Het auto-installatieblok laten staan if (window.Vue) window.Vue.use(Plugin) vindt in Vue 3 niets meer om aan te haken en hoort eruit.
Te veel componenten globaal registreren Ze ontsnappen aan tree shaking en maken de bundle zwaarder, ook ongebruikt.

JavaScriptVue.js

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.