Erste Schritte mit Vue Plugins

Avatar of Francis Cote
Francis Cote am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

In den letzten Monaten habe ich viel über Vue gelernt. Von der Erstellung SEO-freundlicher SPAs über die Erstellung toller Blogs bis hin zum Spielen mit Übergängen und Animationen habe ich das Framework gründlich ausprobiert.

Aber es gab ein fehlendes Stück während meines Lernens: Plugins.

Die meisten Leute, die mit Vue arbeiten, haben entweder angefangen, sich auf Plugins als Teil ihres Workflows zu verlassen, oder werden ihnen irgendwann über den Weg laufen. Wie auch immer, sie sind eine großartige Möglichkeit, vorhandenen Code zu nutzen, ohne ständig neu schreiben zu müssen.

Viele von Ihnen haben wahrscheinlich jQuery verwendet und sind daran gewöhnt, Plugins zu verwenden (oder zu erstellen!), um alles von Karussells und Modals bis hin zu responsiven Videos und Schriftarten zu erstellen. Wir sprechen hier im Grunde über dasselbe bei Vue-Plugins.

Also, Sie möchten eines erstellen? Ich gehe davon aus, dass Sie nicken, damit wir uns gemeinsam mit einer Schritt-für-Schritt-Anleitung zum Schreiben eines benutzerdefinierten Vue-Plugins die Hände schmutzig machen können.

Zuerst ein wenig Kontext…

Plugins sind nichts Spezifisches für Vue und – genau wie bei jQuery – werden Sie feststellen, dass es eine große Vielfalt von Plugins gibt, die viele verschiedene Dinge tun. Per Definition zeigen sie an, dass eine Schnittstelle zur Erweiterbarkeit bereitgestellt wird.

Fazit: Sie sind eine Möglichkeit, globale Funktionen in eine App einzubinden und diese für Ihre Zwecke zu erweitern.

Die Vue-Dokumentation behandelt Plugins im Detail und bietet eine hervorragende Liste von breiten Kategorien, in die Plugins im Allgemeinen fallen

  1. Fügen Sie einige globale Methoden oder Eigenschaften hinzu.
  2. Fügen Sie ein oder mehrere globale Assets hinzu: Direktiven/Filter/Übergänge usw.
  3. Fügen Sie einige Komponentenoptionen durch globale Mixins hinzu.
  4. Fügen Sie einige Vue-Instanzmethoden hinzu, indem Sie sie an Vue.prototype anhängen.
  5. Eine Bibliothek, die eine eigene API bereitstellt und gleichzeitig eine Kombination der oben genannten Elemente injiziert.

OK, OK. Genug der Einleitung. Lasst uns etwas Code schreiben!

Was wir machen

Bei Spektrum, der Mutteragentur von Snipcart, durchlaufen unsere Designs einen Genehmigungsprozess, wie es bei den meisten anderen Agenturen und Unternehmen üblich ist. Wir erlauben einem Kunden, Designs zu kommentieren und Vorschläge zu machen, während er sie überprüft, damit wir letztendlich das grüne Licht erhalten, um fortzufahren und das Ding zu bauen.

Wir verwenden dafür in der Regel InVision. Das Kommentarsystem ist eine Kernkomponente von InVision. Es ermöglicht den Leuten, auf jeden Teil des Designs zu klicken und einen Kommentar für Kollaborateure zu hinterlassen, genau dort, wo dieses Feedback Sinn macht. Das ist ziemlich cool.

So cool InVision auch ist, ich denke, wir können dasselbe mit ein wenig Vue-Magie selbst tun und ein Plugin entwickeln, das jeder nutzen kann.

Die gute Nachricht ist, dass sie nicht *so* einschüchternd sind. Grundlegende Vue-Kenntnisse sind alles, was Sie brauchen, um sofort mit Plugins herumzuspielen.

Schritt 1. Vorbereiten der Codebasis

Ein Vue-Plugin sollte eine install-Methode enthalten, die zwei Parameter entgegennimmt

  1. Das globale Vue-Objekt
  2. Ein Objekt, das benutzerdefinierte Optionen enthält

Dank Vue CLI 3 ist das Starten eines Vue-Projekts super einfach. Sobald Sie dies installiert haben, führen Sie Folgendes in Ihrer Befehlszeile aus

$ vue create vue-comments-overlay
# Answer the few questions
$ cd vue-comments-overlay
$ npm run serve

Dies gibt uns den klassischen "Hello World"-Start, den wir brauchen, um eine Test-App zu erstellen, die unser Plugin nutzen wird.

Schritt 2. Erstellen des Plugin-Verzeichnisses

Unser Plugin muss irgendwo im Projekt leben, also erstellen wir ein Verzeichnis, in dem wir all unsere Arbeit unterbringen können, und navigieren dann mit unserer Befehlszeile in das neue Verzeichnis

