ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

これからの中規模分割型アーキテクチャを考えよう

さんざん言及されている資料であり、
これさえ読んでもらえれば、これ以降の文章は不要ですΣ(・ω・ノ)ノ

speakerdeck.com

個人的に、モノリシックアーキテクチャと、マイクロアーキテクチャも、
どちらも「それだけでは辛い」ものであり、
どこかでバランスを取らないといけない・・・と、考えていました

それに対する明確な回答がこの資料であり、
少なくとも現時点においては最適解だと思っているので、
これ以降はただの自分用メモです...φ(・ω・`)


Case1:業務系CMSAPI

私が最初に「API状のもの」を作った時*1アーキテクチャがこちら*2です(´・ω・)っ

f:id:parrot_studio:20190403111453j:plain


当時、私は関数型言語に興味があったので、
アーキテクチャの設計そのものにも関数型的概念を突っ込んでいました

www.slideshare.net

API層が「副作用がある(Store)」と「副作用がない(Logic)」に明確に分かれており、
Perlで表現されたViewがLogicのAPIのみを触るという、
「ちゃんと分離されている感のある」のが非常に良かったと思います

問題は、Logic層がViewに強く依存しているところです

そもそも、Perlで書く量を限界まで減らそうとした結果、
Logicに業務ロジックのほとんどが移動した・・・というのが真相なので、
Logicにエラー文言のようなViewに関わる情報まで含んでいるのが問題です(´-ω-)

とはいえ、Store層はそこそこリソース単位のAPIとして成立しており、
Logic層も今回の機能に特化したAPI群である・・・と考えると、
(Viewの情報さえ分離できれば)構造そのものは不適切でもなかったと思います

Case2:スマホゲーのAPI

急にシステムが飛びますが、次に設計したのがスマホゲーのサーバですΣ(・ω・ノ)ノ

スマホゲーももちろんWebシステムではあるのですが、
通常のWebシステムに比べて、とがった部分が多々あります

  • セキュリティレベルを相当高くする必要がある(例:通信データ自体を暗号化)
  • 応答を少しでも早くしないといけない
  • ユーザーデータが莫大で、単一スキーマに存在することが必須(意味で分離できない)
  • 業務ロジック(=ゲームルール)が複雑で、多数のリソースを同一トランザクションで処理する必要がある
  • 通信のリトライを雑にすることが許されない
  • etc...

「ま、ここはいっか」って妥協が許されないのがゲームサーバです(lll゚Д゚)
(ユーザーが不正を働くモチベーションが非常に高いため)

にもかかわらず、「ゲームの売り上げ」と「サーバの精度」はあまり相関がなく、
結局のところ別な要因で決まるのも辛いところで、だからこそサーバエンジニアは、
企画やクライアントエンジニアが動きやすい環境を作るのが仕事になります

そんな当時のアーキテクチャは、こんな感じでした(´・ω・)っ

f:id:parrot_studio:20190403111503j:plain


莫大なユーザーデータが単一のスキーマに存在するのが必須なのですが、
その代わりユーザー(のID)で水平分割はしやすいです
(その代わり、「ユーザーをまたいだ処理」*3で地獄を見ます)

また、ゲームロジックが複雑で、処理が多岐に渡るため、
そのロジックをどこに置くかが問題で、当時はmodelに置いていました
その結果、modelが複雑で面倒な構造になってました(lll゚Д゚)

また、管理画面も同一のソースで管理していたのですが、
これは管理画面からの操作でも、ユーザーデータのバリデーション*4などは同一のため、
開発効率を考えると仕方なかった部分はあります

今考えるともうちょっとやり方はあったと思いますが、
小人数で面倒なシステム開発を進めるためにも、
ある程度モノリシックなシステムになってしまうのも仕方ないのかな・・・と

あと、これは当時の会社の方針ですが、
どうしても「アーキテクチャの設計」ができるエンジニアは限られるため、
ある程度プロジェクトが軌道に乗った段階で、別の新規プロジェクトに回されました*5

そのため、「最初から最後まで」見ていたシステムはなくて、
こはちょっと残念です・・・が、
私のようなタイプが「運用フェーズ」に向いてないのも事実なので、合理的です

Case3:モノリシックなAPIとView

そして現状いじっている業務システムです
Case1とCase3の中間を取ったようなアーキテクチャになってます

f:id:parrot_studio:20190403111514j:plain

以前に比べるとAPIRailsに関する理解も深まったので、
かなりリソース志向のAPIになっており、
さまざまなViewでそれを使う設計になってます

いわば、リソースAPIがいろいろなシステムのデータ管理層になっており、
View層がロジカルな部分を担当し、画面を作る・・・という形です

最初はReadがメインだったので問題なかったのですが、
Writeが発生したあたりでだんだん粗が見えてきて、
Viewがどんどん膨らんでしまいました(lll゚Д゚)

また、API側にも業務ロジックが一部入り込んでしまったりと、
Case1のLogicにあたる「業務ロジックを管理する層」を明確にしなかったのが失敗です

今にして思えば、私はCase1を「いまいち」だと思っていたのですが、
その理由を明確に分析してなかったのかな・・・と
実はCase1を整理したバージョンが正解に近かったのですが・・・(´・ω・`)

未来の話:これからの理想的アーキテクチャ

冒頭のスライドの内容を抜粋すると、こうなります

  • Railsはリソース志向に特化したフレームワークである
    • 結果的に小規模のリソース群に対しての開発効率がダントツである
  • しかし、業務ロジック層を無理矢理入れようとした結果、システムが複雑化する
    • 先の例ならmodelがファットになる
    • 最近の例だとService層を導入する
  • Railsの良さを生かしつつ、全体のアーキテクチャでカバーできないのか?

これに加えて、最近のマイクロアーキテクチャが、
「細かすぎてわからなくなってしまう」という問題を抱えているのもあります

「巨大なシステム」も「数が多すぎるシステム」も、
結局のところ「人間の認識限界を超える」のが問題なのです
設計の肝は「人間の認識できる規模にインターフェースを整理すること」ですからね

うまく多層化されたシステムや、うまく構造化されたコードは、
ある処理を考える時に、「その先」のことを考えず、
「今見ている処理」を小さく考えることができます

関数型言語(的な設計)の肝はそこにあるのですが、
本質的な話ばかりが先行した結果、
非常に扱いづらい印象になってしまったのがとても残念です(´-ω-)

先のスライドをふまえつつ、私が理解した範疇で、
もし「次」を考えるとすると、こうなるのかなと

f:id:parrot_studio:20190403111531j:plain

  • View-Logic(Service)-Resource-Dataに層を分ける
    • View層は「表現」で分割
    • Logic(Service)層は「業務」で分割
    • Data層は「トランザクション」で分割
    • Resource層はData層と1対1で、Logic層に対するAPIを提供

