中〜大規模なSPAを開発する時に抑えておきたい10のポイント

こんにちは。カヤックのSPAおじさんこと島津です。

今年はReactとVueを使ったSPA開発プロジェクトをいくつか担当してきたので、そこで得た知見の総まとめをしたいと思います。

※ ここでのSPAとはすべてのViewをJavaScriptで書くWebアプリのことを指します。サーバーサイドMVCを主軸にViewの一部をReactやVueで書くこともありますが、今回はそのケースではありません。

1. フレームワーク

数年前とは事情が変わり、 フレームワークを使わないという選択肢は昨今だともう無いでしょう。丸腰のJSでDOMを弄っていた時代に比べると、かなり安定したフロントエンドの開発ができるようになりました。

人気フレームワークの台頭になっている

React + Redux

Vue + Vuex

をこの1年使ってきましたが、書き方は違えどFluxアーキテクチャ・仮想DOM・コンポーネント志向という大枠含め共通点はかなり多いです。

特にVueのv2はReactより後発のため、Reactで面倒だったあれこれが改善されていて、学習コスト・開発コストが大幅に低くなっているという印象です。

その影響もあり、カヤックの新規プロジェクトでSPAを導入する場合は Vue + Vuexを採用することが多くなってきています。

2. 開発環境

特別な事情がなければwebpackやgulp等タスクランナーの設定を書くコストが浮くので、公式が提供しているボイラープレートを使用するのがおすすめです。

Reactの時は、create-react-app

Vueの時は vue-cli

を利用していました。設定不要で

npm installnpm start

だけですぐに開発がスタートできます。

3. クライアントサイドルーティング

これもスクラッチで開発するメリットはほぼないので、公式のルーターを使いましょう。

react-router vue-router

4. CSS

CSSフレームワークを導入する場合

Bootstrapなど多くのCSSフレームワークは一部の挙動がjQueryに依存しているため、そのまま導入すると結構バグりました。CSSフレームワークに限らずですが、jQueryとVirtualDOMはまぜると危険ですね。

React ComponentやVue Componentとして提供されているもの

react-bootstrap

bootstrap-vue

を使うか、JS依存のないフレームワーク

bulma

Picnic CSS

などを利用するのがSPAとの相性を考えると吉です。

ちなみにVueのマテリアルデザインフレームワーク Vuetify を実際のプロジェクトで導入しましたが、コンポーネントも豊富で使い心地良かったです。

Scoped CSS

独自に書くCSSは、コンポーネント単位でのCSSを書けるScoped CSSを導入するのがおすすめです。グローバルスコープが汚れず衝突の心配もなく、末永く安心してCSSが書けます。

Reactでは、 (react-css-modules)https://github.com/gajus/babel-plugin-react-css-modules を使っていました。

Vueはデフォルトで <style scoped=""> 〜 </style> の中に書くだけでOKなので、Reactと比較して超楽ちんになったと感じた機能の一つです。

5. ユーザー認証

APIとクライアントが疎結合なため、以下のような認証フローが必要になります。

image

表示するビューの制御については、 認証前後のコンポーネントを切り替えます。 Reactの便宜で簡単に示すと以下のようなものです。

<App>
  <Signin /> // → 未ログイン状態のとき表示
  <Mypage /> // → ログイン済みのとき表示
</App>

実装の詳細はrouterの公式マニュアルに記載されている方法が参考になります。

react-router

https://reacttraining.com/react-router/web/example/auth-workflow

vue-router

https://router.vuejs.org/ja/advanced/meta.html

6. 環境変数

APIのルートURLなど、開発環境と本番環境で変えたいオプションは必ず出てきます。

以下のbabel pluginを使うことで、Node.jsではおなじみの process.env.HOGEHOGE が クライアントサイドでも参照できるようになります。

babel-plugin-transform-inline-environment-variables

※ 実際はprocess.envに環境変数が入るわけではなく

ソース

if (process.env.NODE_ENV === 'development') {
  // 開発環境のみ実行するコード
}

NODE_ENV=development npm run build でビルド後

if ('development' === 'development') {
  // 開発環境のみ実行するコード
}

のように変換される仕組みになっています。 これにより、envがすべてクライアントサイドに丸見えになってしまうという危険性はなく安心して使えます。

7. ディレクトリ構成とFluxアーキテクチャ

