Vuexからのマイグレーション
なぜですか?
私たちは、ユーザー向けのすべての機能の主要APIとしてGraphQLを定義しているため、GraphQLが存在するときはいつでも、Apolloクライアントもそうであると安全に想定できます。私たちはApolloでVuexを使用したくないので、REST APIからGraphQLに移行するにつれて、VueXのストア数は自然に減少します。
このセクションでは、既存のVueXストアを純粋なVueとApolloに変換するためのガイドラインと方法、またはVueXへの依存を減らす方法を示します。
どのように?
概要
全体として、変更がどの程度複雑になるかを理解したいと思います。グローバルな状態で保存する価値が本当にあるプロパティが数個しかない場合もあれば、すべて純粋なVue
に安全に抽出できる場合もあります。VueX
プロパティは一般に、これらのカテゴリのいずれかに分類されます:
- 静的プロパティ
- リアクティブなミュータブル・プロパティ
- ゲッター
- APIデータ
したがって、最初のステップは、現在のVueXの状態を読み取り、各プロパティのカテゴリを決定することです。
高度なレベルでは、各カテゴリと同等の非VueXコードパターンを対応付けることができます:
- 静的プロパティ:Vue APIからのProvide/Inject。
- リアクティブな変更可能プロパティ:Vueイベントとprops、Apollo Client。
- ゲッター:ユーティリティ関数、Apollo
update
フック、計算されたプロパティ。 - APIデータ:Apolloクライアント。
例を見てみましょう。各セクションでこの状態を参照し、ゆっくりと完全にマイグレーションしていきます:
// state.js AKA our store
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
blobPath,
summaryEndpoint,
suiteEndpoint,
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
errorMessage: null,
limit : 10,
pageInfo: {
page: 1,
perPage: 20,
},
});
静的な値のマイグレーション方法
最も簡単にマイグレーションできるのは、静的な値です:
- クライアント側の定数:クライアント側の定数:静的な値がクライアント側の定数である場合、他の状態プロパティやメソッドから簡単にアクセスできるように、ストアに実装されている可能性があります。しかし、一般的には、このような値を
constants.js
ファイルに追加し、必要なときにインポートする方がよい方法です。 - Rails注入データセット:Vueアプリに提供する必要のある値です。これらは静的なものなので、VueXストアに追加する必要はありません。代わりに、
provide/inject
Vue APIを使用することで、VueXのオーバーヘッドなしに、同等のことを簡単に行うことができます。これは、コンポーネントをマウントする一番上のJSファイル内にのみ注入する必要があります。
上の例を見てみると、2つのプロパティの名前にEndpoint
。これを確認するには、コードベースでこれらのプロパティを検索し、どこで定義されているかを確認します。さらに、blobPath
も静的プロパティです。ここでは少しわかりにくいのですが、pageInfo
は実際には定数です!これは決して変更されず、ゲッターの内部で使用するデフォルト値としてのみ使用されます:
// state.js AKA our store
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
limit
blobPath, // Static - Dataset
summaryEndpoint, // Static - Dataset
suiteEndpoint, // Static - Dataset
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
errorMessage: null,
pageInfo: { // Static - Constant
page: 1, // Static - Constant
perPage: 20, // Static - Constant
},
});
リアクティブなミュータブル値をマイグレーションする方法
これらの値は、多くの異なるコンポーネントで使用される場合に特に有用なので、まず、各プロパティが取得する読み取りと書き込みの回数と、それらの間隔を評価します。読み取り回数が少なく、お互いの距離が近ければ近いほど、これらのプロパティを削除してネイティブの Vue props やイベントを使用するのが簡単になります。
単純な読み取り/書き込み値
例に戻ると、selectedSuiteIndex
は1つのコンポーネントでしか使われておらず、ゲッター内部でも1度しか使われていません。さらに、このゲッター自体も一度しか使用されていません!このロジックをVueに変換するのは非常に簡単です。なぜなら、これはコンポーネントインスタンスのdata
プロパティになるからです。getterには、代わりにcomputedプロパティを使うか、コンポーネントのメソッドで適切なアイテムを返すようにします。これは、VueXストアがアプリケーションを複雑にしている典型的な例です。
幸いなことに、この例ではすべてのプロパティが同じコンポーネントの中にあります。しかし、そうできない場合もあります。このような場合、Vueのイベントとpropsを使用して、兄弟コンポーネント間で通信することができます。状態を知っているはずの親コンポーネントの内部に問題のデータを格納し、子コンポーネントがそのコンポーネントに書き込みたいときは、$emit
、新しい値でイベントを発生させ、親コンポーネントを更新させます。その後、propsをすべての子にカスケードダウンすることで、兄弟コンポーネントのすべてのインスタンスが同じデータを共有します。
特に非常に深いコンポーネントツリーでは、イベントやプロップが面倒に感じることがあります。しかし、これはほとんど不便なイシューであり、アーキテクチャ上の欠陥や修正すべき問題ではないことを認識することが非常に重要です。深くネストされたものであっても、propsを受け渡すことは、コンポーネントをまたがる通信のパターンとして非常に受け入れやすいものです。
読み取り/書き込み値の共有
ストア内のプロパティが複数のコンポーネントで使用され、その読み書きが非常に多かったり離れていたりして、Vueのプロップやイベントが悪い解決策に見えるとします。その代わりに、Apolloクライアントサイドリゾルバを使用します。このセクションではApollo Clientの知識が必要なので、必要に応じてApolloの詳細を確認してください。
まず、VueアプリがVueApollo
を使用するように設定する必要があります。そして、ストアを作成するときに、resolvers
とtypedefs
(後で定義します)をApollo Clientに渡します:
import { resolvers } from "./graphql/settings.js"
import typeDefs from './graphql/typedefs.graphql';
...
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({
resolvers, // To be written soon
{ typeDefs }, // We are going to create this in a sec
}),
});
この例では、フィールドをapp.status
と呼び、@client
ディレクティブを使用するクエリとミューテーションを定義する必要があります。今すぐ作成しましょう:
// get_app_status.query.graphql
query getAppStatus {
app @client {
status
}
}
// update_app_status.mutation.graphql
mutation updateAppStatus($appStatus: String) {
updateAppStatus(appStatus: $appStatus) @client
}
スキーマに存在しないフィールドについては、typeDefs
を設定する必要があります。例えば
// typedefs.graphql
type TestReportApp {
status: String!
}
extend type Query {
app: TestReportApp
}
これで、変異でフィールドを更新できるようにリゾルバを書くことができます:
// settings.js
export const resolvers = {
Mutation: {
// appStatus is the argument to our mutation
updateAppStatus: (_, { appStatus }, { cache }) => {
cache.writeQuery({
query: getAppStatus,
data: {
app: {
__typename: 'TestReportApp',
status: appStatus,
},
},
});
},
}
}
クエリについては、Object
のように動作するので、追加の命令なしで動作します。app { status }
に対するクエリは、app.status
と等価だからです。しかし、(フィールドが持つ最初の値を定義するために)”デフォルト”writeQuery
を書くか、cacheConfig
](graphql.md#local-state-with-apollo) にこのデフォルト値を提供するための[typePolicies
を設定する必要があります。
これで、この値から読み込みたいときは、ローカルクエリを使うことができます。この値を更新する必要があるときは、突然変異を呼び出し、新しい値を引数として渡します。
ネットワーク関連の値
isLoading
やerrorMessage
のように、ネットワークリクエストの状態に関連する値があります。これらは読み書き可能なプロパティですが、後でApollo Client独自の機能で簡単に置き換えることができます:
// state.js AKA our store
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
blobPath, // Static - Dataset
summaryEndpoint, // Static - Dataset
suiteEndpoint, // Static - Dataset
testReports: {},
selectedSuiteIndex: null, // Mutable -> data property
isLoading: false, // Mutable -> tied to network
errorMessage: null, // Mutable -> tied to network
pageInfo: { // Static - Constant
page: 1, // Static - Constant
perPage: 20, // Static - Constant
},
});
ゲッターのマイグレーション方法
ゲッターはケースバイケースでレビューする必要がありますが、一般的なガイドラインとしては、ゲッターの内部で使用していたステート値を引数として受け取り、好きな値を返す純粋なJavaScriptのutil関数を書くことは大いに可能だということです。次のゲッターを考えてみましょう:
// getters.js
export const getSelectedSuite = (state) =>
state.testReports?.test_suites?.[state.selectedSuiteIndex] || {};
ここで行っていることは、関数の引数になる2つのステート値を参照することだけです:
//new_utils.js
export const getSelectedSuite = (testReports, selectedSuiteIndex) =>
testReports?.test_suites?.[selectedSuiteIndex] || {};
この新しいutilは、以前と同じようにインポートして使用することができますが、コンポーネントの内部で直接使用することができます。また、ゲッターの仕様のほとんどは、ロジックが保持されるため、非常に簡単にユーティルに移植できます。
APIデータのマイグレーション方法
最後のプロパティはtestReports
と呼ばれ、API へのaxios
呼び出しによって取得されます。ここでは、純粋な REST アプリケーションであり、GraphQL データはまだ利用できないと仮定します:
// actions.js
export const fetchSummary = ({ state, commit, dispatch }) => {
dispatch('toggleLoading');
return axios
.get(state.summaryEndpoint)
.then(({ data }) => {
commit(types.SET_SUMMARY, data);
})
.catch(() => {
createAlert({
message: s__('TestReports|There was an error fetching the summary.'),
});
})
.finally(() => {
dispatch('toggleLoading');
});
};
ここでは2つのオプションがあります。このアクションが一度しか使用されない場合、このコードをすべてactions.js
ファイルからフェッチを行うコンポーネントに移動することを妨げるものはありません。そうすれば、data
プロパティを優先して、ステートに関連するコードをすべて削除するのは簡単でしょう。その場合、isLoading
もerrorMessages
も、一度しか使用されないので、一緒に生きていくことができます。
この関数を複数回再利用する場合(または再利用する予定がある場合)、Apollo Clientは、ネットワーク呼び出しとキャッシュという、Apollo Clientが最も得意とすることに活用できます。このセクションでは、Apollo Clientの知識と設定方法を知っていることを前提としていますが、GraphQLドキュメントを自由に読んでください。
ローカルGraphQLクエリ(@client
ディレクティブを使用)を使用して、どのようにデータを受け取りたいかを構造化し、クライアント側リゾルバを使用して、Apollo Clientにそのクエリの解決方法を伝えます。ブラウザのネットワークタブでRESTコールを見て、どの構造がユースケースに適しているかを判断できます。この例では、クエリを次のように書くことができます:
query getTestReportSummary($fullPath: ID!, $iid: ID!, endpoint: String!) {
project(fullPath: $fullPath){
id,
pipeline(iid: $iid){
id,
testReportSummary(endpoint: $endpoint) @client {
testSuites{
nodes{
name
totalTime,
# There are more fields here, but they aren't needed for our example
}
}
}
}
}
}
ここでの構造は任意なので、好きなように書くことができます。RESTコールの構造とは異なるため、project.pipeline.testReportSummary
。しかし、クエリの構造をGraphQL
APIに GraphQL
準拠させることで、後でクエリを変更するGraphQL
必要が GraphQL
なくなり、@client
ディレクティブを削除するだけでよくなります。また、同じパイプラインのサマリーを再度取得しようとした場合、Apollo Clientは既にその結果を持っていることを知っているため、無料でキャッシュすることができます!
さらに、フィールドtestReportSummary
にendpoint
引数を渡しています。これは純粋なGraphQL
では必要ありませんが、リゾルバは後でREST
を呼び出すためにこの情報が必要になります。
次に、クライアント側のリゾルバを書く必要があります。フィールドを@client
ディレクティブでマークすると、そのフィールドはサーバーに送信されず、Apollo Clientはその値を解決するために独自のコードを定義することを期待します。Apollo Clientに渡すcacheConfig
オブジェクトの内部に、testReportSummary
のクライアント側リゾルバを記述します。このリゾルバがAxiosを呼び出し、私たちが望むデータ構造を返します。これは、APIデータにアクセスする際やデータ構造を変更する際に常に使用されるゲッターを転送する場所としても最適です:
// graphql_config.js
export const resolvers = {
Query: {
testReportSummary(_, { summaryEndpoint }): {
return axios.get(summaryEndpoint).then(({ data }) => {
return data // we could format/massage our data here instead of using a getter
}
}
}
testReportSummary @client
フィールドを呼び出すたびに、このリゾルバが実行され、オペレーション結果を返します。これは、基本的にVueX
アクションが行ったのと同じジョブです。
GraphQL呼び出しがtestReportSummary
というデータプロパティの内部に格納されていると仮定すると、このクエリを実行するコンポーネントでは、isLoading
をthis.$apollo.queries.testReportSummary.lodaing
に置き換えることができます。エラーは、クエリのerror
フック内部で処理できます。
マイグレーション戦略
データの種類ごとに説明したところで、VueXベースのストアとそうでないストアの間の移行をどのように計画するかレビュアーしてみましょう。私たちはVueXとApolloの共存を避けようとしているので、両方のストアが同じコンテキストで利用できる時間は短いほどよいのです。この重複を最小限に抑えるために、Apolloストアを追加する必要のないストアをすべて削除することからマイグレーションを開始する必要があります。以下の各ポイントは、それ自体がMRになる可能性があります:
-
Rails
データセットとクライアント側の定数の両方、Static値からのマイグレーションを行い、代わりにprovide/inject
、constants.js
ファイルを使用します。 - 単純な読み取り/書き込みオペレーションをどちらかに置き換えてください:
-
data
プロパティとmethods
のどちらかに置き換えてください。 -
props
ローカライズされたコンポーネントグループで共有されている場合は、emits
。
-
- 共有の読み取り/書き込みオペレーションをApollo Client
@client
ディレクティブに置き換えてください。 - 利用可能な場合は実際のGraphQL呼び出し、またはクライアント側リゾルバを使用してREST呼び出しを行うことで、ネットワークデータをApollo Clientに置き換えます。
共有の読み取り/書き込みオペレーションやネットワークデータを迅速に置き換えることが不可能な場合(たとえば、1つか2つのマイルストーンで)、Apollo Clientと排他的に機能する機能フラグの後ろに別のVueコンポーネントを作成し、VueXを使用する現在のコンポーネントの名前をlegacy-
プレフィックスで変更することを検討してください。新しいコンポーネントは、すぐにすべての機能を実装できないかもしれませんが、MRを作成しながら徐々に追加していくことができます。こうすることで、レガシーコンポーネントはストアとしてVueXのみを使用し、新しいコンポーネントはApolloのみを使用することになります。新しいコンポーネントがすべてのロジックを再実装した後、機能フラグをオンにして、期待通りの動作を確認できます。
FAQ
ネットワークに接続せずにグローバルストアが必要な場合はどうすればよいですか?
これは稀なケースであり、次のような疑問が浮かぶはずです:「本当にグローバル・ストアが必要なのでしょうか?(答えはおそらく「いいえ」です!)答えが「はい」であれば、前述のApolloを使った共有読み書き技術を使うことができます。クライアント側の排他的ストアにApollo Clientを使用することは全く問題ありません。
Piniaを使うのですか?
短い答えとしては、わかりません。グローバルストアライブラリを2つ持つことになり、VueXとApollo Clientが共存するのと同じデメリットがあります。Piniaを使うかどうかに関係なく、グローバルストアのサイズを小さくすることはポジティブなことです!
Apolloクライアントは、クライアントディレクティブに対して本当に冗長です。VueXと混ぜて使うことはできますか?
混在は推奨されません。理由はいろいろありますが、コードベースが利用可能なものによってどのように有機的に成長するかを考えてみてください。仮にあなたがネットワークの状態とクライアントサイドの状態を分離することに長けていたとしても、他の開発者は同じこだわりを共有していなかったり、単純に何がどのストアに保存されるかを選択する方法を理解していなかったりするかもしれません。また、時間の経過とともに、VueXストアとApollo Clientの間で通信を行う必要が出てきます。