Vue

Vueを使い始めるには、ドキュメントを読んでください。

使用例

以下のセクションで説明されていることは、これらの例で見つけることができます:

ビューアーキテクチャ

Vue.jsで構築されたすべての新機能は、Fluxアーキテクチャに従わなければなりません。 私たちが達成しようとしている主な目標は、データフローを1つだけにし、データ入力を1つだけにすることです。 この目標を達成するために、私たちはvuexを使用しています。

このアーキテクチャについては、Vueのドキュメントで状態管理や一方通行のデータフローについても読むことができます。

コンポーネントと店舗

イシューボードや環境テーブルのように、Vue.jsで実装されたいくつかの機能では、明確な関係性の分離を見つけることができます:

new_feature
├── components
│   └── component.vue
│   └── ...
├── store
│  └── new_feature_store.js
├── index.js

一貫性を保つため、同じ構成に従うことをお勧めします。

それぞれについて見てみましょう:

index.js ファイル

これは新機能のインデックスファイルです。 ここには新機能のルートVueインスタンスを配置します。

StoreとServiceはこのファイルにインポートして初期化し、メインコンポーネントのpropとして提供する必要があります。

ページ固有のJavaScriptについても必ずお読みください。

ブートストラップの欠点

HAMLからJavaScriptへのデータ提供

Vueアプリケーションをマウントするときに、RailsからJavaScriptにデータを提供する必要があるかもしれません。 そのためには、HTML要素のdata 属性でデータを提供し、アプリケーションをマウントするときにクエリします。

_注意:_マウントされた要素はVueが生成したDOMに置き換えられるため、アプリケーションの初期化中にのみこの操作を行う必要があります。

メインの Vue コンポーネント内部で DOM にクエリを発行する代わりに、render 関数のprops を介して DOM から Vue インスタンスにデータを提供する利点は、ユニットテスト内でフィクスチャや HTML 要素を作成する必要がないため、テストが容易になることです。 次の例を参照してください:

// haml
.js-vue-app{ data: { endpoint: 'foo' }}

// index.js
document.addEventListener('DOMContentLoaded', () => new Vue({
  el: '.js-vue-app',
  data() {
    const dataset = this.$options.el.dataset;
    return {
      endpoint: dataset.endpoint,
    };
  },
  render(createElement) {
    return createElement('my-component', {
      props: {
        endpoint: this.endpoint,
      },
    });
  },
}));

gl オブジェクトへのアクセス

アプリケーションのライフサイクル中に変更されることのないデータをgl オブジェクトに問い合わせる必要がある場合は、DOM に問い合わせるのと同じ場所で行う必要があります。この方法に従うことで、gl オブジェクトをモックする必要がなくなり、テストが容易になります。Vue インスタンスを初期化するときに行う必要があり、データはprops としてメインコンポーネントに提供する必要があります:

document.addEventListener('DOMContentLoaded', () => new Vue({
  el: '.js-vue-app',
  render(createElement) {
    return createElement('my-component', {
      props: {
        username: gon.current_username,
      },
    });
  },
}));

機能フラグへのアクセス

Vueのprovide/injectメカニズムを使用して、Vueアプリケーションの子孫コンポーネントで機能フラグを利用できるようにします。glFeatures オブジェクトはすでにcommons/vue.jsで提供されているので、フラグを利用するにはmixinだけが必要です:

// An arbitrary descendant component

import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';

export default {
  // ...
  mixins: [glFeatureFlagsMixin()],
  // ...
  created() {
    if (this.glFeatures.myFlag) {
      // ...
    }
  },
}

この方法にはいくつかの利点があります:

  • 任意に深くネストされたコンポーネントは、中間コンポーネントに気づかれることなくフラグにアクセスすることができます(propsを介してフラグを渡すなど)。
  • vue-test-utils からmount/shallowMountへ、小道具のように簡単にフラグを提供できるため、テスト性に優れています。

     import { shallowMount } from '@vue/test-utils';
    
     shallowMount(component, {
       provide: {
         glFeatures: { myFlag: true },
       },
     });
    
  • アプリケーションのエントリーポイント以外では、グローバル変数にアクセスする必要はありません。

コンポーネント用のフォルダ

このフォルダには、この新機能に特化したすべてのコンポーネントが格納されています。 おそらく他の場所で使用されるであろうコンポーネントを使用または作成する必要がある場合は、vue_shared/componentsを参照してください。

いつコンポーネントを作成すべきかを知るための良い経験則は、それが他の場所で再利用可能かどうかを考えることです。

例えば、テーブルはGitLab全体でかなり多くの場所で使われているので、コンポーネントにはテーブルが適しているでしょう。 一方、テーブルセルを一つのテーブルだけで使うのは、このパターンの使い方としては適していません。

コンポーネントについて詳しくは、Vue.jsのサイト、ComponentSystemをご覧ください。

ストア用フォルダ

Vuex

詳しくはこちらのページをご覧ください。

VueとjQueryの混合

  • VueとjQueryの混在は推奨されません。
  • Vueで特定のjQueryプラグインを使用する必要がある場合は、その周りのラッパーを作成します。
  • Vueは、jQueryイベントリスナーを使用して、既存のjQueryイベントをリッスンしてもかまいません。
  • VueがjQueryとやりとりするために、新しいjQueryイベントを追加することは推奨されません。

スタイルガイド

