ぱろっと・すたじお

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

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を多用するのでもない限り、大丈夫かと

Goをやらないとまずいと言われたので触りつつ、適用範囲を考える

まあ、きっかけは流れてきたこれなんですが・・・

www.benfrederickson.com

・・・突っ込みどころはあるのですが、そこはおまけに回すとして、
重要なのは「Goが伸びている」ということですよね

前から一度くらいGoに触ろうとは思っていたものの、そのきっかけがなかったので、
せっかくなのでGoを軽く書いてみたわけですが・・・

github.com

・・・結論からいえばRailsっぽいものをGoで書こうとするな」ってことですΣ(・ω・ノ)ノ

Goの環境構築(GOPATHの罠)

インストールはMacなら「brew install golang」とかするだけなのですが、
ややはまりどころなのが「GOPATH」の設定です

GOPATHは全てのコードが配置されるベースPATHです
それこそ、自分のコードもライブラリも関係なく「全部」です
Goのimportは全て、GOPATHからの相対PATHとして定義されています

逆に言えば、全てのGo関連のコードに同じPATHが強制されるため、
(gemやyarnのように)バージョン管理をするという概念はありません *1

# .bash_profileや.envrc等
export GOPATH="/path/to/gopath"
export PATH="$GOPATH/bin:$PATH"

こんな感じで、GOPATH/binにもPATHを通しておけば、より安心かと(`・ω・´)

Goの仕様

Amazonにちょうどいい本があったので、ポチってみましたが・・・

・・・1時間もかからずに概要は把握できますΣ(・ω・ノ)ノ

言語仕様に難しい要素はないというより、
意図的に徹底して複雑なパラダイムを排除しているくらいです

プロとして何らかの言語で実績があるプログラマはもちろん
大学で軽くCを触ったことがあるくらいでも、おそらく書けてしまいますし、
まさにそれがGoの重要な「設計思想」だと思います *2

「誰が書いても一定のクオリティのコードが強制される」という意味では、
Pythonに近い思想ですが、圧倒的に簡単です

Pythonのように言語自体がインデントを強制するのではなく、
gofmtというフォーマッタをデフォルトで持っており、
そういった意味でもPythonより柔軟で現実的な仕様だと思います(`・ω・´)

なぜGo言語 (golang) はよい言語なのか・Goでプログラムを書くべき理由


RailsっぽいものをGoで書いてみる

こうなると、何かをGoで実装してみようという話になるのですが、
チェンクロパーティーシミュレーター(以下ccpts)のAPI層をGoで書いてみることに

Get our light! - チェンクロ パーティーシミュレーター

GitHub - parrot-studio/cc-pt-viewer

  • DBの定義やデータはそのまま使用
  • APIにアクセスしたら「最新アルカナ一覧」をJSONで返す

サンプル的にまずはこれだけ...φ(・ω・`)

どんな言語でも、最近は「Railsっぽいもの」がたいていあるので、
軽く探してみたところ、こちらが見つかりました

https://revel.github.io/Railsっぽいもの)

http://doc.gorm.io/ActiveRecordっぽいもの)

後者はマイグレーションとか備えていてなかなかですが、
今回は不要なので、接続とmodel定義部分だけ使いました

package models

type Arcana struct {
	Id             uint64 `gorm:"primary_key" json:"id"`
	Name           string `sql:"size:100" json:"name"`
	Title          string `sql:"size:100" json:"title"`
	ArcanaType     string `sql:"size:20" json:"arcana_type"`
	Rarity         uint32 `json:"rarity"`
	Cost           uint32 `json:"cost"`
         // 以下略
}

こんな風に書くだけで、JSONへのシリアライズも定義できるのは楽ですね(`・ω・´) b

JSONを返すController部分も本質的にはこれだけです

func (c Api) Index() revel.Result {
	arcanas := []models.Arcana{}
	DB.Limit(20).Order("id desc").Find(&arcanas) // なんとなくActiveRecordっぽくかける
	return c.RenderJSON(arcanas) // リストを渡すだけでいい感じにJSONにしてくれる
}

あとは、revel経由でコンパイル・実行するだけで、APIが動いてしまいますΣ(・ω・ノ)ノ

