第52回NDS(長岡開発者勉強会)で「怖くないし役に立つ設計原則の話」を発表してきました

新潟県長岡市で開催された、NDSの第52回で、「怖くないし役に立つ設計原則の話」というタイトルで発表してきました。

内容としては、

  • 設計原則は「馬鹿の一つ覚え」でやっていくとむしろ保守性を下げてしまうことを確認する
  • 様々な設計原則について例を出しながら、別の設計原則との有機的なつながりを考る
  • 有機的なつながりを意識しながら設計原則を利用することで、保守性の高い設計を導く

というような内容でした。少し誤解を生む表現などが入っているため資料は公開しませんが、機会があればどこかで再演したいなと思っています。お誘いください。

聞いた発表では、 @yuw27b さんの「今日から使えるCSSパターン」が最も興味深かったです。

保守性の高いCSSを書くのがものすごく難しいなか、どうやってCSSの難しさに立ち向かっていくかという話でしたが、BEMに触れつつ「DRYさを諦めてでも結合度を下げたほうがよい」という話題になったのは「なるほどな〜」という感じがしました。

わたしがCSSに触れて「怖いな」と思う部分は、「全部グローバル変数みたいなもん」というところです。そこにBEMを導入することで、擬似的にスコープを切れるようになるので、BEMはかなり筋のよい手法だと感じています。でも、それを考えるともしかしてつまり本当に求められているのはscoped cssでは?という感じがするし、最近のwebフロントエンドのコンポーネント指向も、scopedなCSSと同様の方向を向いてきているように感じています。そこで「Web Componentsはよ!」という気持ちになってきますが、なんというか、現実はなかなか厳しいですね……。大変であります。

NDSプログラミング言語や分野などが制限されていないため、毎回結構「異種格闘技戦」な様相を呈しており、自分の全然知らないことに出会えたり、学びがあってとても良いですね。またぜひお邪魔させていただきたいと思います。

#y8spring でフロントエンド開発の話をしてきました

先日行われた #y8spring で、@ushiboyさんとともにフロントエンド開発についてのトークをしてきました。

懇親会会場でお酒を飲みながらトークするという、いわゆる「はちぴースタイル」での発表だったため、かなり会場が温まっていてありがたかったですが、わたしにも酒が入っていてグダグダになってしまい、アルコールがダメのためシラフであった相方の@ushiboyさんには申し訳ないことをしたな!?と思っております。ご感想お待ちしております。

以下は聴衆としての感想ですが、ジョージさんによるトークがとてもよかったです。

hyperapp – 1kbのビューライブラリ · Issue #11 · uzulla/y8-2017-spring-talks · GitHub

reduxとかでいうところのreducerに当たる、State => Stateな関数をactionが持てると聴いたときにわたしがまず思ったのは「となるとreduxとおなじく非同期に対する問題が立ち上がってくるよなあ」ということでした、トークの中ではその話に触れられていなかったので、「非同期周りってどうしてるんですか」という質問をしたところ、hyperappはState => Promise[State]な関数をactionとして持てたり、actionの中で別のactionを発火することもできるとのことで、「うーん筋の良い感じのスタイルに思える」と感じました。

ところで、ヤパチーは今回は1トラックでの開催でしたが、1トラック結構いいですね!強制的に(?)様々なトークが聞けるし、「あれの裏番組がそれで、どっちも見たいのに!」とか考えなくていいし、懇親会でも多くのひとが同じコンテキストを共有できるし、巨大カンファレンスではない良さみたいなものがあってよかったですね。

こちらからは以上です。

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / write stack - usecase

前回の記事では、Scala.jsで書かれたモデル層がどのようなクラス/オブジェクトをJSで書かれたUI層に公開し、UI層はそれらをどのようにして扱うのかというのを見てきました。今回からはScala.jsで書かれたモデル層のうち、コマンドから始まる一連の状態更新系(write stackと呼んでいます)について見ていきたいと思います。

write stackを構成するpackageとクラスたち

write stackを構成するクラスは以下の通りです

  • usecase パッケージ
    • Commandクラス
    • Serviceクラス
  • domain パッケージ
    • Domain Modelを表すクラスたち
  • infrastructure パッケージ
    • Repositoryクラス
    • Synchronizerクラス

このうち、今回はusecaseパッケージに注目してみましょう。

usecaseパッケージは、いわゆる「アプリケーション層」に相当するパッケージです。アプリケーション層の責務は、アプリケーションへの入力を受け取り、domaininfrastructureにメッセージを送り、ユースケースを実現する責務です。