Data-Resouceの層を組むのにRailsは便利で、
その範疇であればRailsが破綻しないのであれば、
こういうやり方になるのかな・・・と...φ(・ω・`)

本来、全てのデータ処理に対してトランザクションを張りたいところですが、
それでは(Case3のような)巨大なスキーマができあがり、
管理できなくなるので、ある程度整合性をあきらめて分割する部分も出てくるのかなと*6

まとめ

個人的に「モノリシックなアーキテクチャ」も、「マイクロアーキテクチャ」も、
どちらもどこかで破綻すると思っていて、
「ちょうどいい落としどころ」がどこかにあるだろうな・・・と思ってました

その指針を明確に示しているのが冒頭のスライドであり、
非常に価値があると思います
ぜひ皆さんも読んでくださいね(`・ω・´)

*1: 厳密にはその手前があるのですが、ここに残すような精度のものではないし、結局リリースされていない=運用されてないので、価値はありませんΣ(・ω・ノ)ノ

*2: どこかで似たような図を書いて発表した記憶があるのですが、プライベートな勉強会で資料をUPしなかった気がするので、作り直しました(´-ω-)

*3: 例:フレンド申請・レイド戦・マルチプレイ

*4: しかもめっちゃ複雑(lll゚Д゚) 「お金を増やす」だけでも多数のバリデーションが必要ですからね・・・

*5: 別な人に開発を引き継ぐのが前提だったため、「できるだけわかりやすい、Railsの標準的な設計」を心がけたので、ここでRailsに対する習熟度が上がったと思ってます

*6: もちろん、Case2のようなゲームサーバの場合、ユーザーデータは単一トランザクションでないとダメなので、このような設計は採りづらいと思います。業務系寄りの話ですね

React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その3:設計変更編)

2回に渡って書いてきたSSR化のお話も、今回でラストです(`・ω・´)


<前回>
parrot.hatenadiary.jp

parrot.hatenadiary.jp

<サイト>
ccpts.parrot-studio.com

<修正したコード>
github.com


正直、前回の話で「SSRの設計における一番の肝」は書いているのですが、
今日の件もそれはそれで重要なポイントではあるので、
頑張って書いていきます...φ(・ω・`)

前回の話が「SSRに関するわりと一般的な話」だったのに比べ、
今回の話は「ccptsの仕様やアーキテクチャに依存する話」が多いので、
一つの参考事例として捉えていただければ

もう一度段取りを復習しておくと、こんな感じでした

  1. ブラウザ系オブジェクトの排除(とりあえず実行時エラーを消す)
  2. 「仕様上の正しい動作」になるように修正
  3. インフラの調整

今回は残りのSTEP2とSTEP3のお話です

STEP2:SSRを前提にした設計の変更

STEP1の時点で「とりあえずの表示」はできているのですが、
当然、アプリの仕様として問題のある箇所が山ほどあります

仕様に依存する部分だったり、以前の手抜きの修正だったりするので、
あまり一般的ではないかもしれませんが、一つずつやったことを見ていきます

(1) Cookie周りの設計変更

前回は「parseした塊を丸ごと渡す」みたいな雑な実装をしましたが、
もちろんそれではダメ・・・ってことはないですが、
「最初の描画を全てSSR化する」という文脈ではよろしくありません

Cookieが管理しているデータをparseし、
それに対応するデータを取得するところまでサーバでやる必要があるからです


<今まで>

  1. クライアントの初期処理でCookieをparse
  2. 必要なデータをサーバAPIに問い合わせ
  3. 問い合わせのあったクエリに対するデータを取得
  4. APIの応答としてリストを返す
  5. クライアントの操作によりCookieを更新

<新しいやり方>

  1. サーバ側でCookieをparse
  2. Cookieに含まれる情報に対応するデータを取得
  3. Reactの初期値としてリストを渡す
  4. クライアントの操作によりCookieを更新


後から操作によって動的に変わる部分は今まで通りのやり方なのですが、
最初にレンダリングする情報だけ、サーバのデータ処理を先行した形です

ただまあ、ここに関しては、本質的に大きな設計変更で
「クライアントだけが知っていれば良かった情報を、サーバも知る必要がある」、
言いかえれば、「サーバ・クライアント間の線引きが崩れる」って話なんですよね・・・

そもそも、「サーバはAPIを提供し、クライアントで主要なロジックを組む」という設計で、
Cookieが管理する情報はまさに「クライアントだけが知っている情報」だったのです*1
(例:「お気に入り」「PT構成保存」等)

本当はうまく疎な関係にできればいいと思うし、
やり方はあると思うのですが、
今回の目的である「完全SSR化」を優先しました

(2) イベントの見直し(componentDidMountの排除)

jQuery.ready()」の時代からコードを積み足してきたのもあって、
componentの初期処理がcomponentDidMountに書かれていたケースが多数ありました
(例:componentDidMountの中で初期データをBacon.Busに流す)

これはcomponent同士を疎にするためには都合が良かったのですが、
サーバサイドではcomponentDidMountを初めとした「イベント」は動作しません
つまり、このままでは初期化処理が走りません(´-ω-)

要は、「全てをSSR化する」ためには、
「最初に表示したいデータ」は全てcomponentのpropsに渡すしかないわけです
(個人的には、ベストではないけど、目的のためにはベターくらいの感じで)

  constructor(props: AppViewProps) {
    super(props)

    // ...

    // constructorでサーバから受け取ったqueryStringをmodelに変換
    // queryStringもブラウザの概念なので、サーバから明示的にクライアントに渡す必要あり
    this.query = Query.parse(this.props.queryString)

    // ...
  }

  private renderConditionView(): JSX.Element | null {
    if (!this.state.showConditionArea) {
      return null
    }

    return (
      // クエリmodelをフォームに渡して描画
      // 以前だと Bacon.Bus を経由してデータを送っていたので、propsで渡す必要がなかった
      <ConditionView
        originTitle={this.props.originTitle}
        query={this.query}
        switchMainMode={this.switchMainMode.bind(this)}
      />
    )
  }

まあ、初期表示以降は今まで通り、
Bacon.Bus経由でストリームとしてデータを流しているので、
純粋すぎた部分と現実的な部分で落としどころを見つけられたのかな・・・としておきましょう

(3) 状態管理の手抜きを改善

このあたりまでくると、SSRに関係ない話なのですが・・・

jQuery.ready()」の時代からコードを積み足してきたのもあって、
あらかじめ全ての構造を描画しておいて、
初期処理や何らかのイベントでhide/showする・・・という設計がかなり残ってました

後からの制御はともかく、
初期処理(=componentDidMount)で表示を管理するとSSRでおかしくなるので、
厳密にprops/stateに依存する管理に全て移行させました

これはもう、React化した時のやり残しというか手抜きのフォローに近いのですが、
雑とはいえ問題なく動いていたものであっても、
SSR化する際には厳密に書かないとダメってことです(´-ω-)

結果的に、componentDidMount等に残ったコードは、
(前回の)Browserに依存するコードだけになりました(見た目とか、イベントハンドラのセットとか)
つまり、ブラウザでしか動作しなくても問題ない・・・ということです

