とあるところで、Laravel + Vue.js で社内ツールを作ることになりました…と仮定しましょう。自分としては WPF とかで作ったほうがノウハウがあるので手軽に作れるのですが、先方の要望といういうか、既にあるツールが Vue.js の electron で作ってあるのと、サーバーサイドが Larevel で Web API で作ってあるのでそれに準じましょうという具合です。
ちなみに、私としては Vue.js は半年前に始めたばかりで、それも Laravel の View 上に Vue.js のスクリプトを読み込ませる方式で作っていました。なので、プログラミングの作法としては、Laravel 寄り(PHP寄り)になるので、いまいち Vue.js は判らないのです。JavaScript が苦手というのもありますが。
漠然と Vue.js オンリーで作っている場合は、適当な data を作ってため込むという VB や Windows フォームな作りで十分な訳ですが、ある程度の規模が大きくなると(どの位が「ある程度」というのか疑問はありますが、10画面ぐらいあれば「ある程度」と言えるでしょう)、なんらかの形でデータ層を用意しておいたほうがベターです。
そのような場合、Windows のデスクトップならば WPF で作る、Android や iOS アプリならば Xamarin.Forms を使うという形で MVVM パターンを使うのが常です。View 側に XAML を使って、ロジックに ViewModel を使う。データベースのアクセスなどに Model クラスや Entity Framework を使えばよいので、(自分の中では)かなりノウハウが蓄積しています。Prism を使うという手もあるのですが、私の場合それほど複雑な形にしたくないので毎度 INotifyPropertyChanged を継承したクラスを自作(実際には使いまわし)しています。ICommand を使わないからこれで十分なのです。
MVVM パターンとは何か?
WPF や Xmarin.Forms や、今や瀕死状態の UWP(何処にいちゃったんでしょうね?.NET5 の UI ラインナップにも無かった気が…)でおなじみの MVVM パターンです。実は、Laravel や CakePHP や、以前の mac のように MVC パターンも一緒に覚えておくといいです。Objective-C で mac アプリを使うときは MVC パターンだし。でも今は、Swift か。
画面の View は、大抵の場合は画面専用のマークアップ言語で書きます。WPF であれば XAML、XAML 以外に MVVM パターンの View があるのか?と言えば、探せばあるんじゃないだろうか?な感じですが、MVVM パターンとしては、View 自体は XAML に限るという訳ではありません。ただ、現状の実装としては XAML のようなスタイルにしないと(INotifyPropertyChangedの連携があるので)、うまく実装できないというところです。
Model は、いわゆる値クラスでもあり、データベースから取ってきた Entitiy Framework でもあり、データを保持するところです。
この View と Model の間を取り持つのが ViewModel クラスな訳ですが、一般的な Microsoft 社のサンプルコードだと、ひとつの View に対してひとつの ViewModel が律儀に割り付けられていますが、そうしないといけない訳ではありません。見通し的には、1対1対応のほうが判りやすいという面もあるのですが、共通なデータを持たせる場合(特に画面間でのデータが共通である場合)View – ViewModel の対応が冗長になってしまうので、ひとつの ViewModel に複数の View を持たせるという、Document-View 方式でも良いわけです。
MVVM パターンも MVC パターンも使われ始めた頃(20年前位?)はいろいろと学習しないと訳が分からない感じでしたが(オブジェクト指向とかオブジェクト脳とかUMLとか諸々の波もあったので)、いまとなっては一般的なアプリケーションの View と Model をうまく分離&組み合わえるパターンのひとつです。私的には「MV* パターン」とまとめて呼ぶこともあります。
Vuex とは何か?
Vue.js でコンポーネントを作るときに v-model というのがあるので、MVVM パターンでええやん!と思うわけですが、Vuex というものを使います。正確には Flux アーキテクチャを使って Vue.js に作られた Vuex を使います。
データの取り回しとか、Vue.js であれこれやるのに精いっぱいなのに(初学者的には!)、更に Vuex の方式を覚えないといけないという混乱状態です。実は Vue Router も思えないといけないのと、Vuetify も使わないといけない。
自分としてはできるだけ既知のもの、あるいは「これはいけそう」と思えるものを使いたいので、既に MVC パターンや MVVM パターンがあるところに Vuex という発想は必要なのか?という疑問がでてきます。
Vuex ではおなじみのこのぐるぐるの図ですが、遷移が一方向にしか回らないので理解が簡単!ということなのですが、本当なのでしょうか? MVVM パターンのほうが相互にやり取りする(two way方式)ので、簡単ではないでしょうか?
それにいちいち、dispatch や commit を呼び出すというのはどういうことなのか?メソッド呼び出しでいいんじゃないだろうか?という不信が湧き出るわけです(今でも、ちょっと湧き出るけど)。
でもって、google で「Vuex 使わない」で検索すると、ぽろぽろと出てきますね。疑似的に MVVM パターに則ったライブラリを自作してもよいのですが、Vuex は公式から出ているということで、それなりの理由があるのでしょう。では「それなりの理由」とは何ぞや?というところから、理解したいし、それをした上で使うのか使わないのかを判断したいわけです。
シーケンス図で理解する Vuex
実は、さっきのぐるぐるの図には、ひとつ大きな見落としがあります。
主に Actions からの呼び出しは Web API に対して非同期に行われています。
デスクトップアプリや、サーバーサイドでのデータベースアクセスは、基本は同期的に行われています。現在の C# だとファイルアクセスするときに async/await を使い「非同期」として呼び出すことができますが、本質的に await で処理待ちをしていれば同期的に操作をしているし、通常の場合は印刷や重たい画像処理以外は同期的に書いていてもアプリケーションのレスポンスはCPUのスピードに関係するところが多いので、それほど問題にはなりません。
しかし、Vue.js のように、基本何かのデータを保存したり読み込んだりするのに常に Web APIを呼び出す必要があるときには、何らかの処理のほとんどが非同期的におこなわれます。実装としては、axios クラスを使って処理待ちの then を使うわけです。
なので、コンポーネント(View)でユーザーがボタンクリックなどのアクションを起した後に、Web APIを呼び出して、応答されたデータがリストに表示されるまでにはそれなりの時間が掛かり、この部分はCPU的には連続ではないのです。
Vuex の解説で使われるステートチャート(遷移図)だけでは、時系列が判りづらいので、具体的にシーケンス図で書いたのがこれです。
画面を表示したときの mounted のときに、Web API の api/Rooms を呼び出して、画面に会議室の一覧を表示する画面を想定したものです。
- コンポーネントから dispatch する
- actions から Web API を axios で呼び出す。
- レスポンスは、非同期になり Promise を使う。
- ここでいったん画面に制御が戻る
- レスポンスが返ってきたときに、結果を commit する
- 書き込み専用の mutations で state に書き込む
- 読み込み専用の getters から stete の変更がコンポーネントに通知される
- コンポーネント側の rooms が更新されて、画面にリストが表示される。
という具合です。なるほど、dispatch が必ず Promise を返す=非同期である、というのがここに意味があります。内部で、Web API を呼び出さない同期的な処理であっても、Promise を返すので、画面のコンポーネント側では常に「非同期呼び出し」として処理が統一できます。
つまり、内部動作が非同期処理か同期処理かを区別せずに、(乱暴ですが)すべて非同期処理とみなす、という方法を Vuex は取っています。
同じものを MVVM パターンで書いてみましょう。画面を表示させたときに、Web API を呼び出してリストの初期状態を取得する、というパターンはよくあります。
View がロードしたときに ViewModel の呼び出し自体は同期的に行われます。しかし、ViewModel 内では、Web API を呼び出す(HttpClientなど)ので、これの戻り値の取得は非同期になります。
実際は Task オブジェクトが返されるのですが、コード上では実行待ちのように記述したいので await を使います。
Web API からレスポンスが返ってきたときは、Model クラスに同期的に書き込みが行われます。Model クラスの変更に伴い、ViewModel が View に対して OnProertyChange イベントを発生させます。このイベントを受けて View の該当箇所が更新されます。
シーケンス図を書く Vuex と MVVM パターンの類似点がよくわかります。
少なくとも「非同期で Web API を呼び出す必要がある」場合には、Vuex も MVVM もまったく同じシーケンス図になります。
違いとしては、
- Vuex の場合は、Web API が頻繁に呼び出されるので、非同期処理が前提になっている
- MVVM の場合は、Model の変更を View に伝える、同期的な通知が主になっている
ということでしょう。
Vuex で複数の Web API を呼び出す
View 側のアクションで、複数の Web API を呼び出すパターンを考えてみましょう。さきの Vuex と MVVM パターンの類似点に注目すれば、非同期的に Web API を呼び出す場所は、Actions の中になります。
この例では、api/Rooms と api/Price の2つの Web API を呼び出しています。画面では rooms と prices を独立で表示させているわけですが、画面からは1回の GetRooms を dispatch で呼び出します。その中の Actions の中で2回の Web APIが呼び出されるわけです。
View 側の次のアクションとしては、2つの Web APIが戻ってきた後(あるいは、呼び出している途中)に何かをやりたいわけですから、Actions の中で axios の await か then を使ったほうが都合がよいわけです。
もうひとつ、データを更新した上で、もう一度リストを更新させるという update と getlist の2つを順番で呼び出すパターンを考えてみましょう。
順序的には、Web API で update の API を呼び出した後に、リストの更新をしないと駄目です。何らかの原因でupdate に失敗したときはリストを更新しても意味は無いし、update が完全に終わるまでリストを取得しても意味ありません。サーバー側の update 処理完了まえにリストの取得をしても意味がありませんからね。
本来ならば、サーバー側でトランザクションを取る方法も多いのですが、呼び出し側で順番を守って Web API を呼び出しても ok な訳です。この「順番を守って呼び出す」部分を Actions 側に記述しておけば、View 側の手違いがなくなります。
Vuex はルールを守って使う
MVVMer な私としては、Vuex は「ルールを守って使えば ok」という結論です。MVVM パターンであっても、ViewModel 内で頻繁に HttpClient を非同期で呼び出すならば、同じようなシーケンスになるのならば、Vuex のスタイルに合わせたほうが無難です。
actions:
store.commit('project/projects', items )
getters:
store.getters['project/projects']
dispatch:
store.dispatch('project/projects')
mutatins:
projects( state, payload ) {
state.projects = payload.projects
}
state:
projects: []
ただ、なんといいますか、commit や dispatch を呼び出すときに、メソッド名(タイプとも言う)を文字列になっているのはなんとかならなかったんでしょうか。ここは慣れそうにないですが、まあ、適度に map を使うということで。