実況中継シリーズ「Vue.jsで学ぶMVVM 非同期処理 その光と闇」 Bパート

この記事は、2017年3月4日に大阪にて行われたYAPC::Kansaiというカンファレンスで発表した、「Vue.jsで学ぶMVVM 非同期処理 その光と闇」というプレゼンの再現ブログの後編です。前編はこちらですので、前編をまだ読んでない方はぜひそちらからどうぞ。それでははじめます。

Bパート導入

f:id:nkgt_chkonk:20170322104622j:plain

さて、Aパートでは、

  • 無駄にSPAするのはよくない
  • SPAするならGUIのアーキテクチャ・パターンを参考にしよう
  • GUIアプリケーションパータンの中から、MVVMとはどういうパターンなのか

ということについて見てきました。Bパートでは、MVVMを採用したプロダクトでわたしが実際にどのような課題にぶち当たり、どのようにその課題に対応してきたかについて見ていきたいと思います。

認証、セッションまわりの話

f:id:nkgt_chkonk:20170322104624j:plain

まずは、実用的なwebアプリケーションには付き物である認証、セキュリティ周りの話から見ていきます。

f:id:nkgt_chkonk:20170322104625j:plain

スマートフォンなどのGUIアプリケーションならば、だいたいAPIサーバーにtokenを払い出してもらい、そのtokenをセキュアな場所に永続化して使うというのが普通の実装になるかと思います。

f:id:nkgt_chkonk:20170322104626j:plain

一方、ブラウザ上でうごくアプリケーションの場合、CookieとSessionによる認証/認可のプラクティスがすでに存在しています。また、多くのユーザは「ふつうのwebアプリケーション」と「SPA」の違いについてそこまで意識していないでしょう。ログインフォームには「次回からもにログインしたままにする」というチェックボックスが付いているのが普通だし、長らくアクセスしてなかったらセッションが揮発するのが自然だと感じるようにすでに「教育」されています。そういう中で、自前でlocalstrageなどを利用して認証/認可を実装するのは果たして得策でしょうか?これはあくまで私の意見ですが、認証/認可についてはすでに「普通のブラウザアプリケーション」がセキュリティも含めてベストプラクティスが確立しています。ならば、なるべくそのレールに乗ることが安全なのではないでしょうか。

CORS問題

f:id:nkgt_chkonk:20170322104627j:plain

さて、SPAを開発していると、とくにサーバーサイドレンダリングを行わない場合は、「あれ、これ、バックエンドのAPIサーバーはJSON吐くだけだし、静的ファイルをS3とかから配信すれば自前でサーバー用意しなくていいんじゃないんの」という考えが頭をもたげてきます。

ただまあ、なにも考えずにそうしてしまうと、JSON吐いてるバックエンドwebサーバーと静的ファイルを配信してるサーバーのオリジンが異なるということが普通に起こってくるわけです。最初のうちは「静的ファイル配信してるオリジンからのAjaxリクエストを許可すりゃいいんでしょ?余裕だわ」みたいなこと言ってそれで開発しようとしたんですけど、これ、Safariを利用したとたんに破綻するんですね。

f:id:nkgt_chkonk:20170322104628j:plain

というのも、Safariはデフォルトの挙動では他のオリジンからのSet-Cookieヘッダを捨てるという実装になっています。そうするとですね、

  1. ブラウザ側がログインフォームからログインのリクエストをAjaxで送る
  2. Set-Cookieのついたレスポンスが「別オリジンから」返ってくる
  3. 200 OKなので、次の画面を描画し、別のAPIを叩こうとする
  4. Set-Cookieが捨てられているので空Cookieで送ってしまう
  5. ログインした途端に「ログアウトしました」というメッセージが表示され、ログインフォームに戻される

という最高に便利()なアプリケーションが完成してしまうわけです。

とはいえ、まあこれはどちらかというとSafariが偉くて、セキュリティ的なことやプライバシー的なことを考えるとやっぱりSafariみたいに安全側に寄せていくべきなんですよね。

f:id:nkgt_chkonk:20170322104629j:plain

ということを考えると、とくに問題がないならばなるべく同じオリジンから配信するってのが安牌なのではないでしょうか。我々は結局そのようにしました。そのようにしない知見をお持ちの方は是非どこかしらで共有してください。普通にその話めちゃめちゃ聞きたいです。

CSRF問題

f:id:nkgt_chkonk:20170322104630j:plain

