オフラインファースト的な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されない。

VuexとPDS、immutable modelの所感

Twitterに書いた内容再掲。

最近Vuexについて考えてるのは、Vuex使ってPDS実現するとなると、状態はVuexのレイヤーで持つことになるから、モデル層はステートレスで作るのが相性良い(というか自然とそうなる)だろうな

その時にJSでイミュータブルに寄せて行く辛さがどれくらいキツいものなのかの知見が自分にはない

ある程度以上に複雑なアプリケーションでは、サーバーサイドからpushされる情報のためにコネクション握りっぱなしにする部分とかも出てくるであろうと思っていて、モデル層以下をイミュータブルに寄せて行った時にそういうのをどうハンドルすれば良いかも自分に知見がない

Vuex少し触ったところとても筋が良いと感じるんだけど、そのあたりみんなどうしているのか知りたいなあ

まあある程度の複雑さまでなら「Vuexの層に全部書く」でよいからそれでええやん、というのはありそう

イミュータブルモデルの良いところはテスタビリティがぐんぐんあがるよ!ってところだと思っていて、そのメリットは捨てがたいので知見貯めていきたい

画面ごとの複雑さが異なるアプリケーションにどう立ち向かうか

ツイッターで書いた内容です

SPAやってると画面ごとに複雑さが全然ちがって、単なる一覧表示してる画面の複雑さといろいろな操作ができる画面の複雑さは雲泥の差になる

こういうときに、「この画面はきちんとドメイン層作ったほうが見通しがよくなるけど、この画面もそうしちゃうとオーバーキルなんだよな〜」ということがある