$ mkdir src/plugins
$ mkdir src/plugins/CommentsOverlay
$ cd src/plugins/CommentsOverlay

Schritt 3: Einrichten der grundlegenden Verkabelung

Ein Vue-Plugin ist im Grunde ein Objekt mit einer install-Funktion, die ausgeführt wird, wenn die es verwendende Anwendung es mit Vue.use() einbindet.

Die install-Funktion erhält das globale Vue-Objekt als Parameter und ein Options-Objekt

// src/plugins/CommentsOverlay/index.js
// 
export default {
  install(vue, opts){   
    console.log('Installing the CommentsOverlay plugin!')
    // Fun will happen here
  }
}

Nun binden wir dies in unsere "Hello World"-Test-App ein

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import CommentsOverlay from './plugins/CommentsOverlay' // import the plugin

Vue.use(CommentsOverlay) // put the plugin to use!

Vue.config.productionTip = false

new Vue({ render: createElement => createElement(App)}).$mount('#app')

Schritt 4: Unterstützung für Optionen bereitstellen

Wir möchten, dass das Plugin konfigurierbar ist. Dies ermöglicht jedem, der es in seiner eigenen App verwendet, Dinge anzupassen. Es macht unser Plugin auch vielseitiger.

Wir machen Optionen zum zweiten Argument der install-Funktion. Erstellen wir die Standardoptionen, die das Basisverhalten des Plugins darstellen, d. h. wie es funktioniert, wenn keine benutzerdefinierte Option angegeben ist

// src/plugins/CommentsOverlay/index.js

const optionsDefaults = {
  // Retrieves the current logged in user that is posting a comment
  commenterSelector() {
    return {
      id: null,
      fullName: 'Anonymous',
      initials: '--',
      email: null
    }
  },
  data: {
    // Hash object of all elements that can be commented on
    targets: {},
    onCreate(created) {
      this.targets[created.targetId].comments.push(created)
    },
    onEdit(editted) {
      // This is obviously not necessary
      // It's there to illustrate what could be done in the callback of a remote call
      let comments = this.targets[editted.targetId].comments
      comments.splice(comments.indexOf(editted), 1, editted);
    },
    onRemove(removed) {
      let comments = this.targets[removed.targetId].comments
      comments.splice(comments.indexOf(removed), 1);
    }
  }
}

Dann können wir die Optionen, die in die install-Funktion übergeben werden, über diese Standardwerte legen

// src/plugins/CommentsOverlay/index.js

export default {
  install(vue, opts){
    // Merge options argument into options defaults
    const options = { ...optionsDefaults, ...opts }
    // ...
  }
}

Schritt 5: Erstellen einer Instanz für die Kommentar-Schicht

Eine Sache, die Sie mit diesem Plugin vermeiden möchten, ist, dass sein DOM und seine Stile mit der App, auf der es installiert ist, interferieren. Um die Wahrscheinlichkeit zu minimieren, dass dies geschieht, besteht ein Weg darin, das Plugin in einer neuen, separaten Vue-Instanz leben zu lassen, außerhalb des Komponentengraphen der Hauptanwendung.

Fügen Sie Folgendes zur install-Funktion hinzu

// src/plugins/CommentsOverlay/index.js

export default {
  install(vue, opts){
    ...
  // Create plugin's root Vue instance
      const root = new Vue({
        data: { targets: options.data.targets },
        render: createElement => createElement(CommentsRootContainer)
      })

      // Mount root Vue instance on new div element added to body
      root.$mount(document.body.appendChild(document.createElement('div')))

      // Register data mutation handlers on root instance
      root.$on('create', options.data.onCreate)
      root.$on('edit', options.data.onEdit)
      root.$on('remove', options.data.onRemove)

      // Make the root instance available in all components
      vue.prototype.$commentsOverlay = root
      ...
  }
}

Wesentliche Teile im obigen Snippet

  1. Die App lebt in einem neuen div am Ende des body.
  2. Die im options-Objekt definierten Ereignis-Handler sind mit den entsprechenden Ereignissen auf der Root-Instanz verbunden. Das wird am Ende des Tutorials Sinn ergeben, versprochen.
  3. Die $commentsOverlay-Eigenschaft, die dem Prototyp von Vue hinzugefügt wird, macht die Root-Instanz für alle Vue-Komponenten in der Anwendung zugänglich.

Schritt 6: Eine benutzerdefinierte Direktive erstellen

Schließlich benötigen wir eine Möglichkeit für Apps, die das Plugin verwenden, ihm mitzuteilen, welches Element die Kommentarfunktionalität aktiviert haben soll. Dies ist ein Fall für eine benutzerdefinierte Vue-Direktive. Da Plugins Zugriff auf das globale Vue-Objekt haben, können sie neue Direktiven definieren.