逆に言うと、この層に「ユースケースを実現する」以外の関心が書かれてしまうと「ドメインモデル貧血症」を起こすことになりますので、この層は薄く実装されるべきです。

write stackでは、CommandクラスとServiceクラスがこのusecaseパッケージを構成しています。

Commandクラスの責務

Commandクラスの責務は、以下のふたつの責務を持っています。

  • ユーザー入力のvalidation
  • ユーザー入力をアプリケーション上意味の意味のある値に変換
  • Serviceのdispatch

わかりやすい例はAddTodoCommandでしょう。

trait Command {
  protected val addTodoService: Service

  private var _todoInput = ""
  private var _dueDateInput = ""

  def todoInput = _todoInput
  def todoInput_=(v: String) {
    _todoInput = v
  }

  def dueDateInput = _dueDateInput
  def dueDateInput_=(v: String) {
    _dueDateInput = v
  }

  def isTodoInputValid = _todoInput != ""
  def isDueDateInputValid = {
    val ret:Boolean = try {
      dueDate
      true
    } catch {
      case _:DateTimeParseException => false
    }
    ret
  }

  def isExecutable = isTodoInputValid && isDueDateInputValid

  def dueDate = {
    LocalDate.parse(dueDateInput)
  }

  def execute() = addTodoService.execute(this)
}

todoInputdueDateInputは、フォームの値が変更されたらUI側からここに値がぶっこまれてきます(前回見たとおりですね)。

is*Valid系のメソッドがユーザの入力値の妥当性を検証しています。この値をUI側から読み出すことで、エラーメッセージの表示などができるわけですね。

また、dueDateInputString型ですが、アプリケーション内ではStringではなくてLocalDate型で扱いたいですよね。その変換もこのクラスが行っています。

また、executeメソッドでは自身を引数にServiceをdispatchしています。

このaddTodoServiceが抽象メンバー(って正しい言葉なのかな、わかんない)であるところがちょっとしたポイントですね。Commandクラスが単体テストしやすいように他のクラスへの依存は抽象メンバーとして定義しておいてDIするわけです。DIについてよくわからない、という向きは手前味噌ですが要するに DI って何なのという話がわかりやすいと評判なのでおすすめです。

ちなみに、Scala.jsで定義されたクラスなどをJS側から触るためには、@JSExportの仲間たちのアノテーションを付けてやる必要がありますが、今回はjs_bridgeというパッケージに「JS側にexportするクラス」を置いています。見てみましょう

@JSExportTopLevel("AddTodoCommand")
class AddTodoCommand extends Command {
  protected val addTodoService = new ServiceImpl

  @JSExport
  override def todoInput = super.todoInput
  @JSExport
  override def todoInput_=(v: String) = super.todoInput_=(v)


  @JSExport
  override def dueDateInput = super.dueDateInput
  @JSExport
  override def dueDateInput_=(v: String) = super.dueDateInput_=(v)

  @JSExport
  override def isTodoInputValid = super.isTodoInputValid
  @JSExport
  override def isDueDateInputValid = super.isDueDateInputValid

  @JSExport
  override def execute() = super.execute()
}

さきほどはCommandをtrait(多重に継承できる抽象クラスみたいなもんです)として定義して、addTodoServiceは抽象メンバーでしたね。それを継承してJS側にExportするタイミングで、実際のServiceの実装を配線してDIしているのが見て取れるでしょう。

また、publicなメソッドであっても明示的に@JSExportしないとJS側からはそのメソッドをさわれないため、exportするメソッドは明示的にoverrideしています。しかしここには「実装」は一切書かれておらず、ロジックはすべてピュアScala.js(って言い方で通じるかな)であるtrait側に書かれていることに注意してください。

Serviceクラスの責務

では、CommandクラスによってdispatchされるServiceクラスは具体的にどのような仕事をしているでしょうか。「todoを追加する」というユースケースを実現するusecase.add_todoパッケージのServiceを見てみましょう。

trait Service {
  protected val repository: TodoRepository
  protected val synchronizer: TodoSynchronizer

  def execute(command: Command): Unit = {
    if ( ! command.isExecutable ) {
      return
    }

    // ...(1)
    val todo = Todo.open(
      id = repository.nextId(),
      body = command.todoInput,
      dueDate = command.dueDate
    )

    repository.store(todo) // ...(2)
    synchronizer.sync(todo) // ...(3) 
  }
}