export default abstract class ArcanaRenderer<T> extends React.Component<T> {

  protected div: HTMLDivElement | null = null

  public componentDidMount(): void {
    if (this.div) {
      Browser.hide(this.div)
      Browser.fadeIn(this.div)
    }
  }

  public componentWillUpdate(): void {
    // 更新される時はいったん消す
    if (this.div) {
      Browser.hide(this.div)
    }
  }

  public componentDidUpdate(): void {
    // 再マウントされる時にフェードイン
    if (this.div) {
      Browser.fadeIn(this.div)
    }
  }

// ...

}

一応フォローしておくと、STEP1の時の判断と同じで、
以前は「今回の目的はとにかくReactで動作するようにすることで、
細かい設計の粗は気にしない」というポリシーだったのです

「厳密にやる」ってのは相応のコストがかかる話なので、
「目的に合わせて現実的なコストに落とし込む」ってのも、
「運用」していくという観点では必要なのかなと

(4) 完全なレスポンシブ化

これもある意味手抜きを直しただけなのですが・・・

今まで、初期処理としてwindowサイズを計算し、
一定ラインを超えたら「携帯モード」で表示する、ってのをやってました
windowsサイズを元にフラグで管理)

当然、これはwindowオブジェクトに触れないSSRでは通用しないのですが、
とりあえずPC/タブレット用に描画して、
ブラウザ側の初期処理で修正すれば・・・と思ったら、うまくいかずΣ(゚Д゚)ガーン

SSRで吐き出したDOMと、後から読み込んだReact(on Rails)が処理するDOMで、
差分があるとwarningが出るのですが、携帯サイズだと大量のwarningが出るし、
そもそも挙動もおかしくなるのです

結局、正しい形でBootStrapのレスポンシブ機能を適用しました

  private renderMember(): JSX.Element {
    const m = this.props.member

    // 両方出力して、bootstrapのhiddenクラスに制御を任せる
    if (!m) {
      return (
        <div>
          <div className="none hidden-sm hidden-md hidden-lg summary-size arcana" />
          <div className="none hidden-xs full-size arcana" />
        </div>
      )
    }

  // ...
  }

もっと力技のところもありますが、基本的な方針はこうなってます

継ぎ足されたコードの弊害といってみればそれまでですが、
「ブラウザという環境」を、いかに暗黙的に、無意識に、
前提に置いていたか・・・ということでもあります(´-ω-)

STEP3:Webサーバのチューニング

ここまででコードレベルではだいたいリリース水準に達したので、
問題ないことを確認するため、staging環境にデプロイしたのですが、
やはりいろいろ問題が出ましたΣ(゚Д゚)ガーン

(1) Nginxのキャッシュ

一回目の表示は問題ないのに、二回目以降にエラーが出る問題が発生し、
エラーメッセージでググったところ、こんな話が

osa.hatenablog.com

SSRで巨大なHTMLをproxy先が吐き出したため、
キャッシュが使われるようになったが、
パーミッションに問題があったということです

つまり、staging環境で今までディスクキャッシュは使ってなかったのに、
SSR化してHTMLが大きくなったら、キャッシュに逃がす必要が出た、ということです

本番のパーミッションは特に問題なかったとはいえ、
そもそもストレージへの書き込みが発生する時点でまずいので、
キャッシュを大きめに設定しなおしました

(2) CPUの負荷(未解決・先送り)

前項の問題がもう出ないことを確認するのと、
ちょうど業務でパフォーマンス的な問題が出たりってのがあったので、
ついでにstaging環境でベンチマークをとってみました

すると、RubyのプロセスのCPU使用量がすごい勢いで上がっていき、
何度か実行していると、レスポンスが大幅に遅延したり、
詰まってエラーになってしまうことも(lll゚Д゚)

かなり無茶な叩き方をしたことは事実ですが、
思ったより「Rubyプロセスの中でexecjsの処理を実行する」のが重いようです

こればっかりは仕組みの問題であり、
そもそも最初からRubyでviewを作れば問題ないわけですが、
今回はSSR化自体が目的で、しかもサイトの訪問は皆無なので、気にしないことに

ある程度アクセスが多いようならば、
viewレベルのキャッシュを入れていく必要があると思いますが、
最終的にexecjsにHTML出力処理を投げる関係で、Railsのキャッシュが生かしづらい構造です

まさに「作って運用しようとしたからわかる問題」であり、
それがわかっただけでも収穫なのですが、
やっぱりSSRはいろいろ難しいですね・・・(´-ω-)

まあ、現実にはそこまで大きなアクセスがないサイトですし、
今回は特に何もしませんでした

(3) 通信量が大きすぎる

問題が解決した(あるいは先送りした)ので、今度は本番にデプロイして、
Googleのモバイルスピードテストサイトで確認したところ、
めっちゃ遅いという判定にΣ(゚Д゚)ガーン

developers.google.com

SSRで速くしたはずなのに、なんでや・・・と思ったら、
どうも通信量が大きすぎるという指摘でした

単純にNginxがgzip圧縮することで解決しましたが、
もっと早くやっておけって話ではありますよ(´・ω・)(・ω・`)ネー

あとはまだ、packs/ccpts.jsがminifyされてないって問題があるのですが、
いまいちうまくいかないので、いったん保留しております

まとめ

ということで、長々と書いてきましたが、最後にまとめです...φ(・ω・`)

  • SSRの大前提を忘れてはいけない
    • SPAがあってのSSRである
    • 最初からサーバサイドでviewを作る設計との比較が必要
  • 暗黙的に「ブラウザ」に依存している部分の排除が大変
    • JavaScriptで書かれたコードではあるが、「サーバサイドで動かす」のを意識しないとダメ
    • ccpts程度の規模でもこれだけ大変なので、業務レベルのアプリではかなり面倒
  • (個人的に)「うまい設計」がまだ見えない
    • 全部サーバサイドはなんか違和感があるので、うまくクライアントと切り離したい

今までがそうであったように、今回の修正もこれが完成ってわけではないので、
また何か新しい技術を使って整理できるといいかなと思います

*1: とはいえ、Cookieが扱うのは「データ」であって「ロジック」ではないので、制御のコードはまだクライアントにあり、その意味では設計が維持されています

React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その2:ブラウザ依存排除編)