Unsere wird comments-enabled heißen und sie geht so

// src/plugins/CommentsOverlay/index.js

export default {
  install(vue, opts){

    ...

    // Register custom directive tha enables commenting on any element
    vue.directive('comments-enabled', {
      bind(el, binding) {

        // Add this target entry in root instance's data
        root.$set(
          root.targets,
          binding.value,
          {
            id: binding.value,
            comments: [],
            getRect: () => el.getBoundingClientRect(),
          });

        el.addEventListener('click', (evt) => {
          root.$emit(`commentTargetClicked__${binding.value}`, {
            id: uuid(),
            commenter: options.commenterSelector(),
            clientX: evt.clientX,
            clientY: evt.clientY
          })
        })
      }
    })
  }
}

Die Direktive tut zwei Dinge

  1. Sie fügt ihr Ziel den Daten der Root-Instanz hinzu. Der dafür definierte Schlüssel ist binding.value. Sie ermöglicht es den Nutzern, ihre eigene ID für Zielelemente anzugeben, wie z.B.: <img v-comments-enabled="imgFromDb.id" src="imgFromDb.src" />.
  2. Sie registriert einen click-Ereignis-Handler auf dem Zielelement, der wiederum ein Ereignis auf der Root-Instanz für dieses bestimmte Ziel auslöst. Wie wir damit umgehen werden, sehen wir später.

Die install-Funktion ist nun vollständig! Nun können wir uns der Kommentarfunktionalität und den zu rendernden Komponenten widmen.

Schritt 7: Einen "Kommentar-Root-Container"-Komponenten einrichten

Wir werden einen CommentsRootContainer erstellen und ihn als Root-Komponente der Plugin-Benutzeroberfläche verwenden. Werfen wir einen Blick darauf

<!-- 
 src/plugins/CommentsOverlay/CommentsRootContainer.vue -->

<template>
  <div>
    <comments-overlay
        v-for="target in targets"
        :target="target"
        :key="target.id">
    </comments-overlay>
  </div>
</template>

<script>
import CommentsOverlay from "./CommentsOverlay";

export default {
  components: { CommentsOverlay },
  computed: {
    targets() {
      return this.$root.targets;
    }
  }
};
</script>

Was macht das? Wir haben im Grunde einen Wrapper erstellt, der eine weitere Komponente enthält, die wir noch nicht erstellt haben: CommentsOverlay. Sie sehen, wo diese Komponente im Skript importiert wird und welche Werte innerhalb der Wrapper-Vorlage angefordert werden (target und target.id). Beachten Sie, wie die berechnete target-Eigenschaft aus den Daten der Root-Komponente abgeleitet wird.

Nun, die Overlay-Komponente ist, wo die ganze Magie passiert. Kommen wir dazu!

Schritt 8: Magie mit einer "Comments Overlay"-Komponente

OK, ich werde Ihnen viel Code zumuten, aber wir werden ihn durchgehen

<!--  src/plugins/CommentsOverlay/CommentsRootContainer.vue -->

<template>
  <div class="comments-overlay">

    <div class="comments-overlay__container" v-for="comment in target.comments" :key="comment.id" :style="getCommentPostition(comment)">
      <button class="comments-overlay__indicator" v-if="editing != comment" @click="onIndicatorClick(comment)">
        {{ comment.commenter.initials }}
      </button>
      <div v-else class="comments-overlay__form">
        <p>{{ getCommentMetaString(comment) }}</p>
        <textarea ref="text" v-model="text" />        
        <button @click="edit" :disabled="!text">Save</button>
        <button @click="cancel">Cancel</button>
        <button @click="remove">Remove</button>
      </div>
    </div>

    <div class="comments-overlay__form" v-if="this.creating" :style="getCommentPostition(this.creating)">
      <textarea ref="text" v-model="text" />
      <button @click="create" :disabled="!text">Save</button>
      <button @click="cancel">Cancel</button>
    </div>

  </div>
</template>