domain層で定義されているTodoというドメインモデルを利用して、Todoを新しく作っています(1)。新しく作られたTodoを、アプリケーションの状態を保持する役目を持つrepositoryに保存しています(2)。参照系を構成するread stackの説明をするときに詳述しますが、read stackでこのrepositoryに保存されている状態を読み取ることで、UI側はアプリケーションの論理的な状態をUIとして描画する形です。

また、今回のアプリケーションでは「新しいTodoを作ったらなるべくすぐにサーバーに同期する」という仕様にしてあるので、「同期してくれる君」であるsynchronizerにtodoの同期を頼んで(3)、このクラスの責務はおしまいです。

このとき、サービスは「新しく作られたTodoがどのような状態やプロパティを持っているのか」や「repositoryはTodoをどこに保存しているのか」や「synchronizerはどうやってtodoをサーバーと同期しているのか」の詳細については一切意識していないことに気をつけてください。

次回詳述しますが、「新しくTodoが作られたときにそのTodoはどのような状態なのか」とか、そういうのはdomainパッケージに記述していきます。

このあたりの話の具体例をもう一つ出しましょう。「todoをdone状態にする」というユースケースを実現するusecase.make_todo_done.Service

trait Service {
  protected val repository: TodoRepository
  protected val synchronizer: TodoSynchronizer

  def execute(id: Int) = {
    repository.find(id).foreach {oldTodo =>
      val doneTodo = oldTodo.makeDone
      repository.store(doneTodo)
      synchronizer.sync(doneTodo)
    }
  }
}

repositoryから該当のtodoを引っ張ってきて、そのtodoに対してmakeDoneというメソッドを呼び出して「doneになったTodo」をrepositoryに保存して、同期してるだけです。「todoにどういうプロパティがあるのか」とか一切意識していません。

次回予告

次回は、usecaseパッケージが利用しているdomainパッケージとinfrastructureパッケージののうち、domainパッケージが持つ責務を具体的に見ていく予定です。

NDS(長岡開発者勉強会)の52回で「怖くないし役に立つ設計原則の話」というタイトルで喋ります

新潟県長岡市では定期的に「NDS」という勉強会が開催されています。わたしも新潟に住んでいるときには非常にお世話になった勉強会で、とても良い勉強会です。

その第52回(52回ですよ!?すごくないですか!?)が6/17に開催されるとのことなので、「怖くないし役に立つ設計原則の話」というタイトルで発表させていただくことにしました。

nagaoka.techtalk.jp

まだ内容固まっていないのですが、このトークでは、

  • デザインパターンとか知識としては知ってるんだけどなぜそれが有効なのかよくわからん
  • 「結局設計パターンとかって役に立たないし勘でやるものでしょ」
  • MVCとかMVVMとかMVPとか「わざわざ難しくしてる」としか思えない!!!
  • DRY原則なら知ってる

といった感じのひと(2010年くらいの俺だ……)を想定して、

  • われわれは(主語が大きい)なぜ設計パターンを役立たせることができないのか
  • 設計パターンには抽象度の高いものから低いものまで様々なものがある
  • SOLID原則を例にとって「役に立たせ方」を実践する

といった内容を発表したいと思っています。チャレンジング!どこまで成功するだろうか。

長岡にはおいしいお酒もおいしい料理もあるので、みなさんもぜひこの機会にNDSに参加してみてはいかがでしょうか。お待ちしております。

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Vue.jsによるUI層とScala.jsによるモデル層のコミュニケーション

前回の記事では、Scala.jsをどのように利用したかについて概観を見てきました。

前回、UI層は素直にVue.jsの単一ファイルコンポーネントで書いて、モデル層はScala.jsで書く、というスタイルを取る、と述べましたが、今回はモデル層がどのようなインターフェイスをUI層に公開して、UI層がどのようにそれらを利用する設計としたのかについて見ていきます。

モデル層の公開するみっつの種類のクラス/オブジェクト

モデル層は、UI層に対して、「コマンド」「クエリ」「イベント」のみっつの種類のクラス/オブジェクトを公開します。まずは「コマンド」から見ていきましょう。

コマンドの責務

コマンドは、アプリケーションの状態を更新するような操作(たとえばTodoを追加する、Todoをdone状態にする、など)の窓口を提供します。

なにかを更新するような操作を行う場合、UI層は、

  • コマンドをインスタンス化し
  • コマンドを組み立てて
  • コマンドを実行

