Vuex

Vuexは、もはやストア管理の優先パスとはみなされず、現在はレガシーフェーズにあります。つまり、既存のストアに追加することは可能ですが、Vuex 時間の経過とともにストアのサイズを縮小し、最終的にはVueXから完全にマイグレーションVuexすることをVuex 強くお勧め Vuexします。アプリケーションにVuex 新しい Vuexストアを追加するVuex 前に Vuex、まず、追加する予定のVue アプリケーションがApollo.Vuex使用していないことを確認してApollo ください Apollo。ベースとなるアプリケーションのApollo 構築方法に関するガイドラインについては ApolloApollo GraphQLのドキュメントをお読み Apolloください。

このページに含まれる情報は、Vuexの公式ドキュメントで詳しく説明されています。

関心の分離

Vuexは、ステート、ゲッター、ミューテーション、アクション、モジュールで構成されています。

ユーザーがアクションを選択すると、dispatch 。このアクションはcommits ステートを変更するミューテーションです。アクション自体はステートを更新しません。ステートを更新するのはミューテーションだけです。

ファイルの構造

GitLabでVuexを使用する場合は、可読性を向上させるために、これらの懸念を別のファイルに分割します:

└── store
  ├── index.js          # where we assemble modules and export the store
  ├── actions.js        # actions
  ├── mutations.js      # mutations
  ├── getters.js        # getters
  ├── state.js          # state
  └── mutation_types.js # mutation types

次の例は、ユーザーをリストアップしてステートに追加するアプリケーションです。(より複雑な実装例については、このリポジトリに保存されているセキュリティアプリケーションをレビューしてください)。

index.js

これがストアのエントリポイントです。以下を参考にしてください:

// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';

export const createStore = () =>
  new Vuex.Store({
    actions,
    getters,
    mutations,
    state,
  });

state.js

コードを書く前に最初にすべきことは、状態を設計することです。

多くの場合、HAMLからVueアプリケーションにデータを提供する必要があります。アクセスしやすくするために、それをステートに格納しましょう。

  export default () => ({
    endpoint: null,

    isLoading: false,
    error: null,

    isAddingUser: false,
    errorAddingUser: false,

    users: [],
  });

state プロパティへのアクセス

mapState を使用して、コンポーネントの状態プロパティにアクセスできます。

actions.js

アクションは、アプリケーションからストアにデータを送信するための情報のペイロードです。

アクションは通常、typepayload で構成され、何が起こったかを記述します。ミューテーションと違って、アクションは非同期オペレーションを含むことができます。

このファイルでは、ユーザーのリストを処理するためにミューテーションを呼び出すアクションを記述します:

  import * as types from './mutation_types';
  import axios from '~/lib/utils/axios_utils';
  import { createAlert } from '~/alert';

  export const fetchUsers = ({ state, dispatch }) => {
    commit(types.REQUEST_USERS);

    axios.get(state.endpoint)
      .then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data))
      .catch((error) => {
        commit(types.RECEIVE_USERS_ERROR, error)
        createAlert({ message: 'There was an error' })
      });
  }

  export const addUser = ({ state, dispatch }, user) => {
    commit(types.REQUEST_ADD_USER);

    axios.post(state.endpoint, user)
      .then(({ data }) => commit(types.RECEIVE_ADD_USER_SUCCESS, data))
      .catch((error) => commit(types.REQUEST_ADD_USER_ERROR, error));
  }

アクションのディスパッチ

コンポーネントからアクションをディスパッチするには、mapActions ヘルパーを使います:

import { mapActions } from 'vuex';

{
  methods: {
    ...mapActions([
      'addUser',
    ]),
    onClickUser(user) {
      this.addUser(user);
    },
  },
};

mutations.js

ミューテーションは、ストアに送信されたアクションに応じてアプリケーションの状態がどのように変化するかを指定します。Vuexストアの状態を変更する唯一の方法は、変異をコミットすることです。