というわけで、前回の続きです...φ(・ω・`)


<前回>
parrot.hatenadiary.jp

<サイト>
ccpts.parrot-studio.com

<修正したコード>
github.com


前回は「サーバサイドレンダリングSSR)」の概念的な話と、
そこから導かれる設計の概要、そしてSSRに移行するための段取りについて書きました

  1. ブラウザ系オブジェクトの排除(とりあえず実行時エラーを消す)
  2. 「仕様上の正しい動作」になるように修正
  3. インフラの調整

今回はこの一番最初、「ブラウザ系オブジェクトの排除」からやっていきます

STEP1:実行時エラーの消去

とにかくまずは「エラーが出ない」「何か画面が表示される」を目指します
前回、windowオブジェクトを排除すればエラーにならないのでは・・・という推測しましたが、
これを立証するのが最初のステップになります

(1) 二つの領域のjsと、それをつなぐProxy

具体的なRailsの話であり、前回書いた話になりますが、
Railsが扱うjsはざっくり2種類あります

Railsのassetsに属する「application.js」と、
webpackでまとめられ、SSRにも使われる「Reactを主体にしたjs」
(以下、アプリ名にあわせて「ccpts.js」と表記)です

前者は「app/assets/javascripts」、後者は「app/javascript」に存在し、
それぞれ別なルートでまとめられて一つになります*1

「application.js」は(HTMLのlayoutを経由して)クライアントのみで読み込まれますが、
「ccpts.js」サーバとクライアント双方で実行されます
この「ccpts.js」にwindowオブジェクト等が含まれるとまずいことになります(´-ω-)

ならば、windowに触れるコードをapplication.jsに追い出してしまおう・・・ということです

f:id:parrot_studio:20190124110650p:plain

まず、windowとかjQueryとか、ブラウザ系のオブジェクトに触るコードを、
「Browser」というオブジェクトに閉じ込めてしまいます

// app/assets/javascripts/browser.js の抜粋
// webpackを通さないので、TypeScriptではない

// クラス的に振る舞うグローバルオブジェクトを定義
Browser = {};

// window.location
Browser.thisPage = function () {
  return window.location.href;
}

// window.confirm
Browser.confirm = function (mes) {
  return (window.confirm(mes));
}

// jQueryに依存する処理の例
Browser.addSwipeHandler = function (div, callbackLeft, callbackRight) {
  if (!div) {
    return;
  }

  $(div).swipe({
    swipeLeft: (function (e) {
      e.preventDefault();
      callbackLeft();
    }),
    swipeRight: (function (e) {
      e.preventDefault();
      callbackRight();
    })
  });
}

元々React化した際、jQueryなどのライブラリは「application.js」に追いやられているので、
これでwindowに触るコードとライブラリが全て「application.js」に分離されたことになります *2

しかし、このままではReact側で生成したDOMと、これらの処理が連携できません

そこで、「クライアントサイドではBrowserに処理を委譲し、
サーバサイドでは適当な値を返すオブジェクト(≒クラス)」として、
「BrowserProxy」を定義します

// BrowserProxyの抜粋

// 他のコードから追い出されたdeclare
// ここにしかdeclareがないので、他のコードに外部依存がないことを保証できる
declare var window
declare var Browser

export default class BrowserProxy {

  // windowオブジェクト(とBrowserオブジェクト)の有無を判定
  public static isWindowDefined: boolean = (() => {
    if (typeof window === "undefined") {
      return false
    }
    if (typeof Browser === "undefined") {
      return false
    }
    return true
  })()

  // windowがなければ特定の値を返す例
  public static thisPage(): string {
    if (BrowserProxy.isWindowDefined) {
      return Browser.thisPage()
    }
    return "/"
  }

  // windowがあるときだけhandlerをセットする例
  public static addSwipeHandler(div, callbackLeft, callbackRight): void {
    if (BrowserProxy.isWindowDefined) {
      Browser.addSwipeHandler(div, callbackLeft, callbackRight)
    }
  }

}

このように、windowやBrowserが存在する(≒クライアントサイド)では、
まんまBrowserに処理を委譲していますが、
Browserが存在しない(≒サーバサイド)環境では何もしないか、適当な値を返しています

すでにTypeScript化しているので、外部参照している箇所は
「declare $」とか「declare location」とか、declare宣言されているので、
そこをBrowserとBrowserProxyにどんどん切り出していきます*3


呼び出す側はこんな感じです

// 関連する箇所だけ抜粋

export default class DatabaseTableArea extends ResultView<ResultViewProps> {

  private arcanaTable: HTMLTableElement | null = null

  // componentをマウントする時にハンドラをセット(ただし、クライアントならば)
  public componentDidMount(): void {
    Browser.addSwipeHandler(
      this.arcanaTable,
      this.handleLeftSwipe.bind(this),
      this.handleRightSwipe.bind(this)
    )
  }

}

この段階では、SSRと無関係に、ただコードを整理しているだけですので、
処理を切り出したらSSRなしで動作が変わらないことを確認・・・という感じで進めます

(2) 暫定的Cookie対応

ここまでの作業が終わったところで、試しにSSRに切り替えたところ、
「window not found」ではないエラーに変わりました

これはこれで一歩前進ですが、エラーの内容を調べたところ、
Cookieを読んで初期化するところで落ちてました(´-ω-)

この時点ではサーバサイドJS実行系におけるCookieの扱いがわからなかったのですが・・・

  • Cookieに触れたこと自体はエラーの原因ではない(ただ存在しなかったという挙動)
  • Cookieへの書き込みでは落ちていないように見える(この時点では挙動不明)

・・・ということで、サーバサイドでCookieのオブジェクトをparseし、
Reactの初期値として構造を丸ごと渡す・・・という強引な手にΣ(・ω・ノ)ノ

これはあくまで一時的な処置で、次のSTEP2で適切に置き換えられるのですが、
とにかくこの段階では「エラーを消す」(そしてBrowserProxy方式の妥当性を検証する)のが
最優先なので、無理矢理な手段をとりました


この修正により、ついにSSRでHTMLが吐き出され、
それっぽい画面が表示されました・・・が、
それは新たな戦いの始まりでしかなかったのです・・・

今回のまとめと次回予告

今回は実際の例として、Railsの文脈での作業について書いてみましたが、
一般的なポイントは以下です...φ(・ω・`)

  • jsを2つに分離する
    • 「クライアントでだけ実行されるもの」と「サーバ・クライアント双方で実行されるもの」
  • クライアントでだけ実行されるjsに、ブラウザ系オブジェクトを参照するコードを集める
  • ブラウザ系オブジェクトの処理を代行するProxy層を用意する

これで「表示」まで進むことができ、
SSR自体がいけそうな気がするレベルにはなったのですが、
進めていくと、「クライアント」と「サーバ」を別な形で意識させられることになります


ということで、次回「STEP2:仕様変更編」へ続きます(`・ω・´)


parrot.hatenadiary.jp

*1: 前者はsprockets、後者はwebpacker

*2: どこかで書いたと思ったら、ちゃんと書いてなかった気がするけど、とりあえずこのあたり https://parrot.hatenadiary.jp/entry/2016/02/28/113310

*3: declareについては以前の記事参照 https://parrot.hatenadiary.jp/entry/2018/08/29/171654

React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その1:概要編)

先日、「チェンクロパーティーシミュレーター」(以下「ccps」)をアップデートしまして、
サーバサイドレンダリング(いわゆる「SSR」)に対応しましたヽ(`・ω・´)ノ