します。また、コマンドクラスは「このコマンドはvalidか?」を判別する責務も持ちます。これがUI側からどのように見えるのかがよくわかるのがTodoInput.vueです。JS部分を引用してみます。

    import {AddTodoCommand} from '../../../target/scala-2.12/scalajstodo-fastopt' //...(1)
    export default {
        beforeCreate(){
            this.addTodoCommand = new AddTodoCommand; //...(2)
        },
        data(){
            return {
                todoInput: "",
                dueDateInput: "",
                isTodoInputValid: true,
                isDueDateInputValid: true
            };
        },
        watch: {
            todoInput(v){ // ...(3)
                this.addTodoCommand.todoInput = v;  //...(4)
                this.isTodoInputValid = this.addTodoCommand.isTodoInputValid; //...(5)
            },
            dueDateInput(v){
                this.addTodoCommand.dueDateInput = v;
                this.isDueDateInputValid  = this.addTodoCommand.isDueDateInputValid;
            }
        },
        methods:{
            addTodo(){ //...(6)
                this.addTodoCommand.execute(); 
            }
        }
    }

まずは(1)の部分で、Scala.jsから公開されているAddTodoCommandというクラスをimportして、(2)でコマンドをインスタンス化しています。

フォームのinputに変化が起こると、データバインドされたtodoInputに変化が起こり、それをwatchしている関数(3)が呼び出されるので、その中でコマンドの値をセットています(4)。さらに、コマンドクラスは「この値がvalidな値かどうか」を知っているので、「この値はvalidですか?」というのをコマンドクラスにたずねて、その結果をUI側に書き戻しています(5)

フォームがsubmitされると、addTodoというメソッドが呼び出されます(6)。この中では、組み立て終わったコマンドを実際にexecuteしているだけです。この際、コマンドは投げっぱなしになっていることに注意してください。

コマンドクラスは「アプリケーションの状態を更新する」ための窓口であり、「更新されたあとの状態を取得する」ための窓口ではありません。「更新結果の取得」にはQueryを利用します。

クエリの責務

UI上にアプリケーションの現在の状態を描画するためには、現在の状態をなんとかして取得しなければなりません。その際に利用するのが、Queryです。これもUI側からの利用例を見ましょう。TodoList.vueです

    import {
        TodoQuery,
        /* 中略 */
    } from '../../../target/scala-2.12/scalajstodo-fastopt'
    
    export default {
        beforeCreate(){
            this.todoQuery= new TodoQuery; //...(1)
        },

        /* 中略 */

        data(){
            return {
                todos: this.todoQuery.all() //...(2)
            };
        }
    }

(1)インスタンス化したqueryを利用して、(2)でアプリケーションの状態(今回ならば「現在のtodo一覧」)を取得して、databind用の変数に書き戻しています。

ところで、このqueryは「いつ」実行すべきでしょうか。(2)の部分では、まず、UIの構築時に「初期データ」としてアプリケーションの状態を読み出しています。