Reduxも、Vuexも堅牢なFluxアーキテクチャを提供してくれるフレームワークではありますが、ディレクトリ構成が決められているわけではありません。規模に応じて柔軟に構成を変えられるメリットがあるのですが、設計に迷ってしまう部分でもあります。

規模が大きくなる場合、関心事を適度に分離するよう "モジュール" という概念でディレクトリを括るというのを実践してきましたが、これは導入して正解だと思ったので紹介します。

React + Reduxでは、Ducksと呼ばれる構成方針があり、導入していました。 簡単にいうと、reducer, actionを一つのファイルにまとめるというものです。

参考記事: Mediumのブログ https://medium.freecodecamp.org/scaling-your-redux-app-with-ducks-6115955638be

Vuexにはデフォルトでmoduleという機能がデフォルトで備わっているので、 こちらを使うと良いでしょう。 https://vuex.vuejs.org/ja/modules.html

8. ビルド&デプロイシステム

本番運用するためには、デプロイとビルドは同時に自動実行するよう仕組みを作っておくべきです。

ビルド後に生成するファイルは以下のようなものが一般的だと思います。(webpack利用の場合)

public/
 index.html → ルートのHTML
 script.js → ビルドしたJS(CSSも含む)
 static/**/* → 画像など

上記のファイルを、npm run build 等のコマンド出力できるようにしておきます。

自動化には

  • CircleCIやTravisCIなどgithubと連動したCIサービスを使う
  • gitのwebhook + ビルドとデプロイを実行するコマンドAPIを用意する

等の方法があります。 CIサービスを使ったほうが、コンテナを使ったビルドを行っていて 冪等性が確保されいるのでオススメです。

CircleCIからAWS S3へコードをビルド & デプロイするサンプルはこちら jshimazu/circlci-s3deploy-example

9. サーバーサイドレンダリング (※ 以下SSR)

SEOが必要なアプリに関してはこの対応が必須になってきます。 簡単に解決できるベストプラクティスはあまりなく、力技を要します。

選択肢としては、以下の3つになります。

  1. 何もしない
  2. サーバーサイドアプリレイヤーでSSR処理を実装する
  3. ヘッドレスブラウザを利用したPrerendering

1. 何もしない

というのは実は怠惰ではなく、Googleのクローラはここ数年で進化してきているので、ある程度JSでレンダリングしたものをインデックスしてくれています。(実際にインデックスされているのを確認しました)ただ、レンダリングするタイミングよってはコンテンツがインデックスされなかったり、100%の精度とはまだ言えないようなので、確実なSEOを求める場合は、SSRを検討した方が良いです。

また、Google以外のクローラの多くはSSRの状態しか読み取らないので、 シェア画像をURLごとに最適化したいケースなどは、この方法は使えないでしょう。

2. サーバーサイドアプリレイヤーでSSR処理を実装する

Isomophic (Universal) Javascriptと呼ばれるものです。 expressなどでクライアント側で利用しているコンポーネントをサーバーサイドレンダリングする仕組みを作ります。クライアント固有のオブジェクト(例えばwindow)などは利用できないので、そのあたりを最適化する必要があります。レンダリング前にAPIから必要なデータを取得して表示するというのも、コツコツ実装していきます。

実装していて冗長感が否めないところですが、 フレームワーク・言語問わず「どこまでクライアントサイドとファイルを共有できるか」というのがポイントとなります。 実装言語はJSに限らず、各言語でSSRをサポートしてくれるライブラリがあり、Ruby on Railsを利用していた時には reactjs/react-rails などがありました。

いまから新規開発するなら SSRのためのライブラリも成長してきているので、

Reactなら Next.js

Vueなら Nuxt.js

を導入してみたいところです。

3. ヘッドレスブラウザを利用したPrerendering

ヘッドレスブラウザはレイテンシが大きく、キャッシュして返す仕組みが必須なため、スクラッチで作ると結構大掛かりです。メリットとしては、2のSSRのように冗長な実装が不要なところです。

(prerender.io)https://prerender.io/ のようなPrerenderを代行してくれるSAASもあるので、予算との兼ね合いがありますが利用するのも実装コストを抑える手段の一つとしてありそうです。

10. サーバー構成

4のSSRが必要無い場合は性能限界の心配がない

CDN + ファイルストレージ

がおすすめです。

(AWSだと、Cloudfront + S3)

この場合、どのURLでアクセスしてもindex.htmlをレンダリングするような設定が必要です。 AWS Cloudfrontを利用していたので、Custom Error Pageの仕組みを使って ファイルが存在しなかった場合index.htmlを返すよう設定していました。