$ revel run ccptsgo
# http://localhost9000/api にアクセスするとJSONが返る

コンパイルもびっくりするほど速く、「rails s」したときより速いかもしれませんΣ(゚Д゚)ガーン

こうなると、うまいこと関連のmodelとかもJSONで返せるのでは・・・と、
調べようと思ったところで、ふと思ったのです

果たしてこのままccptsをGoで実装することに意味はあるのか・・・とΣ(・ω・ノ)ノ

一本APIができたことで、全体の工程がおぼろげながら把握できるようになったわけですが、
それにより逆に「Railsが暗黙的にやっていることをRevelで実装するコスト」も見えてきまして、
その多大なコストをかけてGoで書いたところで、コストとメリットが釣り合わないよね・・・と*3

Goの使いどころ

そもそも、Goが出てきた経緯を考えれば、
Goは「大規模なコードを書くための言語」ではなく、
「大規模システムを構成する "小さなコンテナ" を書くために特化した軽量言語」なのです

GoogleMapReduceを発表してから今まで、
大規模システムは「小さな何かが協調して大きなデータを処理する」という形で進化してきました

「小さな何か」はクラスタ*4だったり軽量プロセス*5だったりしましたが、
最近だとOSとアプリが一体化した「軽量コンテナ」になっていて、
そのコンテナの上で動くバイナリを実装する言語としてGoをとらえるとしっくりきます

だからこそ、Goはクロスコンパイラを持っているのです
コンテナのOSはそれ自体が超軽量で特殊な環境であり、
プログラマが使っているOSとは全く異なるはずですからね

先日もDockerに関してちょっと書きました*6が、
マイクロアーキテクチャをベースにした時代にあわせて、
同じようにDockerという「軽量コンテナをデプロイする仕組み」ができたと考えられます

すごく極端な例で言えば、Railsアーキテクチャをこのように分割するのがコンテナです
(これが正しい設計はどうかはともかく)

# [xxx] それぞれがコンテナ
[リクエスト受け付け] IN:テキスト OUT:リクエスト情報テーブル 
-> [リクエストのparse] IN:リクエスト情報テーブル OUT:リクエスト情報(header/body/etc...)
-> [ルーティングの選択] IN:リクエスト情報 OUT:処理先コンテナ名
->[リクエストの処理] IN:リクエスト情報 OUT:レスポンス情報
  -> [パラメータのparse] IN:リクエスト情報 OUT:パラメータテーブル
  -> [model取得(検索)] IN: リクエスト情報 OUT:modelの配列
    -> [データリソースへのアクセス] IN:クエリ OUT:結果
    -> [結果をmodelに格納] IN:結果 OUT:modelの配列
 -> [結果の構築] IN:modelの配列 OUT:レスポンス情報

「一つのメソッド」が「一つのコンテナ」くらいの粒度で考えれば、
Goがシンプルな構造しか持たないとしても、なんら問題がないことがわかると思います

先ほどのサイトにもこういう問題が挙げられていましたが・・・

Go言語(Golang) はまりどころと解決策

・・・そもそも、これが問題にならない粒度の「小さなコード」に使うべきなんだろうなと

「こんな非効率なことは意味がない」と思うかもしれませんが、
MapReduceが適用されるような)大規模なデータを扱う場合においては、
モノリシックなアプリの常識は通用しません(´-ω-)

逆に言えば、Goを適用するような「軽量コンテナの集合体」の場合、
「どのようなコンテナに分割するのか?」という設計が最重要課題になります

小説ではありますが、こちらに登場する「Vilocony」と、
その上に構築された「KNGSSS」こそ、
(言語がJavaとはいえ)まさにコンテナを用いた大規模システム開発の事例と言えるでしょう

el.jibun.atmarkit.co.jp

現実でもコンテナの運用はいろいろ難しいらしく、
白川さんの能力と執念がなければカットオーバーは難しかった気はしますが、
わかりやすい事例です

そもそも、Go自体が関数型的な仕様を持っていなかったとしても、
コンテナを用いたシステムの設計そのものに関数型的な思想が必須になるわけで、
関数型的な知識がGoで構築されたシステムに適用できないなんてことはないはずなんですよね(´-ω-)

