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

ReactコンポーネントのVue.jsへの移行

最終更新日 November 5, 2020

所要時間:1 分

このブログポストでは、私たちが開発者向けプラットフォームを移行する際に経験した道のりを紹介します。 デベロッパー・プラットフォームから リアクトから Vue.js.変更の背景とその方法、そしてその過程で学んだいくつかの教訓について説明します。

アプリケーション

Vonage API Developer PlatformはRuby on Railsアプリケーションで、ユーザーとのやり取りが多い非常に特殊なユースケースを処理するために、いくつかのReactコンポーネントを分離して使用していました。私たちは、フィードバック・ウィジェット、検索バー、SMS 文字カウンター、JWT (JSON Web Token) ジェネレーターを担当する合計 4 つのコンポーネントを移行しました。このアプリはオープンソースで Github.

移行の背景には、社内の異なるチームが異なるJavascriptフレームワークを使用していたことがあります。そのため、異なるアプリケーション間でコンポーネントを再利用できないだけでなく、プロジェクト間を切り替えるエンジニアにとって参入障壁が高くなっていました。このことを念頭に置いて、私たちはVue.jsをJavascriptフレームワークとして選択しました。Javascriptの経験がある人なら、Vue.jsのガイドを読めば、数分で何かを作ることができます。

ReactとVue.jsにはいくつかの共通点がある。仮想DOMを利用し、リアクティブでコンポーザブルなビュー・コンポーネントを提供し、小さなコア・ライブラリに集中し、ルーティングやグローバルな状態管理は追加のライブラリに任せる。しかし、私たちがVue.jsを本当に気に入ったのは、古典的なウェブ・テクノロジーの上に構築している点だ。Reactでは、コンポーネントはJSXとレンダー関数を使ってUIを表現する。一方、Vue.jsは、有効なHTMLを有効なVueテンプレートとして扱い、ロジックをプレゼンテーションから分離する(ただし、レンダー関数とJSXもサポートしている😉)。

Vue.jsの魅力は他にもいくつかあります。 dataそして propsReactの setStateVue.jsがどのように変更を追跡し、それに応じてコンポーネントの状態を更新するか。 リアクティブ・データそして最後に、他のプロパティに依存するプロパティを定義することで、テンプレートからロジックを抽出できるコンピューテッド・プロパティです。

私たちがとったアプローチは、反復的なものでした。プロジェクトにVue.jsを追加し、コンポーネントを1つずつ移行していきました。幸いなことに、Railsにはwebpackが付属しており、React、Vue.js、Elmのための基本的な統合機能がすぐに使えます。詳しくは ドキュメントを参照してほしい:

bundle exec rails webpacker:install:vue

これで、Vue.jsとその依存関係をすべてインストールし、対応する設定ファイルを更新してくれました🎉。

テスト

最初に気づいたのは、テストがないことでした😢。この種の移行において(あるいは一般的に)、自動化されたテスト・スイートを持つことがどれほど重要か、言葉では言い表せません。手作業でのQAには多くの時間がかかるし、自動化が嫌いな人はいないだろう。

そこで私たちが最初にしたことは Jestを追加することだった。私たちは、フレームワークにとらわれない方法で、ユーザーのインタラクションに応じてUIがどのように変化するかという振る舞いをテストすることに集中した。下に、テストの小さな例を示します:

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');
    });

ご覧の通り、フレームワーク特有のものは何もない。私たちは Concatenationコンポーネントをマウントし、いくつかのデフォルト値をレンダリングし、インタラクションの後にUIを更新することをチェックします。

コンポーネントを書き換えている間、私たちはその実装を理解するだけでなく、それらがどのように機能することになっているかを理解することにも時間を費やした。この過程で、いくつかのバグを発見し、それを修正してテストを書きました。テスト・スイートは、コンポーネントがどのように動作し、異なるインタラクションをどのように扱うかを記述しているので、ドキュメント🎉🎉の役割も果たします。

移住