参考: Cloudfront Custom Error ページの設定 http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/custom-error-pages.html#custom-error-pages-cache-behavior

image

SSRが必要な場合は、以下のようにAPIとは別のアプリケーションサーバーが必要になります。

image

まとめ

半年前くらいまではReactおじさんと名乗っていましたが、 今はすっかりVue使いとなっています。

そして数年後にはVueでもReactでもないフレームワークが現れてもおかしくない状況ですよね。

SPAはWebをより快適にするために必要な技術なので きっと今後も破壊的な進化を続けていくでしょう。

そんな時代を生き抜くためには、フレームワークの栄枯盛衰に一喜一憂せず、 本質的なところを抑えておくことが大切だと思っています。

  • SPA環境構築
  • Fluxアーキテクチャ
  • コンポーネント志向

この辺りは必ず今後も生き残っていくものになるはず。

ということで、この記事が誰かの今後のSPA開発の参考になれば幸いです。

KayacではSPAをガシガシ開発してみたいエンジニアを募集しています。

一端のゲームエンジニアが"エレベータ"について本気出して考えてみた

こんにちは。技術部サーバーサイドエンジニアの大河原です。

ゲーム作ってます。一応まだ新卒です。

こちらはTech KAYAC Advent Calendar 2017 の23日目の記事になります。

(昨日の記事は我らが@commojun「新卒一年間で確立した紙のノート仕事術!」でした。)

ちなみに前回僕が書いた記事はカヤックのエンジニアのエディタ事情 2017 です。こちらもよかったら是非。

今回は普段から僕らが利用しているエレベータとエレーベータのアルゴリズムについて調べてみました。

f:id:taiga006:20171221185128j:plain
ポルトガル・リスボンの観光地でもあるサンタ・ジュスタのエレベーター。上の展望台からはリスボン市街地を一望できる。

■ なんでエレベータ?

言うまでもなく、弊社はエレベータを設計したり製造したりしていません。←

これといった大きな理由はないんですが、僕自身昔から電化製品とか電子機器とか見るとその性能よりも「どういうギミックで動いてるんだろ?」と不思議に思う性格で、いろんなものを壊しては親に怒られるタイプの子供でした。 (まあ、いい言い方をすれば好奇心旺盛、悪い言い方をすればものを大切にしない性格ということです。)

その意味で高校のときに出会ったアルゴリズムの勉強はとても興味深いものでした。

…で、どうしてエレベータ?って話ですよね。

弊社は鎌倉に本社を構えており僕自身も現在鎌倉勤務なのですが、実は横浜駅前にもオフィスがあります。

「そもそも、なぜわざわざ鎌倉に本社を?」

と思った方は是非弊社代表の柳澤の書いたこちらの記事を読んでみて下さい。

www.kayac.com

ご存じの方もいらっしゃるかもしれませんが、鎌倉は歴史ある街であることもあり、市の規定により市街地含め一部地域では15メートル以上のビルを建設することが規制されています。

景観地区 - 鎌倉市公式サイト

僕が通っているオフィスも例に漏れず駅前の4階建ての自社ビルでエレベータはなく階段で移動しています。

ただ、たまにイベントやMTGなどで横浜オフィスにいくことがあるんですが、こちらの横浜オフィスのほうは横浜駅前の三井ビルディングの30F(最上階)にあり当然最新式のエレベータが複数台設置されています。

人間誰しもオフィスビルやショッピングモールなどで待てども待てどもエレベータが全然来なかったり、「来た!」と思っても反対方向行きだったりしてイライラしたことがあると思います。

エンジニアであれば尚更、「全然エレベータこねーじゃねーか!!どんなアルゴリズムで動いてんだ?俺が一から実装し直してやろうか!!」と寝坊して遅刻しそうな自分を棚に上げて考えたことが一度はあることでしょう(?)

つい先日久しぶりに横浜オフィスに足を運んだときに、まさに上のような状況になって(…あれ?でも待てよ?たしかにこいつら一体どんな仕組みで30階ものフロアを行ったり来たりしてるんだ?しかも複数台で…!?)と不思議に思い、持ち前の好奇心で今回調査してみた次第です。

■ エレベータのアルゴリズムとエレベータアルゴリズム

『エレベータ アルゴリズム』とかで検索するとまず引っかかるのが「エレベータアルゴリズム」というものです。