さて、普通にCookie認証でやる、となると、普通にCSRF対策をしないといけませんね。でもこれ特に言うことなくて、普通に普通のCSRF対策をすればいいと思います。

f:id:nkgt_chkonk:20170322104631j:plain

このあたりはほんとに「普通にやる」ってことがすごい大事だと思っていて、普通にやれば、API側はweb application frameworkの恩恵も受けられるし、とにかくブラウザの仕組み、HTTPの仕組みに乗っかっていくというのが大切だと思います。

非同期処理との戦い

f:id:nkgt_chkonk:20170322104632j:plain

つづいては非同期処理との戦いについて見ていきたいと思います。

f:id:nkgt_chkonk:20170322104633j:plain

やりがちな失敗として、プレゼンテーションレイヤーのコンポーネントの分割と同じようにモデルを分割しちゃって、なおかつそれぞれが勝手気ままにモデルを呼び出したりするみたいな失敗があると思うんですね。ちょっとこれ例を出さないとわかりにくいと思うので、例をだしましょう。

f:id:nkgt_chkonk:20170322104634j:plain

たとえばこういうアプリケーションを考えてみましょう。ページ上部にグローバルナビゲーションがあって、その下にはフラッシュメッセージが出て来るような領域があります。で、その下がメインで、左側にユーザ基本情報。右側には日毎の情報が並んでるダッシュボードみたいなものがある画面を考えてみます。

f:id:nkgt_chkonk:20170322104635j:plain

グローバルナビゲーションとフラッシュメッセージは常に表示しているので、ページ遷移してもここは基本的に書き換わらないとします。

一方、メインの領域はURLが変われば書き換わります。

f:id:nkgt_chkonk:20170322104636j:plain

また、日別ダッシュボードなので、日付を変えればユーザ基本情報の部分は書きかわらないけれど、ダッシュボードの部分だけ書き換わります。

ViewModelの設計

f:id:nkgt_chkonk:20170322104637j:plain

こういう画面を作るなら、わたしならこういうふうにViewModel(というかコンポーネント)を分割します。

まずRootとなるVMが、常に表示するグローバルナビゲーションと、フラッシュメッセージを表示するためのアラートのVMを持ちます。その下に、RouterとなるViewModelを保持し、こいつがURLに応じてダッシュボードのVMを読み込んだり、別のVMを読み込んだりします。

f:id:nkgt_chkonk:20170322104638j:plain

こうすることで、たとえば/dashboard/:dateにアクセスしたら、ルーターがダッシュボードのVMを読み込み、表示します。

f:id:nkgt_chkonk:20170322104639j:plain

また、日付を変更した場合は、dashboard VM が daily info VM の日付を変更し、その日の情報をロードします。

Modelの設計(駄目なやつ)

さて、つぎは、やりがちなModelの失敗設計です。

f:id:nkgt_chkonk:20170322104640j:plain

それぞれのViewModelが、それぞれのコンポーネントが関心を持つModelを持ち、その向こうにAPIサーバーがあります。

f:id:nkgt_chkonk:20170322104641j:plain

これで、たとえば、/dashboard/:dateにアクセスがあった場合、user info ViewModelはUserモデルの「loadUser」かなんかをdispatchし、daily info ViewModelも「loadDashboardOf(:date)」みたいなメソッドをdispatchします。

f:id:nkgt_chkonk:20170322104642j:plain

すると、ModelたちはAPIと通信し、

f:id:nkgt_chkonk:20170322104643j:plain

その情報を自らの状態に書き戻します。場合によってはエラーが起こった旨を伝えるために、Alertモデルを書き換えたりするでしょう。

f:id:nkgt_chkonk:20170322104644j:plain

それぞれのViewModelは、それぞれのModelの変更を監視しているので、これで無事画面が書き換わります。

:dateの部分が変わった場合は、

f:id:nkgt_chkonk:20170322104645j:plain f:id:nkgt_chkonk:20170322104646j:plain f:id:nkgt_chkonk:20170322104647j:plain f:id:nkgt_chkonk:20170322104648j:plain

daily info VMだけで同じことが起こります。

f:id:nkgt_chkonk:20170322104649j:plain

一見すると結構よさそうな設計に見えますね。

f:id:nkgt_chkonk:20170322104650j:plain

しかし、たとえば、/dashboard/:dateにアクセスしたタイミングでセッションが切れた場合はどうでしょうか。

f:id:nkgt_chkonk:20170322104651j:plain