ccpts.parrot-studio.com

github.com

過去の経緯はこちらを見ていただきたいのですが・・・

parrot.hatenadiary.jp

・・・すでに「React+jQuery」で動いており、「TypeScript」で書かれたSPAでございます

しかし、SPAであるがゆえに、大きな問題を抱えておりまして、
それは「とにかく初期表示が遅い」ということです(´-ω-)

これを解決するのが「サーバサイドレンダリングSSR)」なのですが、
いろいろ検索してみると、わりと否定的な意見が多くあります

やってみた上で、私もどちらかといえば否定的なのですが、
要は「どこで使うべきものなのか?」が重要だと思うのですね

SSR自体、わりと手間がかかるし、それ用の設計や手法が必要になるものであり、
私もかなり苦労させられたのですが、そこを紐解いていくためにも、
今回はあえて「当たり前のこと」から考えてみます...φ(・ω・`)

なお、かなり長くなる予定なので、概要編・実戦編と分けた上で、
全部で数回になる予定です


<第2回>
parrot.hatenadiary.jp



そもそもサーバサイドレンダリングSSR)とは何か?

冷静に考えてもらいたいのですが、
そもそも「サーバサイドでHTMLを出力し、クライアントのJavaScriptで制御する」というのは、
Webシステムとしてはごく「普通」の話です

にもかかわらず、わざわざ「SSR」なんて仰々しい言葉を定義するわけですから、
「サーバサイドでHTMLを作ること」だけではないわけです

そもそも、現代的なWebシステムにおいて、サーバサイドがAPI的なものを提供し、
クライアントが(旧来に比べるとややリッチな)jsで制御する・・・というレベルの設計は、
すでに一般的といっていいレベルだと(少なくとも私は)思ってます

その「リッチなjs」が単なる「制御」を超えて、「画面を大幅に書き換える」、
つまりは「view」を直にいじるレベルになってくると、
クライアントのview制御と、サーバサイドのview生成で、ロジックが「二重化」されることになります

これを解消するため、「viewは全部クライアントに寄せる」ことをした上で、
「画面の切り替えのレベルまで全部viewの制御に寄せる」、
「サーバサイドは純粋なAPI」という設計方針をとったのが「SPA」ということですね

しかし、「クライアントでviewを作る」ということは、
「クライアントでjsを解釈しない限り、画面が見えない」という問題があります
(いわゆる「ファーストビューが遅い」問題)

サーバでHTMLを作っていれば、少なくともその範囲ではすぐ表示されるのですが、
SPAの場合はjsの通信速度やクライアントのCPU速度など、
様々な要因が絡むので、相対的には「遅い」わけです(´-ω-)

一般的に、SPAの問題としてあげられるのはこのあたりでしょうか

  • ファーストビューが遅い
  • SEO的な問題
    • クローラーが正しくSPAを解釈できるとは限らない
    • OGPような静的なタグ出力はサーバサイドでしかできない

私のccptsの場合は「ツール」なので、編集モードで多少待たされるのはともかく、
共有されたURLから飛んでくる「PT閲覧モード」で、
編集用のコードを解釈する時間で待たされる・・・というのは、本来よろしくありません

同様に、例えばBlogやニュース記事のような、
「ユーザーがURLから飛んできて、ただ見たいだけのサイト」は、
基本的にSPAに向いてません(´・ω・`)

そういったサイトは、普通にサーバサイドでHTMLを出力すればいいわけで、
そもそも「SSR」を検討する以前の問題です

言いかえれば、まずSPAにすべきかの検討があって、
SPAが必要だけど、それでは遅いという場合に、
初めてSSRが検討される
・・・というのが本筋かと

じゃあ、SPAにできればSSRは簡単なのかというと、
そんなことはないわけで、めっちゃコストがかかります

Webで良く見る「SSR不要論」は、
「理想的ではあるけど、移行するコストに見合わない」ということだと思ってます

SSRの問題点はこんな感じかと
(あとで出てきますが、この「SSRの問題点」は、
 「SSRに移行するための段取り」と関連してきます)

  • (SPAとして完成していたとしても)実装コストがかかる
    • 「ブラウザ」で動作することを前提に、暗黙的に書かれた仕様を全て見直す必要がある
      • 例:windowオブジェクトへの参照・暗黙的なイベントへの依存
  • クライアントとサーバサイドの担当範囲があいまいになる
    • クライアントだけが知っていればよかったことを、サーバサイドでも考慮しないといけない
  • (通常のHTML出力に比べて)処理が重い
    • 例えばreact_on_railsの場合、Rubyのプロセスでjsの実行系を走らせないといけない
    • サーバサイド自体がNodeのようなjs実行系で動いている場合のコストは不明

これらのメリットとデメリットをふまえた上で、
今作ろうとしているシステムに対し、「普通のHTML出力+js」がいいのか、
「SPAのSSRがいいのか」を判断が必要になってきます

<参考>

www.publickey1.jp


STEP0:SPAをSSRにするための概要設計

(1) SSRを阻害する要因

今回のSSR移行に関しては、以前から使っていた「React on Rails」の機能を利用しています

github.com

以前は「SPAをRailsに絡めて簡単にデプロイできるツール」的に使っていたのですが、
元々SSRの機能がついてまして、SSRを試すだけならprerenderフラグをtrueにするだけです *1

<%= react_component("HelloWorld", props: @some_props, prerender: true) %>

もちろん、「それだけ」でSSRが動いてくれるなら、
React on Railsを開発している会社のコンサル業は成り立たないわけで、
そんな単純な話ではありません(´・ω・`)

「サーバサイドでjsを動かす」というのは、
Railsの文脈においては「execjs」のことを指します*2
(一般的にはNode.jsのサーバサイドプロセス・・・ですかね)

execjsはサーバサイドでのjs実行系なので、「ブラウザ」の概念がありません
SSRを試して、おそらく最初に目にするエラーは、
「windowオブジェクトがない」というものでしょう

windowと書いてなくても、例えば「location」や「confirm」のように、
暗黙的にwindowオブジェクトを参照しているものはたくさんあります
そういったものは全て、サーバサイドjsでは動作しないことになります

React世代のライブラリはともかく、
旧世代のライブラリはほぼ確実にwindowを触っているはずで、
その代表格はもちろんjQueryということになります

私のccptsにおいて、jQueryは仕様上必須です
しかし、jQueryに依存している限り、実行系がエラーを吐いてしまいます

ここが、今回の設計における最大の肝になります

(2) SSRに使うjsの制約と、その対処

ところで、「React on Railsが実行しているjsファイル」ってどれなんでしょう?
configを見ると、こんな設定項目があります

# config/initializers/react_on_rails.rb から抜粋

ReactOnRails.configure do |config|
  # buildに使うコマンド
  config.build_production_command = 'RAILS_ENV=production bin/webpack'

  # SSRで実行するjs名
  config.server_bundle_js_file = 'ccpts.js'
end

ここで指定している「ccpts.js」がどこにあるかというと、
エラーメッセージから追った結果、「public/packs/ccpts.js」にありますが、
これは「app/javascript」以下のjsをwebpackで固めたファイルになります

React on Railsに限らず、SSRしようとした場合、
webpack等で固めた単一のjsを、クライアントとサーバで共有し、
サーバサイドでも実行する・・・という流れになります

そして・・・ここがポイントなのですが・・・

裏を返せば、「サーバサイドで実行するjsにwindowオブジェクト等は含まれず、
クライアントで実行する際には含まれる」
という状況が作れれば解決するはずなのです

実際、これを試したことでSSRに成功したのですが、
概要を図にまとめるとこうなります

f:id:parrot_studio:20190124110650p:plain
ccptsのJS概要図

解説については次回以降やっていきますが、
私が立てたSSR化の段取りは以下のようになります
(まさに、先ほど挙げた「SSRの問題点」と対応しております)

  1. ブラウザ系オブジェクトの排除(とりあえず実行時エラーを消す)
  2. 「仕様上の正しい動作」になるように修正
  3. インフラの調整

次回は「実践編」として、先ほどの図の解説をしながら、
どのように先ほどの問題を解決したのか・・・を書いていきます...φ(・ω・`)