しかし、よく考えるとそれだけではなくて、「アプリケーションの状態に変化があったとき(つまり今回ならtodoのリストに変化があったとき」にも、状態を読み出してUIを更新しなければなりません。そこで必要になるのが「イベント」です。

イベントの責務

イベントは、モデル側でなにか状態に変更が起こったのをUI側に伝えるためのオブジェクトです。これも例を見るのが一番でしょう。クエリと同じく、TodoList.vueです。

    import {
        /* 中略 */
        TodoRepositoryChanged //...(1)
    } from '../../../target/scala-2.12/scalajstodo-fastopt'
    export default {
        /* 中略 */
        created(){
            // (2)
            this.subscription = TodoRepositoryChanged.subscribe(() => {
                this.todos = this.todoQuery.all();
                console.log(this.todos);
            });
        },
        beforeDestroy(){
            this.subscription.unsubscribe();
        },
        /* 中略 */
    }

TodoRepositoryChangedというイベントをScala.js側からimportしてきています(1)。さらに、(2)でそのイベントが発火したらtodoを読み込み直すようにしています。UIがdestroyされたときにきちんとunsbscribeするのも忘れないようにしましょう(3)

UI層とモデル層のコミュニケーションまとめ

以上をまとめると、以下のようなフローでUI層とモデル層はコミュニケーションすることになります。

まず、初期データをQueryを通じて読み込みし、UI側でレンダリングします。

f:id:nkgt_chkonk:20170522150803p:plain

状態の更新を伴うような操作は、Commandを組み立てて実行します。

f:id:nkgt_chkonk:20170522150924p:plain

Commandの結果としてアプリケーションの状態が変化し、Eventが通知されます

f:id:nkgt_chkonk:20170522151017p:plain

Eventに応じて、最新の状態を読み出し、表示します。

f:id:nkgt_chkonk:20170522151058p:plain

このように、モデル層では「更新の窓口」と「読み出しの窓口」と「状態通知の窓口」のみっつを設けて、UI層ではそれらを適切にimportし利用することで、「UIの定義」と「アプリケーションのロジック」を分離することができます。いわゆる「プレゼンテーションとドメインの分離」ってやつですね。

次回予告

次回は、オフラインファーストなアプリケーションにおいて、モデル層をどのように設計していったかについて書きます。

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Scala.jsについて

はじめに

オフラインファーストへの要求

近年、オフラインファーストというか、「オフラインのときにも普通につかえて、オンラインになったら同期する」みたいなことに対する要求が高まっているように感じます。

その場合は、ローカルにもきちんと永続データを持っておき、オンラインのときにバックエンドと通信をしながらバックエンドのデータと同期していく、というスタイルを考えるのが自然だと思います。

また、普通のJSアプリケーションであっても、「サーバーに投げる際に失敗したデータはローカルでメモリ上にもっておいてリトライしたい」などの要求もあるでしょう。

さらに、ここでモバイルアプリも視野に入っているとなると、どうしても「オッRealm Mobile Platformか!?」という感じが出てきますが、Realm Mobile Platformにロックインされるのと引き換えに開発の速を選ぶのか、それとも、というのは判断の別れるところでしょう。

そこで、一旦オフラインファーストなGUIアプリケーションをどのように設計するべきかの素振りを、ブラウザプラットフォーム上でしてみることにしました。これはわたしが単に慣れているプラットフォームだから、という理由です。

複雑なロジックは表現力の高い言語で書きたいという要求

オフラインファーストなアプリケーションを書いていく場合、「いつ同期するのか」「同期のストラテジはどうするのか」「同期中のデータはどう管理するのか」「同期に失敗した場合は?」「サーバーから降ってくる同期用データをどうハンドリングするか」など、かなり複雑なことをモデル層以下でハンドリングしなければならないということが予想されます。

また、そもそもモデル層はなるべくリッチで設計上の制約がない言語で書いて、ドメインの設計やアプリケーションの設計に集中したいという要求があります。

今回は、「どうせ素振りなのだから」ということでScala.jsに手を染めてみました。ちなみにリポジトリShinpeim/Scala.jsTodoExampleにあります。

本記事では、この素振りによって得られた知見をまとめます。全3,4回くらいになる予定です。

初回はScala.jsについてです。

Scala.jsについて

JSとの協調、懸念点

Scala.jsを使ってみるにあたって

  • JSのライブラリ資産ってScala.js側からは使えるの?
  • Scalaのライブラリ資産ってそのまま使えるの?

というところがまず懸念点として上がります。また、やはりJSが得意とする部分(たとえばVue.jsのシングルファイルコンポーネントでUIを組み立てていくこととか、Routingとか、そういうブラウザと密結合している部分)についてはJSで書けたほうが余分なハマりが少なそうという感じもあります。

そこで、今回は

  • index.js(エントリポイント)とUI定義であるVue.jsのシングルファイルコンポーネント(*.vueってやつ)だけJSで書く
  • そのほかはすべてScala.jsで書き、sbtがwatchしてJSにコンパイル
  • webpackがそのファイルもwatchして、がっちゃんする

というスタイルを取ることにしました。

このスタイルの懸念点としては、

  • Scala.jsが吐くファイルサイズがでかくなるけどそれをwebpackでがっちゃんって実用に耐えるの?
  • そもそもScala.jsで吐いたJSってES6 importとかCommonJS requireで扱えるの?

というあたりがあるので、そのあたりの検証も行いました。

以下、その懸念点に対する回答です。

JSのライブラリ資産ってScala側からは使えるの?

結論から言うと、使おうと思えば使えますが、「そのまま」は使えません。

  • すでにJSライブラリをScala.jsで動かすためのファサードが提供されている場合はそれ使う
  • 自分でファサード書く
  • 型検査なしにScala.js側からがんばってJSのオブジェクトなどを操作して使う

というみっつの選択肢があります。

詳しくは公式のドキュメント - JavaScript libraries for Scala.jsを参照してください。

ただ、今回はJSのほうが得意な部分は素直にJSで書く、というスタイルを選択しているため、jQueryだとかVue.jsだとかをScala.js側から触る必要はないので、JSのライブラリ資産をScala側から利用するシーンはありませんでした。

Scalaのライブラリってそのまま使える?

結論から言うと、「Scala.jsコンパチなライブラリならそのまま使える」ということのようです。

公式のドキュメント Compatible Scala librariesを御覧ください。ScalazとかCatsとかShaplessとかあって強い……

joda.timeを利用しようとしたらうまくfastOpt(最後にjs吐く部分の操作だと思って)できなくて、「どういうライブラリなら"Scala.jsコンパチ"って言えるんだ?」ってことはよくわかってなくて、まだ調べきれてません。

Scala.jsが吐くファイルサイズがでかくなるけどそれをwebpackでがっちゃんって実用に耐えるの?

  • でかいとwebpackがbundle.js吐くのに時間かかって開発のサイクルが遅くなったりしない?
    • 開発時はminifyなどをスキップする、Scala.jsが吐くjsファイルはバベったりローダー噛ませたりしないでそのままimportすると、現実的な速度でサイクル回せるなあという感じがする
  • で、最終的なサイズはどうなの、実用できそう?
    • 今回のリポジトリの規模で、minifyなしで4.08MB、minifyありで2.16MB。でかいライブラリに依存したりするとかなり厳しいかなという気がするが、まだちょっと未知数という感じがする。

そもそもScala.jsで吐いたJSってES6 importとかCommonJS requireで扱えるの?

扱える。see Export Scala.js APIs to JavaScript - Exports with modules

Scala.jsの書き味はどうだった?

最高の体験だった……。静的型付けによるIDEの賢いリファクタ……、堅牢で柔軟な言語仕様……、なんか矛盾があればsbtさんが怒ってくれる……、traitを利用したDI……。ほしいものがここにあった……ここは地上の楽園か……?

また、Scalaが表現力の高い言語なので、結果として設計やロジックに集中することができて、JSでがんばって書きながら設計してたときよりもより良い設計が導けた気がする。

次回予告というか今後語られる予定の話

  • Vue.jsのシングルファイルコンポーネントの世界とScala.jsの世界をつなぐ窓口をどのように設計したか(それはつまりプレゼンテーション層とドメイン層をつなぐ窓口の設計である)について
  • オフラインファーストを視野に入れた際のドメイン層の設計について

-> 書きました

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Vue.jsによるUI層とScala.jsによるモデル層のコミュニケーション - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く

vue-routerのafterEachグローバルフックがhookされるタイミングとonReadyがhookされるタイミング

vue-routerにはonReadyというフックが用意されている

ドキュメントを読めばわかるように、これは when the router has completed the initial navigation, which means it has resolved all async enter hooks and async components that are associated with the initial route. 、つまり最初に表示するルートに紐付けられたbeforeEnter ナビゲーションガードやbeforeEachglobalナビゲーションガードが解決され、「どのrouteが実行されるべきか」が解決されたあとに呼ばれる。

これが便利になるのはSSRのときで、preloadしたいデータがあるような場合はbeforeEnter内で非同期読み込みし、その読み込みが終わったらnextするようにしておき、onReadyのタイミングでSSRすればよい、というような使いかたができるわけだ。

ところで、ナビゲーションガードが解決されたあとに呼ばれるhookはもうひとつある。それがafterEachグローバルhookだ。

このafterEachというhookとonReadyhookはどちらが先に呼び出されるのだろう。ドキュメントを読んでもよくわからなかったので、ソースを追った。

まず、onReadyで登録したhookを実際に呼び出しているのはここである。

  transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()

      // fire ready cbs once
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => { cb(route) })  // ココね!!!ここ!!!
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => { cb(err) })
      }
    })
  }

で、その前にコールされてるthis.updateRoute(route)の中身見ると

  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }

ここでafterHooksを呼んでいる。というわけで、onReadyよりも先にafterEatchのhookが呼ばれるようになっていることが確認できた。

注意すべき点として、(あたりまえだが)afterEachは同期的に呼び出されているので、その中で非同期な操作を行った場合、その非同期操作の完了を待たずにonReadyhookが呼び出されるので、onReadyにhookしてSSRしても、afterEach内で行った非同期操作の結果はSSRされない。