関数型的な設計でよく出てくるのがUNIXの思想で、
「小さなプログラムをつないで処理」が、「小さなコンテナをつないで処理」になっているだけです
その意味では、UNIXは立派な「マイクロアーキテクチャのシステム」です

まとめ

Goの設計思想を考えれば、「コンテナで構築された大規模システム向け」だとは思うのですが、
今までのモノリシックなシステムで適用できないのか・・・というと、
全くできないってことはないはずです

(私のような)「普通のエンジニアが考える普通のWebサービス」は、
おそらくRuby/PHP/Python等の方が実装工数が小さく、
必要なライブラリが揃っており、リリースまでの工数を小さくできると思います

そういったシステムが肥大化していって、
パフォーマンスが要求された場合に、裏側の一部をGoのコンテナに置き換えていく、
なんて改善計画は十分にありだと思います *7

幸い、Goは前述の通り「言語仕様がめっちゃシンプル」なので、
「何らかの言語を触ったことがあればとりあえず書ける」という利点があります

frasco.io

こちらの記事でも、「普通に書くだけでかなりのパフォーマンスが期待でき、
チューニングの時間を短縮できる」と書いてますが、
一方で、「CURDを扱うだけの普通のAPIPythonがいい」とも書いてあります

やはり・・・

システムをうまくレイヤー分けして、それぞれの処理が得意な処理系(言語)を選ぶ

・・・というのが大事ですね(´-ω-)



(そしてまたおまけに続く・・・)

*1: ここは批判の対象になっているようですが、後述するように、「本来のGoの適用領域」からするとむしろ、「一つのバイナリ単位で環境を分ける」くらいでもいいのでは・・・と思います なんなら、direnvを使ってプロジェクトディレクトリごとにGOPATHを切り替えれば解決するわけですし

*2: 強いていえば、「構造体」といわれてメモリ空間が意識できるなら、という前提はあるかもしれません まあ、どんな言語であれ、メモリ空間を意識できるかは重要です

*3: なぜ釣り合わないのかについて、一度は長々書いたのですが、どんどん本題からずれていってしまうので、ばっさり切りましたΣ(・ω・ノ)ノ

*4: 初期のGCEとかHadoopとか

*5: ErlangとかScalaのAkkaとか

*6: https://parrot.hatenadiary.jp/entry/2018/07/27/162939

*7: かつてもRubyで書いたシステムのバックエンドをScalaで置き換えるとかありましたよね? わりと有名な「Twitter」ってサービスですがΣ(・ω・ノ)ノ

続きを読む

性能と無関係にUnicornからPumaに移行した件

今回の結論を先に書けば・・・

CapistranoとPumaをあわせて使うとめっちゃはかどる」

・・・って話でございます

Unicornにこだわりがなければ、Pumaは便利だと思います、以上Σ(・ω・ノ)ノ

(私には必要だが一般的には無視していい)前置きというか経緯

そもそもCapistranoはお仕事で使ったことがありましたが、
チェンクロパーティーシミュレーター(以下、ccpts)ではまだ導入してませんでした

お仕事でごりごり使っていたのはRails4.2とかの頃で、
Passengerは重たいよねって認識*1から、Unicornを当たり前のように使っており、
ccptsでもNginx+Unicornを手動デプロイで運用してました

ccpts.parrot-studio.com

別に手動でも困ってなかったのですが、一つだけ問題がありまして

サーバダウンから再起動した際、サービスも一緒に起動してほしかったので、
init.dから(ccpts内の)unicornを起動する仕組みにしていたのですが、
実行ユーザーを考えてなかったので、いろいろなファイルがroot権限になってしまいましてΣ(゚Д゚)ガーン *2

それを直すのも面倒なのでそのまま運用していたのですが、違和感があったのも事実で、
そこを修正するついでにデプロイプロセスを改善したいってのもあり、
最初はDockerを検討したわけですよ、Dockerを

最近はいくらでもドキュメントがあるので、DB+Redis+Railsのコンテナを作成し、
サービスを起動するところまではできたわけです

・・・もちろん、ローカルで(´-ω-)

問題は「これをどうやってデプロイするか?」とか、
「修正からデプロイまでどうやって運用するか」なのですが・・・
ccptsのような「普通のサイト」だと、あまりに重すぎなんですよね