<続き>

parrot.hatenadiary.jp

*1: しかも、サーバで出力したReactのHTMLと、クライアントのReactが管理するDOMで、いい感じに制御してくれるΣ(・ω・ノ)ノ

*2: 厳密にいえば、execjsはRubyプロセスでjsの実行系を動かすためのインターフェースであり、実際の実行系はインストールしてあるものから選択される

ES6で書いたReactのアプリをTypeScriptに移行した件(+そこに至るまでの歴史)

これだけ読めばいい(かもしれない)概要

めちゃくちゃ長くなってしまったので、先に概要だけ...φ(・ω・`)

  • クライアントサイドのコードが複雑化してきたので、TypeScriptの適用領域が広がっている
  • いきなりTypeScriptを導入するのではなく、まずES6で整理してみる
  • 汎用型「any」を使いながら、見える範囲で徐々に型を埋めていけば良い
  • 外部ライブラリは型定義を追加してからimport
  • jQueryとかグローバルオブジェクトはdeclareを使って宣言
  • TypeScriptを書く時にVSCodeをつかうとめっちゃ便利
  • interfaceで「ゆるい制約」をかけられるのが便利
  • うまくinterfaceを使えば、Reactのpropsやstateの型もチェックできる
  • 型が導入されても、テストが不要なわけではない

以下、めっちゃ長い話になってますので、後はお好みで(´・ω・)っ


TypeScriptを導入した経緯


先日はとある事情からGoを試したわけですが・・・

parrot.hatenadiary.jp

・・・その際に、もう一つ「伸びている言語」として挙げられていたのが、
「TypeScript」でございます

www.typescriptlang.org

2012年に初めて発表された際は、まだクライアント側の開発が成熟しておらず、
ブラウザのDOMをいじる程度の用途であれば、まだ生のJavaScriptが優勢だったと思います

当時はそれこそ山のようにAltJSが存在し、クライアントのフレームワーク同様、
何もスタンダートがない状況で、トランスパイルの手間を考えても、
RailsCoffeeScriptが便利・・・くらいだったかと(´-ω-)

「あくまでJavaScriptの拡張として型を導入したもの」という触れ込みから、
出たばかりのTypeScriptをいじってみた記憶はありますが、
やはり「当時のJSのスタンダード」からは逸脱しており、諦めた記憶が *1

しかし、今や時代は変わりました

Webアプリケーションの主体はクライアント=ブラウザとなって複雑化したり、
Babel+Webpackがほぼ主流に定まったことにより、
「AltJSをトランスパイルしてデプロイする」というのも一般化してきました(`・ω・´)

「標準的なJavaScript」も進化し、ES2015(いわゆるES6)により、
他の言語にありがちなパラダイムが、「標準的なJS」として書けるようにもなっています
(それでもまだ、古い環境向けのトランスパイルは必要ですが)

明示的に「クラス(もちろん継承も)」が存在したり、
最大のネックだったスコープ問題の解決など、最近の言語を知るプログラマからすると、
「やっとできるようになったか」という内容が満載です

qiita.com

現在のTypeScriptは実質、「ES6の仕様を拡張したもの」*2として考えることができます
特に「型」の概念を明確に導入していることで、大規模開発に有利なのもポイントです

そしてなにより・・・以前とは比べものにならないほど、採用事例が増えています
あの(Dartを開発していた)Googleですら、TypeScriptは「標準言語」ですΣ(・ω・ノ)ノ

www.publickey1.jp

先日のGoに比べると、「JavaScriptで書かれたそこそこ大きなアプリ」であれば適用できるため、
採用する基準がわかりやすいのも利点かと

少なくとも、「ES6で書かれ、Reactの採用を考えなければならないサイズのアプリ」であれば、
TypeScriptを導入する価値は十分にあると考えられます

・・・まあ、結局はやってみたかったからやったのですが(´-ω-)

ccptsにおけるクライアントアーキテクチャの変遷

長い導入はさておいて、ここから本題に対する長い前置きです...φ(・ω・`)

ということで今回は、「チェンクロパーティーシミュレーター」(以下ccpts)を、
TypeScriptで書きなおした・・・というお話なのですが、
そのためにはまず、そこに至るまでの経緯から説明しないといけません

ccpts.parrot-studio.com

github.com

開発に着手した2014年当時の私は、まだクライアント側の経験が浅く、
スマホタブレットで見ることを前提としたクライアント主体のアプリ」として、
ccptsのコンセプトを定義しました

  • jQueryでイベントを定義しつつ、簡単なmodel状のもので整理する
    • 初期リリース当時のコードを見たところ、Classが2つで1ファイル、450行程度のCoffeeScriptΣ(・ω・ノ)ノ
  • 機能の増加に合わせて、modelの細分化やviewの複雑化が進む
    • 特にチェンクロの2部実装で「絆システム」が導入され、急に複雑化した
  • 2015年頃、Reactを試そうとして断念する
    • Reduxの「データをどこかで一元管理する」というやり方になじめなかった
    • 当時はreact-bootstrapがまだ実用レベルではなかった(あるいは私の理解が足りなかった)
  • 仕事でNode.jsのサーバを書いている際、FRPの概念を知り、試したくなる
    • https://parrot.hatenadiary.jp/entry/2015/11/29/175113
    • 結果的にBacon.js+Lodashという組み合わせでコードが整理され、イベントがストリーム化される
    • これにより「view」と「イベント→データ」の流れが分離される(とても重要)
  • 2016年の始め、Bowerからnpm+Browserifyに移行する
  • これをきっかけに本格的なReact化プロジェクトを開始
  • 2016年末、Webpackを導入
  • (以降、しばらくはチェンクロの3部対応=データ周りの仕様変更に追われる)
  • 2017年の夏、Rails5.1+React on RailsRoR)の組み合わせで設計が整理される
    • RoRのreact_component機能を利用し、サーバサイドとviewの関係をきれいに整理
    • この時の作業をBlogにまとめるつもりが、さぼっていたΣ(゚Д゚)ガーン
  • (ここからもしばらく、データ周りの仕様変更やサーバサイドのアーキテクチャ変更)
  • 2018年の春、チェンクロのヒロイックスキル対応(の準備)を始める
  • 2018年の夏、ES6のコードをTypeScriptに書き直す <- イマココ


要するに何が言いたいのかというと、急にTypeScriptが導入されたわけではなく、
時代にあわせて段階的にクライアントのコードが整理されていき、
TypeScript周りの情報も整理されていった結果、わりと低コストで導入できた・・・ということです

最初から「よーし、ReactをTypeScriptで書いてアプリつくっちゃうぞー」だったら破綻してます
Reactを導入するようなサイズのアプリをいきなり設計するのは難しいですし、
Reactを使いたいならまず、ES6から入った方が問題が少ないわけで(´-ω-)

TypeScript導入の実例

お待たせしました、本題です...φ(・ω・`)