これで意外とあっさりブログが書き終わってしまいそうと思われたかもしれませんが、残念ながらこれはある特定のアルゴリズムを指すものであり、僕らが普段利用するエレベータに直接適用されているアルゴリズムではありません。

エレベータアルゴリズム - wikipedia

アルゴリズムだけ簡単にまとめると、

  1. ある1つのリクエストを受信すると、同じ移動方向で別のリクエストが有る限りその方向に移動し処理しつづける(無くなれば2へ)
  2. 1とは逆方向でリクエストが有る限りその方向に移動し処理しつづける(また無くなれば1へ)

といった具合ですごくシンプルな内容です。

このアルゴリズムは身近なところで使われています。

つい最近だと、GoogleのRui Ueyamaさんのnoteでも話題に挙げられていたHDDのヘッダのスケジューリングの基本アルゴリズムの一つだったりします。

note.mu

エレベータの下の階が「HDDのトラックの内側」で上の階が「HDDのトラックの外側」とイメージするとわかりやすいと思います。

このアルゴリズムを使ったSCAN方式と呼ばれるものはあくまでディスクスケジューリングアルゴリズムの一つであり、他にも色々とあるようです。

ここでは上のエレベータアルゴリズムを含むディスクスケジューリングのアルゴリズムをいくつか紹介します。


- FCFS (First-Come-First-Served)

単純にリクエストが来た順に処理して行くアルゴリズム。どのリクエストも公平に処理され余計な遅延も起きないが、アクセス時間は非効率でパフォーマンスも悪くなりやすい。

- SSTF (Shortest Seek Time First)

現在位置から一番近い(移動時間が短い)リクエストを順に処理して行くアルゴリズム。平均のレスポンスタイムがFCFSより早くなる一方でリクエストによってはずっと処理されず餓死状態になる場合がある。

- SCAN

上で出たエレベータアルゴリズム。(SCANがなんの略なのかはわからなかった。)移動方向が変わらないため応答時間の変動が少なく上2つに比べると単位時間あたりの処理能力が高くなる。デメリットは追加のリクエストに対しても一度通過してしまった場所だと処理が後回しにされる点や両端のトラックの待ち時間が必然的に長くなってしまうこと。(まさにエレベータ)

- LOOK

SCANとすごく似ているアルゴリズム。実はSCANはある一方向にリクエストを処理しづけて終端まで移動してしまうのに対して、これはそれ以降その方向にリクエストがない場合に引き返す、というアルゴリズム。

- C-SCAN

最初のCはCircularのC。SCANは終端までチェックし続けて引き返すがつまりそれは一度走査した箇所を再度チェックして行くことになる。それに対してC-SCANは一気にもう片方の終端まで飛んでから再度同一方向に移動しながらリクエストを処理する(つまり循環する)、というアルゴリズム。

- C-LOOK

説明不要かと。つまり循環するLOOKということ。


より詳しく知りたいという方はgeelsforgeeksの記事 とか イリノイ工科大学の授業資料 を読んでみると面白いかもしれません。

さて、実際のエレベータもそのビルに1台しか設置されていないのであればこのアルゴリズム(あるいはそれを少し拡張したもの)に則るだけで十分な性能が得られそうですが、多くの場合そうではなく2,3台、大きなビルとなればもっと多くのエレベータが同じエントランスに存在することになります。

しかし、これまた有名な話で恐縮ですが、SCANのような単純なアルゴリズムで複数台のエレベータを運転させた場合、最終的にエレベータ同士が連れ添ったような動き(団子運転)をしてしまうことが知られています。

これを解消するためにあるのが 郡管理 と呼ばれるシステムです。

正確には郡管理システムという言葉自体は、企業や工場などで複数の制御対象に対してある法則に則って連動させることで高効率化を測ることを意図したシステムを意味します。(ただしざっとググった感じだと特にエレベータのシステムに対して使われている印象です。)

群管理システムには例えば「過去の利用データから各時間帯の利用者数を予想して効率的に人を運送する方式」であったり、あるいは「運行効率を工夫することでより省エネを実現できる方式」であったり、はたまた「心理学の観点からエントランスでの待ち時間と実際の乗車時間のうちそれぞれの時間あたりのストレス値などを計算して乗客のイライラを最小限にする方式」であったり...他にも様々な方式が存在し、大手電気メーカの開発・技術関連のページを巡回してみるだけでも面白い情報が得られます。

