https://d226lax1qjow5r.cloudfront.net/blog/blogposts/migrating-react-components-to-vue-js-dr/E_Migrating-to-Vue-js_1200x600.png

React-Komponenten nach Vue.js migrieren

Zuletzt aktualisiert am November 5, 2020

Lesedauer: 5 Minuten

In diesem Blog-Beitrag erzähle ich von der Reise, die wir bei der Migration unserer Entwicklerplattform von React zu Vue.js. Ich werde die Gründe für den Wechsel erläutern, wie wir ihn vollzogen haben, und einige Lektionen, die wir dabei gelernt haben.

Die Anwendung

Die Vonage API-Entwicklerplattform ist eine Ruby on Rails-Anwendung mit einigen React-Komponenten, die wir isoliert für sehr spezifische Anwendungsfälle mit viel Benutzerinteraktion eingesetzt haben. Wir haben insgesamt vier Komponenten migriert, die für ein Feedback-Widget, die Suchleiste, einen SMS-Zähler und einen JWT-Generator (JSON Web Token) verantwortlich waren. Die App ist Open Source und Sie finden sie auf Github.

Der Grund für die Migration war, dass verschiedene Teams innerhalb des Unternehmens unterschiedliche Javascript-Frameworks verwendeten, was uns nicht nur daran hinderte, Komponenten über verschiedene Applikationen hinweg wiederzuverwenden, sondern auch eine höhere Einstiegshürde für Ingenieure darstellte, die zwischen Projekten wechselten. Vor diesem Hintergrund haben wir uns für Vue.js als unser erstes Javascript-Framework entschieden, vor allem wegen seiner Einfachheit. Es ist ziemlich einfach für jemanden mit Javascript-Erfahrung, etwas innerhalb von Minuten nach dem Lesen der Vue.js-Anleitungen zu bauen.

React und Vue.js haben einige Gemeinsamkeiten: Beide nutzen ein virtuelles DOM, bieten reaktive und zusammensetzbare View-Komponenten und konzentrieren sich auf eine kleine Kernbibliothek, wobei das Routing und die globale Zustandsverwaltung zusätzlichen Bibliotheken überlassen wird. Aber was uns an Vue.js wirklich gefallen hat, ist die Art und Weise, wie es auf klassischen Webtechnologien aufbaut. In React drücken die Komponenten ihre UI mit JSX und Renderfunktionen aus. Vue.js hingegen behandelt jedes gültige HTML als gültiges Vue-Template und trennt die Logik von der Präsentation (obwohl sie auch Renderfunktionen und JSX unterstützen 😉 ).

Es gibt noch ein paar andere Eigenschaften von Vue.js, die es für uns attraktiv gemacht haben: die bequeme und einfache Art, wie es die Zustandsverwaltung mit data und props im Vergleich zu Reacts setStateDie Art und Weise, wie Vue.js Änderungen verfolgt und den Zustand einer Komponente entsprechend aktualisiert, indem es reaktive Datenund schließlich die berechneten Eigenschaften, die es ermöglichen, Logik aus den Vorlagen zu extrahieren, indem man Eigenschaften definiert, die von anderen Eigenschaften abhängen.

Der Ansatz, den wir gewählt haben, war ein iterativer. Wir fügten Vue.js zum Projekt hinzu und migrierten dann eine Komponente nach der anderen. Glücklicherweise wird Rails mit Webpack und mit grundlegenden Out-of-the-Box-Integrationen für React, Vue.js und Elm geliefert. Sie können mehr darüber in den docsaber alles, was wir tun mussten, war zu starten:

bundle exec rails webpacker:install:vue

Damit war die Installation von Vue.js und all seinen Abhängigkeiten erledigt, während die entsprechenden Konfigurationsdateien für uns aktualisiert wurden 🎉.

Tests

Das erste, was uns auffiel, war, dass wir keine Tests hatten 😢. Ich kann gar nicht in Worte fassen, wie wichtig eine automatisierte Testsuite für diese Art der Migration (oder generell) ist. Die manuelle Qualitätssicherung nimmt viel Zeit in Anspruch, und außerdem, wer mag keine Automatisierung?

Als erstes fügten wir also Jest zum Projekt hinzu, zusammen mit Tests für die verschiedenen Komponenten. Wir konzentrierten uns darauf, das Verhalten zu testen, wie sich die Benutzeroberfläche als Reaktion auf Benutzerinteraktionen auf eine Framework-unabhängige Weise verändert, damit wir sie verwenden konnten, während wir die Komponenten neu schrieben. Unten sehen Sie ein kleines Beispiel für einen der Tests:

describe('Concatenation', function() {
  describe('Initial rendering', function() {
    it('Renders the default message', async function() {
      const wrapper = shallowMount(Concatenation);

      expect(wrapper.find('h2').text()).toEqual('Try it out');
      expect(wrapper.html()).toContain('<h4>Message</h4>');
      expect(wrapper.find('textarea').element.value).toEqual(
        "It was the best of times, it was the worst of times, it was the age of wisdom..."
      );

    it('notifies the user if unicode is required and updates the UI accordingly', function() {
      const wrapper = shallowMount(Concatenation);

      wrapper.find('textarea').setValue('😀');
      expect(wrapper.find('i.color--success').exists()).toBeTruthy();
      expect(wrapper.find('#sms-composition').text()).toEqual('2 characters sent in 1 message part');
      expect(wrapper.find('code').text()).toContain('😀');

      wrapper.find('textarea').setValue('not unicode');
      expect(wrapper.find('i.color--error').exists()).toBeTruthy();
      expect(wrapper.find('#sms-composition').text()).toEqual('11 characters sent in 1 message part');
      expect(wrapper.find('code').text()).toContain('not unicode');
    });

Wie Sie sehen können, gibt es nichts Rahmenspezifisches. Wir mounten die Concatenation Komponente, überprüfen dann, ob sie einige Standardwerte rendert und die Benutzeroberfläche nach einer Interaktion aktualisiert.

Während wir die Komponenten neu schrieben, verbrachten wir Zeit damit, nicht nur ihre Implementierung zu verstehen, sondern auch, wie sie funktionieren sollten. Dabei haben wir mehrere Fehler gefunden, die wir behoben und für die wir Tests geschrieben haben. Die Testsuite dient auch als Dokumentation 🎉🎉🎉, da sie beschreibt, wie die Komponenten funktionieren und wie sie mit verschiedenen Interaktionen umgehen.

Migration

Um unseren Migrationsprozess zu veranschaulichen, konzentrieren wir uns auf die Komponente SMS-Zähler. Die Hauptfunktionalität dieser Komponente besteht darin, zu erkennen, ob der vom Benutzer eingegebene Text auf der Grundlage seines Inhalts, seiner Kodierung und seiner Länge in mehrere SMS-Nachrichten aufgeteilt werden kann. Sie können sich auf unsere Dokumentationen nachlesen, wenn Sie mehr darüber wissen wollen, wie sich diese Dinge auf das auswirken, was gesendet wird. Die Komponente sieht wie folgt aus:

SMS character counter componentSMS character counter component

Es hat einen textarea mit einem Platzhalter, in den der Benutzer den Inhalt eintippen/einfügen kann. Anschließend teilt die Komponente mit, in wie viele Teile die Nachricht aufgeteilt wird, wie lang sie ist und welche Art der Kodierung verwendet wird (ob es sich um unicode oder text).

Wir haben eine kleine Bibliothek, CharacterCounterdie die gesamte SMS-Verarbeitung übernimmt und alle notwendigen Informationen zurückgibt, wie z.B. die Anzahl der benötigten Nachrichten, deren Inhalt, etc. Die Vue.js-Komponente kümmert sich also nur um die Benutzerinteraktion, verarbeitet die Informationen und stellt den Inhalt entsprechend dar.

Wir haben die Vue.js Stilrichtlinien und entschieden uns, Komponenten in einer einzigen Datei zu verwenden. Dies macht es einfacher, Komponenten zu finden und zu bearbeiten, als wenn mehrere Komponenten in einer Datei definiert sind. Der Code für die Komponente ist wie folgt:

<template>
  <div class="Vlt-box">
    <h2>Try it out</h2>

    <h4>Message</h4>
    <div class="Vlt-textarea">
      <textarea v-model="body" />
    </div>

    <div class="Vlt-margin--top2" />

    <h4>Data</h4>
    <div class="Vlt-box Vlt-box--white Vlt-box--lesspadding">
      <div class="Vlt-grid">
        <div class="Vlt-col Vlt-col--1of3">
          <b>Unicode is Required?</b>
          <i v-if="unicodeRequired" class="icon icon--large icon-check-circle color--success"></i>
          <i v-else class="icon icon--large icon-times-circle color--error"></i>
        </div>
        <div class="Vlt-col Vlt-col--2of3">
        </div>
        <hr class="hr--shorter"/>
        <div class="Vlt-col Vlt-col--1of3">
          <b>Length</b>
        </div>
        <div class="Vlt-col Vlt-col--2of3" v-html="smsComposition" id="sms-composition"></div>
      </div>
    </div>

    <h4>Parts</h4>
    <div class="Vlt-box Vlt-box--white Vlt-box--lesspadding" id="parts">
      <div v-for= "(message, index) in messages" class="Vlt-grid">
        <div class="Vlt-col Vlt-col--1of3"><b>Part {{index + 1}}</b></div>
        <div class="Vlt-col Vlt-col--2of3">
          <code>
            <span v-if="messages.length > 1">
              <span class="Vlt-badge Vlt-badge--blue">User Defined Header</span>
              <span>&nbsp;</span>
            </span>
            {{message}}
          </code>
        </div>
        <hr v-if="index + 1 !== messages.length" class="hr--shorter"/>
      </div>
    </div>
  </div>
</template>

<script>
import CharacterCounter from './character_counter';

export default {
  data: function () {
    return {
      body: 'It was the best of times, it was the worst of times, it was the age of wisdom...
    };
  },
  computed: {
    smsInfo: function() {
      return new CharacterCounter(this.body).getInfo();
    },
    messages: function() {
      return this.smsInfo.messages;
    },
    unicodeRequired: function() {
      return this.smsInfo.unicodeRequired;
    },
    smsComposition: function() {
      let count = this.smsInfo.charactersCount;
      let characters = this.pluralize('character', count);
      let messagesLength = this.messages.length;
      let parts = this.pluralize('part', messagesLength);

      return `${count} ${characters} sent in ${messagesLength} message ${parts}`;
    }
  },
  methods: {
    pluralize: function(singular, count) {
      if (count === 1) { return singular; }
      return `${singular}s`;
    }
  }
}
</script>

<style scoped>
  textarea {
    width: 100%;
    height: 150px;
    resize: vertical;
  }
  code {
    whiteSpace: normal;
    wordBreak: break-all;
 }
</style>

Zuerst haben wir das Template definiert. Sie haben vielleicht bemerkt, dass wir einige Vue.js-Direktiven verwendet haben für bedingtes Rendering, wie v-if und v-else. Dies ist eine der besten Eigenschaften von Vue.js, die React nicht bietet. React behandelt bedingtes Rendering anders, indem es entweder den ternären Operator inline verwendet, inline if mit dem logischen && Operator, oder durch den Aufruf einer Funktion, die je nach Argumenten unterschiedliche Inhalte zurückgibt. Unten ist ein Vergleich, wie wir rendern, dass die Kodierung ist unicode in Vue.js vs. React:

  // Vue.js
  <div class="Vlt-col Vlt-col--1of3">
    <b>Unicode is Required?</b>
    <i v-if="unicodeRequired" class="icon icon--large icon-check-circle color--success"></i>
    <i v-else class="icon icon--large icon-times-circle color--error"></i>
  </div>
  // React
  renderUtfIcon(required) {
    if (required) {
      return (<i className="icon icon--large icon-check-circle color--success"/>)
    } else {
      return (<i className="icon icon--large icon-times-circle color--error"/>)
    }
  }
  <div className="Vlt-col Vlt-col--1of3">
    <b>Unicode is Required?</b>
    { this.renderUtfIcon(smsInfo.unicodeRequired) }
  </div>

In beiden Fällen wurde der Wert einer Eigenschaft verwendet. Im Fall von Vue.js machen es die Direktiven recht einfach, alles inline zu rendern. Bei React hingegen mussten wir eine Hilfsmethode erstellen, die die verschiedenen Inhalte auf der Grundlage der übergebenen Eigenschaft zurückgibt, was nicht nur zu mehr Code führte, sondern auch zu einer Aufteilung des Markups auf die render Funktion und Hilfsmethoden aufgeteilt.

Die Migration war recht einfach, da die Komponente alle Informationen in ihrem Zustand behielt, ohne sie mit anderen teilen zu müssen. Es mussten lediglich einige Methoden, berechnete Eigenschaften und Bedingungen in der HTML-Datei implementiert werden.

Die textarea ist gebunden an eine Dateneigenschaft namens body. Die folgenden berechneten Eigenschaften wurden definiert:

  • smsInfo

  • messages

  • unicodeRequired

  • smsComposition

Berechnete Eigenschaften sind im Wesentlichen Eigenschaften, mit dem Unterschied, dass sie nur neu ausgewertet werden, wenn eine ihrer reaktiven Abhängigkeiten ändert. Diese Abhängigkeiten sind die Eigenschaften, die in ihrer Body-Definition verwendet werden. Sehen wir uns ein Beispiel an:

  data: function () {
    return {
      body: 'It was the best of times, it was the worst of times, it was the age of wisdom...'
    };
  },
  computed: {
    smsInfo: function() {
      return new CharacterCounter(this.body).getInfo();
    },
  }

Hier, smsInfo zwischengespeichert, bis sich der Wert von body ändert. Wenn Sie ihn bei jedem Aufruf neu auswerten müssen, sollten Sie stattdessen eine method stattdessen verwenden.

Sobald wir die Vue.js-Komponente hatten, stellten wir sicher, dass unsere Tests erfolgreich waren, und schließlich ersetzten wir die Komponenten in unserer Anwendung. Und das war's! Der gesamte Code ist quelloffen und Sie können ihn finden auf GitHub. Wir Beiträge! Wenn Sie einen Blick auf die vollständige Migration werfen wollen, können Sie den entsprechenden Pull-Anfrage.

Wir planen, alle unsere Komponenten in naher Zukunft als Pakete zur Verfügung zu stellen, damit wir sie mit Ihnen allen teilen können!

Teilen Sie:

https://a.storyblok.com/f/270183/384x384/d4e395e293/fabianrodiguez.png
Fabian RodriguezVonage Ehemalige

Fabian war Teil des Developer Experience Teams bei Vonage. Er ist ein leidenschaftlicher Softwareentwickler, der Open Source, maschinelles Lernen und Kaffee liebt. Wenn er nicht gerade daran arbeitet, unsere Dokumente zu verbessern, kann man ihn beim Radfahren, Lesen oder Anfeuern des Club Nacional de Football antreffen.