同じようにuser info VMとdaily info VMがUser ModelとDailyInfoModelを叩きます。そして、それぞれのモデルが独立に非同期リクエストをサーバーに対して送るでしょう。その結果、サーバーはそれぞれのモデルに対して独立に「セッションが切れましたよ」というエラーを返すでしょう。

f:id:nkgt_chkonk:20170322104652j:plain

そして、セッションが切れたのであれば。「ログインしてるアカウント」の情報を保持してる「CurrentAccount」みたいなモデルを適切にログアウトさせてあげないといけません(そうすると、この「CurrentAccountがログアウトした」というイベントを監視してるVMがログインページへ遷移させるなどの操作をするでしょう)。

しかし、今回は独立に2回もエラーが返ってきているので、なんとCurrentAccountモデルは二回もlogoutメソッドを呼ばれてしまいました!

f:id:nkgt_chkonk:20170322104653j:plain

OOPS!レースコンディションです!今回のレースコンディションなら、CurrentAccountモデルが「ログアウトしてる状態でlogoutメソッド呼ばれてもなにもしない」くらいの対応でなんとかなるかもしれませんが、もしこれが「呼ばれる順番に意味がある」みたいなメソッドだったら、事態は一層深刻です。

f:id:nkgt_chkonk:20170322104654j:plain

と、こんな感じで、複数のモデルを好き勝手VMから叩いてしまうと、非同期処理でレースコンディションを起こしがちという問題があります。これに対応するためには、「窓口」をきちんと作ってあげるのが有効です。

f:id:nkgt_chkonk:20170322104655j:plain

各ViewModelが、関心のあるモデルの変化を監視しているというのには違いがありませんが、「usecase」という層をひとつ設けてあります。

f:id:nkgt_chkonk:20170322104656j:plain f:id:nkgt_chkonk:20170322104657j:plain

ダッシュボードにアクセスすると、VMはこのusecaseを叩きます。

f:id:nkgt_chkonk:20170322104658j:plain

叩かれたusecaseは、APIと通信をします。

f:id:nkgt_chkonk:20170322104659j:plain

その結果に応じ、各モデルのメソッドを呼び、モデル内に変化を起こします。すると、各モデルの変化を監視しているViewModelは、各モデルから新しい値を読み出し、Viewに反映する、という流れです。

f:id:nkgt_chkonk:20170322104700j:plain

仮に、セッションが切れた場合はどうでしょうか。

f:id:nkgt_chkonk:20170322104701j:plain

さきほどまでは独立にモデルが好き勝手非同期処理を行っていましたが、今回はusecaseが「非同期処理の待ち合わせポイント」になっています。

f:id:nkgt_chkonk:20170322104702j:plain

そのため、ふたつのレスポンスをまってもよし、いっこめでエラーが返ってきた時点で次のPromiseの値は捨てちゃって、すぐにログアウトするもよし、という感じで、非同期処理によるレースコンディションに立ち向かうことができました。

f:id:nkgt_chkonk:20170322104703j:plain

なにが言いたいのかというと、つまり、こういうことです。

Bパートまとめ

f:id:nkgt_chkonk:20170322104704j:plain

さて、Bパートのまとめです。

ブラウザを作ってるひとたちや、webの仕組みを作ってるひとたちはわたしたちのアプリケーションを作る以上のとてつもない労力と知力をつぎ込んでそれらを作っています。そもそも規模が違う。ならば、なるべくそのレールに乗る、というのが、SPAを作る上でも重要なのではないでしょうか。

画面遷移するたびに同期的に画面を全部書き換える牧歌的な時代は過ぎ、われわれは非同期処理でばしばし画面の一部を書き換える時代に生きています。そういうときに、プレゼンテーションレイヤーに非同期処理の複雑さが漏れ出してしまわないように、ドメインのレイヤーにきちんと非同期処理の「待ち合わせポイント」「管理ポイント」を設けてあげて、非同期処理の複雑さに立ち向かいましょう。

f:id:nkgt_chkonk:20170322104705j:plain

というわけで、Bパート完です。エンディングテーマを挟んでの、Cパートとして、全体のまとめです。

f:id:nkgt_chkonk:20170322104706j:plain

f:id:nkgt_chkonk:20170322104707j:plain

はい、とくにこれ以上に言うことはありません。

f:id:nkgt_chkonk:20170322104708j:plain

このプレゼンは、株式会社メディロムの提供でお送りいたしました(交通費とか宿泊費とか)。株式会社メディロムでは一緒にはたらくメンバーを募集しています。ぜひお声がけください。