ほとんどの変異は、commit を使用してアクションからコミットされます。非同期オペレーションがない場合は、mapMutations ヘルパーを使用してコンポーネントから変異を呼び出すことができます。

コンポーネントから変異をコミットする例については、Vuexのドキュメントを参照してください。

命名パターン:REQUESTRECEIVE 名前空間

リクエストがなされたとき、ローディング状態をユーザーに見せたいことがよくあります。

ロード状態をトグルする変異を作成する代わりに、次のようにします:

  1. REQUEST_SOMETHING 、ローディング状態を切り替えるための突然変異。
  2. 成功コールバックを処理するための、RECEIVE_SOMETHING_SUCCESS 型の変異。
  3. エラーコールバックを処理するための、RECEIVE_SOMETHING_ERROR 型の変異。
  4. アクションfetchSomething リクエストを行い、言及されたケースで変異をコミットするためのもの。
    1. アプリケーションがGET 以上のリクエストを行う場合、これらを例として使うことができます:
      • POST:createSomething
      • PUT:updateSomething
      • DELETE:deleteSomething

その結果、fetchNamespace アクションをコンポーネントからディスパッチすることができ、REQUEST_NAMESPACERECEIVE_NAMESPACE_SUCCESSRECEIVE_NAMESPACE_ERROR 変異をコミットする役割を担います。

以前は、変異をコミットする代わりにfetchNamespace アクションからアクションをディスパッチしていたので、コードベースの古い部分で異なるパターンを見つけても混乱しないでください。しかし、新しいVuexストアを書くときは、新しいパターンを活用することをお勧めします。

このパターンに従うことで、以下のことが保証されます:

  1. すべてのアプリケーションが同じパターンに従うことで、誰でも簡単にコードをメンテナーすることができます。
  2. アプリケーション内のデータはすべて同じライフサイクルパターンに従います。
  3. ユニットテストはより簡単です。

複雑な状態の更新

特に状態が複雑な場合、変異が更新する必要があるものを正確に更新するために状態を走査するのが本当に難しいことがあります。理想的には、vuex の状態はできるだけ正規化/非連結化されていることが望ましいのですが、必ずしもそうとは限りません。

portion of the mutated state 、変異そのもので選択され、変異された場合、コードはずっと読みやすく、メンテナーしやすくなることを覚えておくことが重要です。

この状態を考えると

   export default () => ({
    items: [
      {
        id: 1,
        name: 'my_issue',
        closed: false,
      },
      {
        id: 2,
        name: 'another_issue',
        closed: false,
      }
    ]
});

突然変異をこう書きたくなるかもしれません:

// Bad
export default {
  [types.MARK_AS_CLOSED](state, item) {
    Object.assign(item, {closed: true})
  }
}

この方法はうまくいきますが、いくつかの依存関係があります:

  • コンポーネント/アクション内のitem の正しい選択。
  • item プロパティはclosed の状態ですでに宣言されています。
    • 新しいconfidential プロパティは反応的ではありません。
  • itemitems によって参照されていることに注意してください。

このように書かれた変異はメンテナーが難しく、エラーが起こりやすい。むしろこのように書くべきです:

// Good
export default {
  [types.MARK_AS_CLOSED](state, itemId) {
    const item = state.items.find(x => x.id === itemId);

    if (!item) {
      return;
    }

    Vue.set(item, 'closed', true);
  },
};

このアプローチの方が優れているのは

  • 変異の中で状態を選択し更新するので、よりメンテナーです。
  • 内部依存性がなく、正しいitemId が渡されれば、状態は正しく更新されます。
  • 初期状態との結合を避けるために新しいitem

このように書かれた突然変異はメンテナーが簡単です。さらに、反応性システムの制限によるエラーを避けることができます。

getters.js

特定のプロップに対するフィルタリングのように、ストアの状態に基づいて派生した状態を取得する必要があることがあります。gettersゲッターを使用すると、計算されたプロップがどのように動作するかによって、依存関係に基づいて結果をキャッシュすることもできます:

// get all the users with pets
export const getUsersWithPets = (state, getters) => {
  return state.users.filter(user => user.pet !== undefined);
};

コンポーネントからゲッターにアクセスするには、mapGetters ヘルパーを使います:

import { mapGetters } from 'vuex';

{
  computed: {
    ...mapGetters([
      'getUsersWithPets',
    ]),
  },
};

mutation_types.js

Vuexの変異に関するドキュメントより: > 様々なFluxの実装において、変異の型に定数を使用するのはよく見られるパターンです。これにより、コードはリンターのようなツールを利用することができます。また、すべての定数を1つのファイルにまとめることで、共同作業者はアプリケーション全体でどのような変異が可能かを一目で把握することができます。

export const ADD_USER = 'ADD_USER';

ストアの状態の初期化

Vuexストアは、actionを使用する前に初期状態を必要とすることがよくあります。多くの場合、これにはAPIエンドポイント、ドキュメントURL、IDなどのデータが含まれます。

この初期状態を設定するには、Vueコンポーネントをマウントするときに、ストアの作成関数にパラメータとして渡します:

// in the Vue app's initialization script (for example, mount_show.js)

import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { createStore } from './stores';
import AwesomeVueApp from './components/awesome_vue_app.vue'

Vue.use(Vuex);

export default () => {
  const el = document.getElementById('js-awesome-vue-app');

  return new Vue({
    el,
    name: 'AwesomeVueRoot',
    store: createStore(el.dataset),
    render: h => h(AwesomeVueApp)
  });
};

ストア関数は、このデータをステートの作成関数に渡すことができます:

// in store/index.js

import * as actions from './actions';
import mutations from './mutations';
import createState from './state';

export default initialState => ({
  actions,
  mutations,
  state: createState(initialState),
});

そして、state関数はこの初期データをパラメータとして受け取り、state オブジェクトに焼き付けることができます:

// in store/state.js

export default ({
  projectId,
  documentationPath,
  anOptionalProperty = true
}) => ({
  projectId,
  documentationPath,
  anOptionalProperty,

  // other state properties here
});

なぜ…初期状態を拡散しないのですか?

勘のいい読者なら、上の例から数行のコードをカットするチャンスだと気づくでしょう:

// Don't do this!

export default initialState => ({
  ...initialState,

  // other state properties here
});

私たちは、フロントエンドのコードベースを発見し、検索する能力を向上させるために、このパターンを避けることを意識的に決定しました。Vueアプリにデータを提供する場合も同様です。その理由は、このディスカッションで説明します:

someStateKey 、ストアの状態で使用されていると考えてください。もしそれがel.dataset によってのみ提供されていた場合、それを直接grepすることは_できないかも_しれません。some_state_keyRailsテンプレートから来た可能性があるから some_state_keyです。some_state_key逆もまた真なりで、Rails some_state_keyテンプレートを見ていると、 , が何にsome_state_key使われているのか気になるかも some_state_keyしれませんが、someStateKeyをgrep_しなければ_なりません。

ストアとの通信

<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';

export default {
  computed: {
    ...mapGetters([
      'getUsersWithPets'
    ]),
    ...mapState([
      'isLoading',
      'users',
      'error',
    ]),
  },
  methods: {
    ...mapActions([
      'fetchUsers',
      'addUser',
    ]),
    onClickAddUser(data) {
      this.addUser(data);
    }
  },
  created() {
    this.fetchUsers()
  }
}
</script>
<template>
  <ul>
    <li v-if="isLoading">
      Loading...
    </li>
    <li v-else-if="error">
      {{ error }}
    </li>
    <template v-else>
      <li
        v-for="user in users"
        :key="user.id"
      >
        {{ user }}
      </li>
    </template>
  </ul>
</template>

Vuexのテスト

Vuexの懸念事項のテスト

アクション、ゲッター、ミューテーションのテストについては、Vuexのドキュメントを参照してください。