<script>
export default {
  props: ['target'],

  data() {
    return {
      text: null,
      editing: null,
      creating: null
    };
  },

  methods: {
    onTargetClick(payload) {
      this._resetState();
      const rect = this.target.getRect();

      this.creating = {
        id: payload.id,
        targetId: this.target.id,
        commenter: payload.commenter,
        ratioX: (payload.clientX - rect.left) / rect.width,
        ratioY: (payload.clientY - rect.top) / rect.height
      };
    },
    onIndicatorClick(comment) {
      this._resetState();
      this.text = comment.text;
      this.editing = comment;
    },
    getCommentPostition(comment) {
      const rect = this.target.getRect();
      const x = comment.ratioX  <em> rect.width + rect.left;
      const y = comment.ratioY  <em> rect.height + rect.top;
      return { left: `${x}px`>, top: `${y}px` };
    },
    getCommentMetaString(comment) {
      return `${
        comment.commenter.fullName
      } - ${comment.timestamp.getMonth()}/${comment.timestamp.getDate()}/${comment.timestamp.getFullYear()}`;
    },
    edit() {
      this.editing.text = this.text;
      this.editing.timestamp = new Date();
      this._emit("edit", this.editing);
      this._resetState();
    },
    create() {
      this.creating.text = this.text;
      this.creating.timestamp = new Date();
      this._emit("create", this.creating);
      this._resetState();
    },
    cancel() {
      this._resetState();
    },
    remove() {
      this._emit("remove", this.editing);
      this._resetState();
    },
    _emit(evt, data) {
      this.$root.$emit(evt, data);
    },
    _resetState() {
      this.text = null;
      this.editing = null;
      this.creating = null;
    }
  },

  mounted() {
    this.$root.$on(`commentTargetClicked__${this.target.id}`, this.onTargetClick
    );
  },

  beforeDestroy() {
    this.$root.$off(`commentTargetClicked__${this.target.id}`, this.onTargetClick
    );
  }
};
</script>

Ich weiß, ich weiß. Ein bisschen einschüchternd. Aber im Grunde tut es nur ein paar wichtige Dinge.

Zuerst einmal etabliert der gesamte erste Teil im <template>-Tag das Markup für einen Kommentar-Popover, der auf dem Bildschirm mit einem Formular zur Eingabe eines Kommentars angezeigt wird. Mit anderen Worten, dies ist das HTML-Markup, das unsere Kommentare rendert.

Als nächstes schreiben wir die Skripte, die das Verhalten unserer Kommentare steuern. Die Komponente empfängt das vollständige target-Objekt als prop. Hier sind das Kommentarray und die Positionsinformationen gespeichert.

Dann, die Magie. Wir haben mehrere Methoden definiert, die wichtige Dinge tun, wenn sie ausgelöst werden

  • Lauscht auf einen Klick
  • Rendert eine Kommentarbox und positioniert sie dort, wo der Klick ausgeführt wurde
  • Erfasst die vom Benutzer übermittelten Daten, einschließlich des Namens des Benutzers und des Kommentars
  • Bietet Möglichkeiten, einen Kommentar zu erstellen, zu bearbeiten, zu entfernen und abzubrechen

Schließlich wird der Handler für die commentTargetClicked-Ereignisse, die wir zuvor gesehen haben, innerhalb der mounted- und beforeDestroy-Hooks verwaltet.

Es ist erwähnenswert, dass die Root-Instanz als Event Bus verwendet wird. Auch wenn dieser Ansatz oft abgeraten wird, habe ich ihn in diesem Kontext für angemessen gehalten, da die Komponenten nicht öffentlich zugänglich sind und als monolithische Einheit betrachtet werden können.

Und, wir sind fertig! Nach ein wenig Styling (ich werde meine fragwürdigen CSS-Fähigkeiten nicht weiter ausführen) ist unser Plugin bereit, Benutzerkommentare zu Zielelementen entgegenzunehmen!

Demo!

Live-Demo

GitHub-Repo

Sich mit mehr Vue-Plugins vertraut machen

Wir haben den Großteil dieses Beitrags dem Erstellen eines Vue-Plugins gewidmet, aber ich möchte dies zum Grund zurückführen, warum wir Plugins überhaupt verwenden. Ich habe eine kurze Liste von *extrem* beliebten Vue-Plugins zusammengestellt, um all die wunderbaren Dinge zu zeigen, die Sie erhalten, wenn Sie Plugins nutzen.

  • Vue-router – Wenn Sie Single-Page-Anwendungen erstellen, werden Sie zweifellos Vue-router benötigen. Als offizieller Router für Vue integriert er sich tief in den Kern, um Aufgaben wie das Abbilden von Komponenten und das Schachteln von Routen zu erledigen.
  • Vuex – Als zentraler Speicher für alle Komponenten einer Anwendung ist Vuex ein No-Brainer, wenn Sie große Apps mit hoher Wartbarkeit erstellen möchten.
  • Vee-validate – Beim Erstellen typischer Geschäftsanwendungen kann die Formularvalidierung schnell unübersichtlich werden, wenn sie nicht sorgfältig gehandhabt wird. Vee-validate kümmert sich auf anmutige Weise darum. Es verwendet Direktiven und ist auf Lokalisierung ausgelegt.

Ich beschränke mich auf diese Plugins, aber wissen Sie, dass es viele andere gibt, die darauf warten, Vue-Entwicklern wie Ihnen zu helfen!

Und, hey! Wenn Sie kein Plugin finden, das Ihren genauen Bedürfnissen entspricht, haben Sie jetzt praktische Erfahrung im Erstellen eines benutzerdefinierten Plugins. 😀