そういうときにアプリケーション内で一貫したアーキテクチャでやっていくのか、この画面はXの層をスキップしますとするのか、こーたえーはーなーい(ABSTRACT TRUTH / NUMBERGIRL

ディレクトリ構成、名前空間構成に貴様の 意思 を込めろ」という煽りを思いついたので自由に使ってください

ひとつのアプリケーションの中に複数の画面があり、画面ごとに複雑さが異なるような場合、やはり画面ごとにアーキテクチャを変えるというのが正解な気がしてきた。その場合、たとえばパッケージ構造は次のようにする

- presentation
  - A
  - B
- model
  - transaction
    - A transaction
  - layered
    - usecase
      - B usecase
    - domain
    - infrastructure

その上で、domainとinfrastructureの下は画面構造に依存しない分割構造でパッケージを分ける

どうか。

しかし、これを実践するためには、意図をきちんと説明したら理解してくれるメンバーで開発を行う必要があり、弊社なら問題ないな

MVVM, Layered Architecture, Clean Architectureを綜合して位置づけを整理する

一つ前の記事にもある通り、掲題の通りの試みを行いました。

github.com

一応、「一通り書けたぞ!」という感じになったので、ここにてこのREADMEを Ver.1.0.0 メジャーリリースとします。

自分ではかなりわかりやすく整理できたつもりでいますが、まだまだ「えっここってどういうこと?わかりにくいな〜」という部分があったり、「あ、ここは間違いですね」といった部分があるかと思います。そういうときには是非 issue やブックマークなどでフィードバックをください。それをもとにさらに良いものに育てて行きたいと思います。

また、もしこのリポジトリがみなさんの一助になるようなことがあったとしたら、

  • READMEの最後に書いてあるwishlistからカンパする
  • 弊社で一緒に働いて一緒に議論を深めていく

のどちらかを是非!是非!!!!検討してみてください。twitter @neko_gata_s が誰からでもDMを受け付けるようになっているので、弊社に興味がある方はぜひメッセージをください。techblogもやってます。

GUIアプリケーションアーキテクチャ総合!みたいなやつ書いてる

最近はずっとJSでGUIを書くっていうお仕事をやっていて、その中で様々な知見が溜まってきてます。

そのときにひしひしと感じたんだけど、世の中にはさまざまなアプリケーション・アーキテクチャの話が溢れかえっていて、結構混乱を産んでいるように思います。

MVVMだけ見ても、「MVVMで実装しています」って主張しているものがMVVMパターンと違うパターンで実装されていたり、「軽量MVVM」なんて言葉が生まれていたりという状況があって、これはだいぶ混乱が極まっているぞ、と感じる状況です。

また「最近はClean Architectureが熱いらしい!MVVMを捨ててやってみよう!」とか言う発言を目にしたりして、「Clean ArchitectureとMVVMは矛盾しないからMVVMを捨てる必要はないんだよ!」って思ったりもするわけです。

そういう混乱の中にあるひと(それは知見を貯めるまえの自分のことでもあります)のために、今自分がきちんと理解している範囲で、様々なアプリケーション・アーキテクチャと、それぞれの関係みたいなものがきちんと説明できたらいいな、と思って、サンプルアプリケーション付きのリポジトリを作りました。

github.com

サンプルアプリ自体はGithubPagesで動いています(再生ボタン押すと音が出るので注意)。

README.mdに解説が書いてあります。

このサンプルアプリのアーキテクチャを解説していく中で、Presentation Domain Separation と MVVM と Layered Architecture と Clean Architecture について、整理された概念を読者が獲得していくことを目指しています。

と、いいつつ、実はこのREADME.md、「執筆中」って部分があるんですけど、まあ、そうです、まだ執筆中なんですよね。

とはいえ、すでに結構な分量が書かれたので、一旦ここで「こういうの書いてるよ!よかったら読んでね!」ってアナウンスしておこうかな、と思った感じです。

読んでいただいて、よかったら「よかったよ!」って言ってもらえると、続きを書くモチベーションにもなるので、心優しいひとは読んでみて、よかったら「良かったよ!」って言ってください……。

また、技術的な誤りなどが含まれている場合はIssueなどでご指摘を歓迎いたします。よろしくおねがいします。

Vue.jsとvuex、Fluxについて

最近立て続けにそのあたりの話をする機会があったので。わたしの意見です。

vuexというかFluxに手を出すタイミング

  • Vue.jsを利用していて、相互に関連のある二つ以上の状態を扱う必要が出てきたら、それはもうすでに「十分に複雑な状態管理」である
    • たとえば、APIとの通信中はインジケータを出したいので「通信中かどうか」を管理し、通信が終わったらその結果を表示するために「通信結果」も管理したい、など。
  • 十分に複雑な状態管理に立ち向かうためには、自分でピュアなDomain側をきちんと作ってそこで状態管理するか、vuex利用するべきだと思う
  • vuexを触ってみたところ、非同期処理含めてかなり筋が良くてわかりやすくて見通しが良く、かなりお勧めできる

vuexで状態管理するか、自分でPDSのDomain側のアーキテクチャ設計からやるか

  • vuexに乗っかると、PDSで言うところのDomain側もvuexが示すレールに乗ることになる
  • 楽だしレールが明確というメリットがある
  • vuexに破壊的な変更が起こった時、PDSのDomain側も破壊的な変更に巻き込まれるというデメリットがある

わたしはフレームワークのアップデートでアプリ全部に手を入れるの嫌なのでプロダクションではvuex使ってない。

自分でDomain側の設計からやるとき今わたしがどうやってるか

  • 状態は全部PDSのDomain側に持つ
  • VMから叩かれる「窓口」は一本化しておき、そこで非同期通信の待ち合わせなどする。いわゆる「usecase」だとか「service」と呼ばれるやつ。
  • usecaseやserviceがデータモデルやドメインモデルやインフラストラクチャを操作する
  • データモデルやドメインモデルに起こった変更はObservableな機構を利用してPresentation側に通知する

このへんの話はYAPC::Kansaiで話したんだけど未だに発表再現ブログ書けてない。明日書けたら書く。