たぶん、「Dockerでデプロイする環境」は作れます
ただ、それが「メリットをデメリットが上回った状態」なのかというと、
激しく疑問だったわけです(´・ω・`) *3

とはいえ、「メリットをデメリットが上回った状態」を証明するには、
「もっと低コストなデプロイプロセスで回せる」ことを示す必要があり、
Capistranoならもっと低コストなはずだよね・・・?」という流れでのCapistranoでございます

CapistranoでPumaだと、Unironより何がいいのか?

前置きが長かったので、結論から先に...φ(・ω・`)

  • unicornと違い、Rails5.xでは標準でpumaがついてくる(追加でgemを管理しなくていい)
  • puma.rbをcapistrano3-pumaが自動生成してくれるので、pumaのconfigを書かなくていい


Rails5.0からWebrickを捨ててPumaに移行したのは、
ActionCableを使うのに、スレッドベースのAPサーバが必要だったからですが、
そもそもUnicornとPumaは昔からライバル関係にはあったわけです

とはいえ、Unicornのノウハウが成熟しているに比べ、
Rails5.0RC当時のPumaはまだ詰まるポイントが多く、
ccptsのRails5移行の時にも結構苦労した記憶があります(´-ω-)

それでもやはり、「Rails標準」の勢いは重要で、
久々にCapistranoの記事を探したら、みんなPumaとの連動になっていて、
私も試してみるか・・・と、軽い気持ちだったのですが、これがめっちゃ便利なのです

そもそも、Unicornはmasterプロセスとworkerプロセスが協調して動き、
workerを増減させたり、メモリを使いすぎたら再起動させたり・・・というのが、
無停止で行えるってのがポイントでした*4

とはいえ、そのworkerがDBへのコネクションを持ったままだと、
workerが作り直されるたびにリソースを食い尽くしてしまうので、
おまじないのようにこんなconfigを書いていたはずです

https://github.com/tablexi/capistrano3-unicorn/blob/master/examples/unicorn.rb

毎回そんなのが必要ならば、いっそう自動生成すればええやん・・・というのが、
capistrano3-pumaのえらい点でございます(`・ω・´)

github.com

例えば、deploy.rbにこんな感じで書くだけで、shared/puma.rbを自動生成してくれます
(sharedに作ってくれるあたり、実に気が利いてます)

# puma
set :puma_threads,    [4, 16]
set :puma_workers,    0
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.access.log"
set :puma_error_log,  "#{release_path}/log/puma.error.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true # DBコネクション周りのおまじないがこの1行でOK

怖くて試していないのですが、どうもNginx等のconfigも生成できるようで、
ssl用の鍵指定までできるあたり、かなり本格的に使えるように見えます

もちろん、pumaの再起動も「cap [stage] puma:restart」とかするだけですし、
非常に簡単ですねヽ(`・ω・´)ノ

ccptsの場合、元々Unicornのソケットにリクエストを流していたので、
それをPumaに変えるだけ*5で、移行があっさり完了してしまいました


そんなわけで、あくまで運用上の観点から、UnicornをPumaに置き換えたわけですが、
Rails5系を使っていて、まだUnicornで動かしているって方は、
一度Pumaを評価してみてはいかがでしょうか(´・ω・)っ

*1: WebサーバとAPサーバを個別にスケールできない・・・とか、Webサーバで503を返して、APサーバ側を更新したり・・・といった運用を考えると、Passengerはめんどいのです(´-ω-)

*2: その時に書いた記事 https://parrot.hatenadiary.jp/entry/2014/10/29/123509

*3: デプロイのたびにコンテナ構築に10分とかかかり、そこから数百MBのコンテナを転送し、それを本番でpullしてきて・・・という流れを、修正のたびに回すのは(たとえ自動化しても)辛すぎます 「超軽量なコンテナを組み合わせたシステム(それこそ、AmazonのLambdaのような)」なら使いやすいのですが、Railsのような、それ自体が巨大ライブラリの塊で、モノリシックなシステムの場合、あまり向かないのではと(´-ω-) 極端な話、「サーバごとimmutableにしてデプロイ時に差しかえる」という(古典的だけど確実な)手もあるわけですし・・・