Vueコンポーネントやテンプレートを書く際のベストプラクティスについては、スタイルガイドのVueセクションを参照してください。

Vueコンポーネントのテスト

Vueの各コンポーネントには固有の出力があり、この出力はレンダリング関数の中に常に存在します。

Vueコンポーネントの各メソッドを個別にテストすることもできますが、私たちの目標は、常に状態を表すrender/template関数の出力をテストすることです。

このVueコンポーネントのユニットテストの例を示します:

import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import App from '~/todos/app.vue';

const TEST_TODOS = [
  { text: 'Lorem ipsum test text' },
  { text: 'Lorem ipsum 2' },
];
const TEST_NEW_TODO = 'New todo title';
const TEST_TODO_PATH = '/todos';

describe('~/todos/app.vue', () => {
  let wrapper;
  let mock;

  beforeEach(() => {
    // IMPORTANT: Use axios-mock-adapter for stubbing axios API requests
    mock = new MockAdapter(axios);
    mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
    mock.onPost(TEST_TODO_PATH).reply(200);
  });

  afterEach(() => {
    // IMPORTANT: Clean up the component instance and axios mock adapter
    wrapper.destroy();
    wrapper = null;

    mock.restore();
  });

  // NOTE: It is very helpful to separate setting up the component from
  // its collaborators (i.e. Vuex, axios, etc.)
  const createWrapper = (props = {}) => {
    wrapper = shallowMount(App, {
      propsData: {
        path: TEST_TODO_PATH,
        ...props,
      },
    });
  };
  // NOTE: Helper methods greatly help test maintainability and readability.
  const findLoader = () => wrapper.find(GlLoadingIcon);
  const findAddButton = () => wrapper.find('[data-testid="add-button"]');
  const findTextInput = () => wrapper.find('[data-testid="text-input"]');
  const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() }));

  describe('when mounted and loading', () => {
    beforeEach(() => {
      // Create request which will never resolve
      mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
      createWrapper();
    });

    it('should render the loading state', () => {
      expect(findLoader().exists()).toBe(true);
    });
  });

  describe('when todos are loaded', () => {
    beforeEach(() => {
      createWrapper();
      // IMPORTANT: This component fetches data asynchronously on mount, so let's wait for the Vue template to update
      return wrapper.vm.$nextTick();
    });

    it('should not show loading', () => {
      expect(findLoader().exists()).toBe(false);
    });

    it('should render todos', () => {
      expect(findTodoData()).toEqual(TEST_TODOS);
    });

    it('when todo is added, should post new todo', () => {
      findTextInput().vm.$emit('update', TEST_NEW_TODO)
      findAddButton().vm.$emit('click');

      return wrapper.vm.$nextTick()
        .then(() => {
          expect(mock.history.post.map(x => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
        });
    });
  });
});

コンポーネントの出力テスト

Vueコンポーネントの主な戻り値は、レンダリングされた出力です。 コンポーネントをテストするには、レンダリングされた出力をテストする必要があります。Vueのユニットテストのガイドでは、まさにそれを示しています:

イベント

これは、正しいイベントが正しい引数で発生していることを確認するのに便利です。

DOMイベントには、trigger

// Assuming SomeButton renders: <button>Some button</button>
wrapper = mount(SomeButton);

...
it('should fire the click event', () => {
  const btn = wrapper.find('button')

  btn.trigger('click');
  ...
})

Vueのイベントを発生させる必要がある場合は、emit

wrapper = shallowMount(DropdownItem);

...

it('should fire the itemClicked event', () => {
  DropdownItem.vm.$emit('itemClicked');
  ...
})

emitted() メソッドの結果に対してアサートすることで、イベントが発生したことを確認します。

Vue.jsエキスパートのロール

Vue.jsエキスパートになるには、作成したマージリクエストとレビューが表示されたら、MRを開く必要があります:

  • VueとVuexの反応性に関する深い理解
  • VueとVuexのコードは、公式ガイドラインと私たちのガイドラインの両方に従って構造化されています。
  • VueおよびVuexアプリケーションのテストに関する完全な理解
  • Vuexのコードは、文書化されたパターンに従います。
  • 既存のVueおよびVuexアプリケーションと既存の再利用可能なコンポーネントに関する知識

Vue 2 -> Vue 3 への移行

このセクションは、コードベースをVue 2.xからVue 3.xに移行する作業をサポートするために一時的に追加されます。

現在のところ、最終的な移行のための技術的負債を増やさないために、コードベースへの特定の機能の追加は最小限に抑えることを推奨しています:

  • フィルター
  • イベントバス;
  • 機能的テンプレート
  • slot 属性

Vue 3への移行の詳細については、こちらをご覧ください。

付録 - テスト対象の Vue コンポーネント

これは、Vueコンポーネントのテストセクションでテストされるサンプルコンポーネントのテンプレートです:

<template>
  <div class="content">
    <gl-loading-icon v-if="isLoading" />
    <template v-else>
      <div
        v-for="todo in todos"
        :key="todo.id"
        :class="{ 'gl-strike': todo.isDone }"
        data-testid="todo-item"
      >{{ toddo.text }}</div>
      <footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
        <gl-form-input
          type="text"
          v-model="todoText"
          data-testid="text-input"
        >
        <gl-button
          variant="success"
          data-testid="add-button"
          @click="addTodo"
        >Add</gl-button>
      </footer>
    </template>
  </div>
</template>