TypeScriptを導入してみたときに感じたことを、
いくつかのテーマに分けて書いていってみます

(1) まずはmodelから段階導入

TypeScriptが「ES6に型を導入したもの」くらいなのはわかっていたので、
言語仕様の把握もそこそこに、model周りから移行を開始しましたΣ(・ω・ノ)ノ

modelは単体、あるいはmodel同士の関連で完結しているものが多いですし、
「型」の代表格は「Class」なわけですから、導入も簡単だろうと

実際、「model」と「lib」・・・つまり、React以外のコードを書きなおし、
ここで一度リリースしました

すでに動いている「Class」に「型」を定義していくだけなので、
作業自体はほぼ機械的です

クラスメソッドやクラス定数を使えるので、
この点でもES6のコードより厳密ですっきりします(`・ω・´)

この段階ではTypeScript自体に慣れていないのもあり、
外から受け取るオブジェクトは「any」でごまかしてしまい、
modelの文脈で型がわかるものだけを定義していきました

eslintに代わる「tslint」がありますし、型がおかしい場合は書いている時点でエラーになるので、
詰まるたびに調べて・・・という感じで言語仕様を把握していきました...φ(・ω・`)

palantir.github.io

ほぼeslintの上位互換になっていて、
eslintの項目に「型」に関わるルールを追加したもの・・・というのが私の理解です

このように、段階的に型を詳細化していくだけで十分「使える」のですが、
最終的に「anyみたいなあいまいなのは嫌!」ってなった場合、
tslintでanyをチェックすることもできるので、固まってきたら導入してもいいかもしれません

(2) デプロイ時における、グローバル変数jQuery等)の扱い

そもそも、Rails+RoRでES6をデプロイする仕組みが存在し、
そこにTypeScriptのトランスパイルを加えるだけなので、
デプロイの手順自体は今まで通り変わりません

ES6のコードとTypeScriptのコードが混在することになりますが、
それぞれいい感じに生のJavaScriptにトランスパイルされる(つまり、型情報は全て消える)ので、
最終的なコードで競合する心配はありません(`・ω・´)

TypeScriptの「型」は、あくまでTypeScriptの文脈でチェックされるだけなので、
極端な話、TypeScript側の型定義が間違っていたとしても、
ES6側で正しいObjectを渡している限り、動作上は何の問題もありません
(実際、全く異なる型定義になっていたが、特に問題なく動いていた)

引っかかったのは、今回もやはりjQueryの扱いです(´-ω-)

ローカルで動かしているときはなんともなかったのですが、
いざ本番にデプロイし、Railsのassets:precompileを実行したところでエラーに

ccptsはReactとjQueryを共存させており、特にjQueryはグローバルに存在する必要があるのですが、
TypeScriptの中で普通にimportしてしまうと、外のjQueryと別になる・・・という、
まさに以前と同じ問題が発生します

ES6の時点では、eslintのレイヤーで問題になるだけで、
eslintcでグローバルであると指定すればよく、ES6側で何か対応する必要はありませんでした
(ただ放置しておけば参照できた)

しかし、TypeScriptは厳密なので、「外から導入される、型が不明なもの」に対して、
明確にエラーになりますΣ(゚Д゚)ガーン

これはjQueryに限った話ではなく、そもそもLodashやBacon.jsも外のライブラリであり、
何らかのimportをおこなう場合、「型定義」を導入して、型情報を与える必要があります

$ yarn add @types/baconjs

現在は主要なライブラリのほとんどに型定義が存在し、
「@types/hoge」を加えるだけで、型情報が追加されるのですが、
これが昔はめっちゃ面倒だったのです(´-ω-)

しかし、jQueryの場合はグローバルなオブジェクトなので、
結論からいえば以下のような宣言が必要になります
(実は、「@types/hoge」の中身は、ひたすらdeclareをまとめたファイルだったりします)

import * as _ from "lodash" // @types/lodashの型定義を参照してくれる
declare var $ // jQueryがグローバルから導入されるという宣言

同様に、ブラウザのwindow由来のグローバルオブジェクトもdeclareを記述するようにしてます *3

以前も書いた話ですが、jQueryで何でもやっちゃうから問題なのであって、
それが必要なレイヤーにおいて、エンジニアの都合で制限をかけるのはやりたくないので、
あえてこのような方法を追求している面もあります

・・・まあ、私自身がjQueryがないと書けないからってのが大きいですがΣ(・ω・ノ)ノ

(3) ReactへのTypeScript適用

独立したmodel関連をリリースし、TypeScriptにも慣れてきたところで、
いよいよReactのjsxをtsxに差しかえる作業を開始したのですが、
基本的には拡張子を変えて、エラーになったところを修正していく感じでいけます

問題は、「型」に関してもう一歩踏み込む必要があることです
model周りは基本の型と自分のClassを見ていればよかったのですが、
React周りには多数の「型」が存在します

  • HTML(というよりDOM)の型
    • 例:HTMLSelectElement
  • JSXの型
    • 例:JSX.Element
  • Reactの型
    • 例:React.FormEvent

これらをガンガン適用していくわけですが、
実は私、これらの型に関するドキュメントを一切読んでいませんΣ(゚Д゚)ガーン

いや、読んだ方がいいのは確実なのですが、
それが不要なレベルで、VSCodeの支援機能が強力なのです

code.visualstudio.com

VSCode上で何らかのオブジェクトをポイントすると、
型の定義を推論して提示してくれるので、
それを参考に順番に埋めていっただけです

f:id:parrot_studio:20180828111753p:plain

もちろん、「すでに動いているアプリ」なので、
こういった裏技(というより邪道)に近いやり方が可能なのですが、
AtomからVSCodeになんとなく移行して、初めて「VSCodeすごい」と思いました(`・ω・´)