ストアが必要なコンポーネントのテスト

小さなコンポーネントは、store プロパティを使ってデータにアクセスするかもしれません。そのようなコンポーネントのユニットテストを書くには、ストアを含めて正しい状態を提供する必要があります:

//component_spec.js
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { createStore } from './store';
import Component from './component.vue'

Vue.use(Vuex);

describe('component', () => {
  let store;
  let wrapper;

  const createComponent = () => {
    store = createStore();

    wrapper = mount(Component, {
      store,
    });
  };

  beforeEach(() => {
    createComponent();
  });

  it('should show a user', async () => {
    const user = {
      name: 'Foo',
      age: '30',
    };

    // populate the store
    await store.dispatch('addUser', user);

    expect(wrapper.text()).toContain(user.name);
  });
});

テストファイルによっては、@vue/test-utilslocalVue.use(Vuex)createLocalVue 関数 をまだ使っているものがあります。これは不要なので、可能な限り避けるか削除してください。

双方向データバインディング

Vuexでフォームデータを保存するとき、保存されている値を更新する必要があることがあります。ストアは決して直接変更されるべきではなく、代わりにアクションを使用する必要があります。私たちのコードでv-model を使用するには、このフォームに計算プロパティを作成する必要があります:

export default {
  computed: {
    someValue: {
      get() {
        return this.$store.state.someValue;
      },
      set(value) {
        this.$store.dispatch("setSomeValue", value);
      }
    }
  }
};

別の方法として、mapStatemapActions を使用することもできます:

export default {
  computed: {
    ...mapState(['someValue']),
    localSomeValue: {
      get() {
        return this.someValue;
      },
      set(value) {
        this.setSomeValue(value)
      }
    }
  },
  methods: {
    ...mapActions(['setSomeValue'])
  }
};

これらのプロパティをいくつか追加するのは面倒になり、書くテストが増えてコードの繰り返しが多くなります。これを簡単にするために、~/vuex_shared/bindings.js にヘルパーがあります。

このヘルパーは、次のように使用します:

// this store is non-functional and only used to give context to the example
export default {
  state: {
    baz: '',
    bar: '',
    foo: ''
  },
  actions: {
    updateBar() {...},
    updateAll() {...},
  },
  getters: {
    getFoo() {...},
  }
}
import { mapComputed } from '~/vuex_shared/bindings'
export default {
  computed: {
    /**
     * @param {(string[]|Object[])} list - list of string matching state keys or list objects
     * @param {string} list[].key - the key matching the key present in the vuex state
     * @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
     * @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
     * @param {string} defaultUpdateFn - the default function to dispatch
     * @param {string|function} root - optional key of the state where to search for they keys described in list
     * @returns {Object} a dictionary with all the computed properties generated
    */
    ...mapComputed(
      [
        'baz',
        { key: 'bar', updateFn: 'updateBar' },
        { key: 'foo', getter: 'getFoo' },
      ],
      'updateAll',
    ),
  }
}

mapComputed ヘルパーは次のように使うことができます: ストアからデータを取得し、更新されたときに正しいアクションをディスパッチする適切なコンピューテッドプロパティを生成します。

キーのroot が1階層以上深いイベントの場合は、関数を使用して関連するステートオブジェクトを取得できます。

例えば、以下のようなストアです:

// this store is non-functional and only used to give context to the example
export default {
  state: {
    foo: {
      qux: {
        baz: '',
        bar: '',
        foo: '',
      },
    },
  },
  actions: {
    updateBar() {...},
    updateAll() {...},
  },
  getters: {
    getFoo() {...},
  }
}

root

import { mapComputed } from '~/vuex_shared/bindings'
export default {
  computed: {
    ...mapComputed(
      [
        'baz',
        { key: 'bar', updateFn: 'updateBar' },
        { key: 'foo', getter: 'getFoo' },
      ],
      'updateAll',
      (state) => state.foo.qux,
    ),
  }
}