そういえば最近、現在の階数表示がされていないエレベータをよく見かけますね。あれは、そのエレベータの群管理システムによって敢えて乗車希望者のいるフロアを通過していたりするのを隠すためにあるようです。たしかにいくら全体の効率が良くなるからと言って自分の目の前をエレベータが素通りしていったらストレスたまりそうですよね。

そして当然といえば当然のことですが、それらの詳細なシステムロジックについては各企業の最重要機密事項に当たるため簡単に調査することはできませんでしたあぁああぁああぁ。←

■ 閑話休題

「ボタン押したのに全然こねーじゃねーか!」

以外のエレベータあるあるに「やっべ、違う階のボタン押しちゃった!」があると思います。

あまり知られていませんが製造元メーカによっては押したボタンをキャンセルできる機能がついてたりします。

www.buzzfeed.com

エレベータの動くロジックとは直接関係ありませんが、自分の会社が入っているオフィスビルのキャンセル方法くらいは覚えておいて損はないかと思います。

(もちろん型番によってキャンセルできる/できない場合があることも含めて。)

余談ついでにもう一つ言うと、世界初のエレベータはかのアルキメデスが発明したそうです。

とはいえ、彼が生きていたのは紀元前。ロープと滑車を使った人力のもので人ではなく荷物を運搬するためだったそうです。

■ エレベータのアルゴリズムを書いてみる ~ Elevator Saga ~

f:id:taiga006:20171221173705p:plain

エレベータのアルゴリズムについて調べていたらたまたまElevator Sagaというサイトを見つけました。(GitHubはこちら)

Elevator Sagaはエレベーターのシミュレーションゲームを通じてプログラミングの基礎を学べるサイトのようです。利用できるAPIがドキュメント込みで公開されているのでパズル感覚でアルゴリズムを考えられます。(言語はJavaScriptです。)

例えば「エレベータが待ち状態の時には1階に行く」みたいな指示は次のように書けます。

elevator.on("idle", function() {
    elevator.goToFloor(0);
});

これらのAPIを駆使して指定時間内に目標人数を行きたい階数に輸送できるとクリアとなります。

面白そうなので最初の簡単なレベルだけ挑戦してみました。

lv.1 3階建て:60秒で15人を輸送せよ

{
    init: function(elevators, floors) {
        var elevator = elevators[0];

        elevator.on("idle", function() {

            elevator.goToFloor(0);
            elevator.goToFloor(1);
            elevator.goToFloor(2);

        });
    },
    update: function(dt, elevators, floors) {
    }
}

まだよくわかってないのでとりあえず1階から3階までを巡回するだけのエレベータを作ってみました。

(実際あったらブチ切れそう。)

elevatorsとあるようにレベルが上がって行くに従ってエレベータの数自体も増えて行きます。

ちなみに lv2 は上と変わらずに巡回する階数を増やせばクリアできます。

lv.3 5階建て:60秒で23人を輸送せよ

{
    init: function(elevators, floors) {
        var elevator = elevators[0];

        _.each(floors, function(floor) {
            floor.on("up_button_pressed down_button_pressed", function() {
                elevator.goToFloor(floor.floorNum());
            });
        });

        elevator.on("floor_button_pressed", function(floorNum) {
            elevator.goToFloor(floorNum);
        });

    },
        update: function(dt, elevators, floors) {
        }
}

今度は少し工夫しました。と言っても

  • 「各フロアでボタンが押されたらその階に移動する」
  • 「乗客が押した階に移動する」

と行ったエレベータのごくごく基本的な動作を入れただけです。

台数が増えてこのあとLv4あたりから本番、と行った感じです。

普段から競プロとかやられてる人はこう言ったゲームおそらく得意でしょうね。

■ 最後に

いかがだったでしょうか。おそらくこのブログの読者層からして普段の業務には1ミリも役立たない内容だったかと思いますが、通勤時間の暇つぶしくらいにはなったんじゃないでしょうか。

最後に募集告知です。

カヤックでは、でかいオフィスビルでいつまでもエレベータを待っていられない!一秒でも長くコードを書いていたい!というエンジニアを募集しています。(強引)

www.kayac.com

www.kayac.com

さて、毎日コツコツ続けてきたカヤックのAdvent Calenderも残すはあと僅か!

明日は@butchi_yの記事を予定しております。お楽しみに。

本記事で参考にさせていただいたサイト