しかも、この型推論はES6の文脈でも発揮されます

ES6の文脈で使われているオブジェクトに型情報があった場合、
同じように推論してくれるので、それを参考にして厳密な処理を書くことが可能です

実際、modelに型を導入し、React側がまだES6で書かれている時点で、
かなりの数の潜在的バグを、型定義から見つけて排除することができました(`・ω・´) b

ある程度コードの規模が大きくなってくると、
「これどこから来たんだっけ?」というのがわからなくなりますが、
IDEの支援を駆使し、型を手がかりにすることで、かなり把握しやすくなるわけです

Lodashの「_.chain」とか、Bacon.jsの「Bacon.Bus」はとても便利な仕組みですが、
つないでいくうちに何をつないでいるのかわからなくなってしまいがちです
そんな複雑な高階関数であっても、型定義のおかげで安心して書けます...φ(・ω・`)

(4) ReactのPropTypesの代替としてのinterface定義

逸れてきたので話をReactに戻すと、元々のReactのコードにはPropTypesが定義されてませんでした

reactjs.org

これはもう、完全に私の怠慢なんですが、今回TypeScriptを導入するにあたり、
当然ながらpropsやstateにも型が欲しくなるので、きちっと定義しました

// interfaceで型を定義する
interface LatestInfoAreaProps {
  latestInfo: LatestInfo // interfaceが定義されたObject
}

interface LatestInfoAreaState {
  visible: boolean
}

// React.Componentにinterfaceを渡す
export default class LatestInfoArea 
  extends React.Component<LatestInfoAreaProps, LatestInfoAreaState> {

  constructor(props: LatestInfoAreaProps) {
    super(props)

    this.state = {
      // constructor内で初期化してないと、ちゃんとコンパイルエラーになる
      visible: true
    }
  }
// ...
}

このように、Reactの機能とは無関係に、TypeScriptの文脈で細かく型を指定できるため、
PropTypesを使わなくても同じようなことができます(`・ω・´)

「PropTypes.oneOf」に相当することもTypeScriptの範囲でできます

interface SummaryMemberProps extends MemberRendererProps {
  // props.viewは型としてはstringだが、値はこのどちらかしかとれない
  view: "full" | "chain"
}

export default class SummaryMember
  extends MemberRenderer<SummaryMemberProps> {
  // ...
}

また、componentに継承関係をつける際も、
親クラス側でpropsのinterfaceに制約をかけることができます

// 一番基底のクラスではただのジェネリクス
// abstractも明示できる
export default abstract class ArcanaRenderer<T> extends React.Component<T> {
}

// 継承する子クラスではinterfaceで制限をかける
export interface MemberRendererProps {
  member: Member | null
}

// 総称型TはMemberRendererPropsを継承している必要がある
//    -> 「props.memberを持つ」という制約
export abstract class MemberRenderer<T extends MemberRendererProps>
  extends ArcanaRenderer<T> {
}

// SummaryMemberPropsはMemberRendererPropsを継承している=props.memberを持つ
interface SummaryMemberProps extends MemberRendererProps {
  // 継承しているので、「member: Member | null」も定義に含まれる
  view: "full" | "chain"
}

// SummaryMemberPropsはMemberRendererの制約を満たす
export default class SummaryMember 
  extends MemberRenderer<SummaryMemberProps> {
  // ...
}

こういう継承構造を、PropsTypes+ES6のクラス定義だけで書いていくと、
間違いなくどこかで抜けが発生するとか、理解できなくなりそうですが、
TypeScriptだと、このように厳密な定義が書けるわけです(`・ω・´) b

(5) その他、言語仕様に関わる雑多なこと

書こうと思っていたけど、流れに乗らなかったのことを最後にまとめて...φ(・ω・`)


例えば、あるプロパティにnullが設定される可能性があったとします

f:id:parrot_studio:20180828135641p:plain

このままこのオブジェクトのメソッドを呼ぶと、
nullの可能性があるため「エラー」になります(警告ではありません)

その代わり、上記のようにnullチェックを通した後だと、型推論からnullが排除されます

f:id:parrot_studio:20180828135825p:plain

つまり、ちゃんと文脈を見ているということなので、とても便利です(`・ω・´) b
(なお、TypeScriptでは「null」と「undefined」は別の型です)


あと、先ほど出てきた「interface」、Javaなどでも制約をかけるのに使われますが、
TypeScriptの場合、Objectそのものに「ゆるい制約」をかけるために使われます

classにinterfaceをimplementsするJavaに対し、
TypeScriptではObjectにラベルを貼るような感覚で使えます

// 「お気に入り」オブジェクトの構造(key -> boolean)
export interface FavoritesParams {
  [key: string]: boolean
}

export default class Favorites {

  // FavoritesParams型のクラス定数(初期値は空Object)
  private static favorites: FavoritesParams = {}

  // 一見複雑なchainに見えるが、いい感じに型推論してくれる
  public static list(): string[] {
    return _.chain(Favorites.favorites)
      .map((s, c) => { // s: boolean c: string
        if (s) {
          return c
        }
        return ""
      }).compact().value().sort()
  }
}

interfaceのポイントは、あくまでTypeScriptにおける制約でしかないので、
ES6の文脈でも、トランスパイルされたコードでも、ただのObjectとして扱えることです
そのため、クラスよりも「ゆるい制約」として気軽に使いまくれます(`・ω・´)

終わりに

というわけで、「ある程度の規模のアプリだとTypeScriptは便利だよ」って話でした

ただ、あくまでTypeScriptが確認してくれるのは「型の整合性」だけです
「型の定義が誤ったまま整合性が取れてしまっている」とか、
「渡された値そのものに問題がある」といった問題は検知できません(´-ω-)

それでも、最低限の「nullが渡されて落ちた」とか、
「全然違うObjectが渡されて落ちた」といった、
基本的なやらかしは検知して防ぐことが可能です

大規模になるほど、こういったつまらないミスを排除して、
仕様やデータレベルのテストに注力したいはずですし、
型を決めることで共同作業もやりやすくなる・・・といったメリットは十分にあります

生のJavaScriptで十分な領域にはオーバースペックですが、
ES6で細かく書きたいレベルであれば、
基本的な型のレベルから試してみるといいと思います(`・ω・´)

*1:JavaScript Good Parts」がまだ主流だった時代です 今でもJSの基本的な仕様について知るにはとても良い本ですが、設計の面ではどうしても古いかと(´-ω-)

*2: 先行してES7の概念を取り込んでいたり、C# の利点を取り込みつつ、あくまでJSの範疇に収めたもの・・・と考えると、非常に良くできた言語だと思いませんか(´・ω・)?

*3: ちなみに「declare var $: JQueryStatic」とすることで、より厳密に型が定義できるのですが、どうしてもanyを排除したいとか、全体でjQueryを多用するのでもない限り、大丈夫かと