移行プロセスを説明するために、SMS文字カウンターコンポーネントに焦点を当てます。このコンポーネントの主な機能は、ユーザーが入力したテキストがその内容、エンコーディング、長さに基づいて複数のSMSメッセージにまたがるかどうかを判断することです。私たちの ドキュメントを参照してください。コンポーネントは次のようになります:

SMS character counter componentSMS character counter component

それには textareaには、ユーザが内容を入力/貼り付けできるプレースホルダがあります。そして、このコンポーネントは、メッセージがいくつの部分に分割されるか、その長さ、使用されるエンコーディングのタイプ(それが unicodeまたは text).

私たちは小さなライブラリを持っています、 CharacterCounterこのライブラリは、すべてのSMS処理を処理し、必要なメッセージの数や内容など、必要な情報をすべて返します。そのため、Vue.jsコンポーネントは、ユーザーとのインタラクションを処理し、情報を処理し、それに応じてコンテンツをレンダリングするだけです。

Vue.jsの スタイルガイドに従い、単一ファイルのコンポーネントを使用することにしました。これにより、1つのファイルに複数のコンポーネントが定義されているよりも、コンポーネントを見つけやすく、編集しやすくなります。コンポーネントのコードは以下の通りです:

<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>

まず、テンプレートを定義します。お気づきかもしれませんが、Vue.jsのディレクティブを使って 条件付きレンダリングを使用していることにお気づきでしょう。 v-ifそして v-else.これは、ReactにはないVue.jsの優れた機能のひとつだ。Reactは 条件付きレンダリング三項演算子をインラインで使用するか、論理演算子 &&演算子を使うか、引数に応じて異なるコンテンツを返す関数を呼び出すかです。以下は、エンコーディングが unicodeVue.jsと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>

どちらの場合も、プロパティの値が使われている。Vue.jsの場合、ディレクティブを使えば、すべてをインラインでレンダリングすることができる。一方、Reactの場合は、渡されたプロパティに基づいて異なるコンテンツを返すヘルパー・メソッドを作成しなければならず、コードが増えるだけでなく、マークアップが render関数とヘルパー・メソッドにマークアップが分割されることになる。

このコンポーネントは、他と共有する必要なく、すべての情報をその状態に保持しているため、移行はかなり簡単だった。必要だったのは、HTMLにいくつかのメソッド、計算されたプロパティ、条件式を実装することだけだった。

textareaというデータ・プロパティにバインドされている。 body.以下の 計算プロパティが定義されている:

  • smsInfo

  • messages

  • unicodeRequired

  • smsComposition

計算プロパティは基本的にプロパティであるが、その違いは リアクティブな依存関係が変更されたときにのみ再評価されるという違いがあります。これらの依存関係は、ボディ定義内で使用されるプロパティです。例を見てみよう:

  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();
    },
  }

ここで smsInfoの値が変更されるまでキャッシュされる。 bodyの値が変更されるまでキャッシュされる。呼び出されるたびに再評価する必要がある場合は、代わりに methodを使うのがよいでしょう。

Vue.jsコンポーネントを手に入れたら、テストがパスしていることを確認し、最後にアプリケーションのコンポーネントを置き換えた。それで終わりだ!すべてのコードはオープンソースで、GitHubの GitHub.私たちは のコントリビューションにあります!完全な移行をご覧になりたい場合は、対応する プルリクエスト.

私たちは近い将来、すべてのコンポーネントをパッケージとして提供し、皆さんと共有できるようにする予定です!

シェア:

https://a.storyblok.com/f/270183/384x384/d4e395e293/fabianrodiguez.png
Fabian Rodriguezヴォネージの卒業生

ファビアンはVonageのデベロッパー・エクスペリエンス・チームの一員でした。オープンソース、機械学習、コーヒーをこよなく愛する情熱的なソフトウェア・エンジニア。 私たちのドキュメントをより良いものにするために働いていないときは、サイクリング、読書、クラブ・ナシオナル・デ・フットボールの応援をしています。