*4: Apache等もgraceful restartできますが、まあ置いておいて・・・Σ(・ω・ノ)ノ

*5: 一応、WebのrootをCapistranoを想定したpublicに変えましたが、Pumaに移行したから発生した修正ではないです

続きを読む

Chromeに煽られたので、あまり使ってないサイトもhttps対応する

一年半ぶりの更新になります...φ(・ω・`)

その間にも「チェンクロパーティーシミュレーター」(以下、ccpts)に
大量の技術的な更新を入れていたのですが、
ついつい仕事を優先してBlogを書いてませんでした

ccpts.parrot-studio.com

github.com

  • ccptsの主な更新内容

その中のトピックスの一つが「(ccptsの)https対応」で、
昨年の冬くらいに対応し、暗号化方式でなんやかんや悩んだ記憶があるのですが、
とりあえず置いておいて・・・Σ(・ω・ノ)ノ

昨日、Chromeがアップデートされ、「httpは安全ではない」と表示されるようになりました

forest.watch.impress.co.jp

実質まともに動かしているのはccptsくらいなのですが、
念のためポータル的なサイトの方も対応しておくことに...φ(・ω・`) *1

https://parrot-studio.com/

とはいえ、現在は「Let's Encrypt」という素敵な仕組みが存在し、
やろうと思えば数分でhttps対応が終わってしまうご時世です

letsencrypt.org

letsencrypt.jp

ccptsをhttps対応した頃は、まだツール(certbot)の精度がいまいちで、
(特にnginxだと)多少苦労があったり、自動更新がうまくいかなかったりしましたが、
クライアントが「certbot-auto」に移行してからは、さらに簡単になりましたヽ(`・ω・´)ノ

基本的にはここに書いてある通りで、nginxでもapacheでも余裕です

Let's Encrypt の使い方 - Let's Encrypt 総合ポータル

  1. certbot-autoをwgetしてchmod +x
  2. certbot-autoを初回起動して必要なライブラリを取得
  3. certbot-autoをドメイン名つき起動し、証明書発行してもらう
  4. (途中でメールアドレスを求められるので入力)
  5. ローカルに生成された証明書をconfに書く
  6. portをあける
  7. nginx or apacheを再起動

これだけです(`・ω・´) b

一度証明書を取得してしまえば、環境情報がサーバに保存されるため、
cronにセットしてしまえば、再取得も簡単にできます

$ sudo crontab -l

# 週に一回 "certbot-auto renew" を実行する(念のため、実行ログも取る)
2 5 * * 0 /path/to/certbot-auto renew >>/hoge/piyo/certbot.log 2>>/hoge/piyo/certbot_err.log

証明書の期限は90日で、1ヶ月くらい前から更新が可能になりますが、
更新が不要な場合はメッセージだけ出して無視してくれます(`・ω・´)

最悪、cronでの更新がうまくいかなくても、証明書作成時に登録したメールアドレスに、
「もうすぐ期限が切れます」というメールが届くので、手動で更新すれば大丈夫です
(以前はそのような運用をしてました)

あとは・・・問題なければhttpからhttpsにリダイレクトしてあげるといいのですが、
ccptsではやっているものの、ポータルの方はほっといてます

server {
    listen 80;
    listen [::]:80;
    server_name ccpts.parrot-studio.com;
    return 301 https://$host$request_uri; # redirect to https
}

server {
  listen 443;
  listen [::]:443;
  server_name ccpts.parrot-studio.com;

  # 以下略

}

慣れれば本当に数分のレベルですので、業務でいきなり導入する前に、
ぜひ自分のサイトでお試しを(´・ω・)っ


<2018/8/28追記

cronを使ってnginxの証明書を更新しようとした場合、
nginxのpathを見失って更新できないことがあります

その場合、crontabにPATHを明示する必要があります

$ sudo crontab -l

# PATHを明示
PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin # その他必要なPATH

# 以下同じ

cronあるあるですが、忘れやすいのでご注意を(´-ω-)

*1: はてなBlogも現在はhttpsに対応しています・・・が、mixed contentへの対応がやや面倒な感じです(´-ω-)