ES6で書いたReactのアプリをTypeScriptに移行した件(+そこに至るまでの歴史)
これだけ読めばいい(かもしれない)概要
めちゃくちゃ長くなってしまったので、先に概要だけ...φ(・ω・`)
- クライアントサイドのコードが複雑化してきたので、TypeScriptの適用領域が広がっている
- いきなりTypeScriptを導入するのではなく、まずES6で整理してみる
- 汎用型「any」を使いながら、見える範囲で徐々に型を埋めていけば良い
- 外部ライブラリは型定義を追加してからimport
- jQueryとかグローバルオブジェクトはdeclareを使って宣言
- TypeScriptを書く時にVSCodeをつかうとめっちゃ便利
- interfaceで「ゆるい制約」をかけられるのが便利
- うまくinterfaceを使えば、Reactのpropsやstateの型もチェックできる
- 型が導入されても、テストが不要なわけではない
以下、めっちゃ長い話になってますので、後はお好みで(´・ω・)っ
TypeScriptを導入した経緯
先日はとある事情からGoを試したわけですが・・・
・・・その際に、もう一つ「伸びている言語」として挙げられていたのが、
「TypeScript」でございます
2012年に初めて発表された際は、まだクライアント側の開発が成熟しておらず、
ブラウザのDOMをいじる程度の用途であれば、まだ生のJavaScriptが優勢だったと思います
当時はそれこそ山のようにAltJSが存在し、クライアントのフレームワーク同様、
何もスタンダートがない状況で、トランスパイルの手間を考えても、
RailsでCoffeeScriptが便利・・・くらいだったかと(´-ω-)
「あくまでJavaScriptの拡張として型を導入したもの」という触れ込みから、
出たばかりのTypeScriptをいじってみた記憶はありますが、
やはり「当時のJSのスタンダード」からは逸脱しており、諦めた記憶が *1
しかし、今や時代は変わりました
Webアプリケーションの主体はクライアント=ブラウザとなって複雑化したり、
Babel+Webpackがほぼ主流に定まったことにより、
「AltJSをトランスパイルしてデプロイする」というのも一般化してきました(`・ω・´)
「標準的なJavaScript」も進化し、ES2015(いわゆるES6)により、
他の言語にありがちなパラダイムが、「標準的なJS」として書けるようにもなっています
(それでもまだ、古い環境向けのトランスパイルは必要ですが)
明示的に「クラス(もちろん継承も)」が存在したり、
最大のネックだったスコープ問題の解決など、最近の言語を知るプログラマからすると、
「やっとできるようになったか」という内容が満載です
現在のTypeScriptは実質、「ES6の仕様を拡張したもの」*2として考えることができます
特に「型」の概念を明確に導入していることで、大規模開発に有利なのもポイントです
そしてなにより・・・以前とは比べものにならないほど、採用事例が増えています
あの(Dartを開発していた)Googleですら、TypeScriptは「標準言語」ですΣ(・ω・ノ)ノ
先日のGoに比べると、「JavaScriptで書かれたそこそこ大きなアプリ」であれば適用できるため、
採用する基準がわかりやすいのも利点かと
少なくとも、「ES6で書かれ、Reactの採用を考えなければならないサイズのアプリ」であれば、
TypeScriptを導入する価値は十分にあると考えられます
・・・まあ、結局はやってみたかったからやったのですが(´-ω-)
ccptsにおけるクライアントアーキテクチャの変遷
長い導入はさておいて、ここから本題に対する長い前置きです...φ(・ω・`)
ということで今回は、「チェンクロパーティーシミュレーター」(以下ccpts)を、
TypeScriptで書きなおした・・・というお話なのですが、
そのためにはまず、そこに至るまでの経緯から説明しないといけません
開発に着手した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化プロジェクトを開始
- https://parrot.hatenadiary.jp/entry/2016/02/28/113310
- イベント→データのストリームを、そのままsetStateにつなげられたのが成功のポイント
- この時点ではまだ、modelはCoffeeScriptで書かれていた
- クラス変数・メソッドが使えないのがどうしても・・・
- 2016年末、Webpackを導入
- https://parrot.hatenadiary.jp/entry/2017/02/07/144805
- Rails5.1でwebpackerが導入される情報を見て移行
- この時点でCoffeeScriptは排除され、完全にES6ベースに移行する
- (以降、しばらくはチェンクロの3部対応=データ周りの仕様変更に追われる)
- 2017年の夏、Rails5.1+React on Rails(RoR)の組み合わせで設計が整理される
- 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」がありますし、型がおかしい場合は書いている時点でエラーになるので、
詰まるたびに調べて・・・という感じで言語仕様を把握していきました...φ(・ω・`)
ほぼ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の支援機能が強力なのです
VSCode上で何らかのオブジェクトをポイントすると、
型の定義を推論して提示してくれるので、
それを参考に順番に埋めていっただけです
もちろん、「すでに動いているアプリ」なので、
こういった裏技(というより邪道)に近いやり方が可能なのですが、
AtomからVSCodeになんとなく移行して、初めて「VSCodeすごい」と思いました(`・ω・´)
しかも、この型推論はES6の文脈でも発揮されます
ES6の文脈で使われているオブジェクトに型情報があった場合、
同じように推論してくれるので、それを参考にして厳密な処理を書くことが可能です
実際、modelに型を導入し、React側がまだES6で書かれている時点で、
かなりの数の潜在的バグを、型定義から見つけて排除することができました(`・ω・´) b
ある程度コードの規模が大きくなってくると、
「これどこから来たんだっけ?」というのがわからなくなりますが、
IDEの支援を駆使し、型を手がかりにすることで、かなり把握しやすくなるわけです
Lodashの「_.chain」とか、Bacon.jsの「Bacon.Bus」はとても便利な仕組みですが、
つないでいくうちに何をつないでいるのかわからなくなってしまいがちです
そんな複雑な高階関数であっても、型定義のおかげで安心して書けます...φ(・ω・`)
(4) ReactのPropTypesの代替としてのinterface定義
逸れてきたので話をReactに戻すと、元々のReactのコードにはPropTypesが定義されてませんでした
これはもう、完全に私の怠慢なんですが、今回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が設定される可能性があったとします
このままこのオブジェクトのメソッドを呼ぶと、
nullの可能性があるため「エラー」になります(警告ではありません)
その代わり、上記のようにnullチェックを通した後だと、型推論からnullが排除されます
つまり、ちゃんと文脈を見ているということなので、とても便利です(`・ω・´) 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で細かく書きたいレベルであれば、
基本的な型のレベルから試してみるといいと思います(`・ω・´)
Goをやらないとまずいと言われたので触りつつ、適用範囲を考える
まあ、きっかけは流れてきたこれなんですが・・・
・・・突っ込みどころはあるのですが、そこはおまけに回すとして、
重要なのは「Goが伸びている」ということですよね
前から一度くらいGoに触ろうとは思っていたものの、そのきっかけがなかったので、
せっかくなのでGoを軽く書いてみたわけですが・・・
・・・結論からいえば「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にちょうどいい本があったので、ポチってみましたが・・・
プログラミング経験者がGo言語を本格的に勉強する前に読むための本
- 作者: 天田士郎
- 発売日: 2017/03/10
- メディア: Kindle版
- この商品を含むブログを見る
・・・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
サンプル的にまずはこれだけ...φ(・ω・`)
どんな言語でも、最近は「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は「大規模なコードを書くための言語」ではなく、
「大規模システムを構成する "小さなコンテナ" を書くために特化した軽量言語」なのです
GoogleがMapReduceを発表してから今まで、
大規模システムは「小さな何かが協調して大きなデータを処理する」という形で進化してきました
「小さな何か」はクラスタ*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がシンプルな構造しか持たないとしても、なんら問題がないことがわかると思います
先ほどのサイトにもこういう問題が挙げられていましたが・・・
・・・そもそも、これが問題にならない粒度の「小さなコード」に使うべきなんだろうなと
「こんな非効率なことは意味がない」と思うかもしれませんが、
(MapReduceが適用されるような)大規模なデータを扱う場合においては、
モノリシックなアプリの常識は通用しません(´-ω-)
逆に言えば、Goを適用するような「軽量コンテナの集合体」の場合、
「どのようなコンテナに分割するのか?」という設計が最重要課題になります
小説ではありますが、こちらに登場する「Vilocony」と、
その上に構築された「KNGSSS」こそ、
(言語がJavaとはいえ)まさにコンテナを用いた大規模システム開発の事例と言えるでしょう
現実でもコンテナの運用はいろいろ難しいらしく、
白川さんの能力と執念がなければカットオーバーは難しかった気はしますが、
わかりやすい事例です
そもそも、Go自体が関数型的な仕様を持っていなかったとしても、
コンテナを用いたシステムの設計そのものに関数型的な思想が必須になるわけで、
関数型的な知識がGoで構築されたシステムに適用できないなんてことはないはずなんですよね(´-ω-)
関数型的な設計でよく出てくるのがUNIXの思想で、
「小さなプログラムをつないで処理」が、「小さなコンテナをつないで処理」になっているだけです
その意味では、UNIXは立派な「マイクロアーキテクチャのシステム」です
まとめ
Goの設計思想を考えれば、「コンテナで構築された大規模システム向け」だとは思うのですが、
今までのモノリシックなシステムで適用できないのか・・・というと、
全くできないってことはないはずです
(私のような)「普通のエンジニアが考える普通のWebサービス」は、
おそらくRuby/PHP/Python等の方が実装工数が小さく、
必要なライブラリが揃っており、リリースまでの工数を小さくできると思います
そういったシステムが肥大化していって、
パフォーマンスが要求された場合に、裏側の一部をGoのコンテナに置き換えていく、
なんて改善計画は十分にありだと思います *7
幸い、Goは前述の通り「言語仕様がめっちゃシンプル」なので、
「何らかの言語を触ったことがあればとりあえず書ける」という利点があります
こちらの記事でも、「普通に書くだけでかなりのパフォーマンスが期待でき、
チューニングの時間を短縮できる」と書いてますが、
一方で、「CURDを扱うだけの普通のAPIはPythonがいい」とも書いてあります
やはり・・・
システムをうまくレイヤー分けして、それぞれの処理が得意な処理系(言語)を選ぶ
・・・というのが大事ですね(´-ω-)
(そしてまたおまけに続く・・・)
*1: ここは批判の対象になっているようですが、後述するように、「本来のGoの適用領域」からするとむしろ、「一つのバイナリ単位で環境を分ける」くらいでもいいのでは・・・と思います なんなら、direnvを使ってプロジェクトディレクトリごとにGOPATHを切り替えれば解決するわけですし
*2: 強いていえば、「構造体」といわれてメモリ空間が意識できるなら、という前提はあるかもしれません まあ、どんな言語であれ、メモリ空間を意識できるかは重要です
*3: なぜ釣り合わないのかについて、一度は長々書いたのですが、どんどん本題からずれていってしまうので、ばっさり切りましたΣ(・ω・ノ)ノ
*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を手動デプロイで運用してました
別に手動でも困ってなかったのですが、一つだけ問題がありまして
サーバダウンから再起動した際、サービスも一緒に起動してほしかったので、
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のえらい点でございます(`・ω・´)
例えば、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の主な更新内容
その中のトピックスの一つが「(ccptsの)https対応」で、
昨年の冬くらいに対応し、暗号化方式でなんやかんや悩んだ記憶があるのですが、
とりあえず置いておいて・・・Σ(・ω・ノ)ノ
昨日、Chromeがアップデートされ、「httpは安全ではない」と表示されるようになりました
実質まともに動かしているのはccptsくらいなのですが、
念のためポータル的なサイトの方も対応しておくことに...φ(・ω・`) *1
とはいえ、現在は「Let's Encrypt」という素敵な仕組みが存在し、
やろうと思えば数分でhttps対応が終わってしまうご時世です
ccptsをhttps対応した頃は、まだツール(certbot)の精度がいまいちで、
(特にnginxだと)多少苦労があったり、自動更新がうまくいかなかったりしましたが、
クライアントが「certbot-auto」に移行してからは、さらに簡単になりましたヽ(`・ω・´)ノ
基本的にはここに書いてある通りで、nginxでもapacheでも余裕です
Let's Encrypt の使い方 - Let's Encrypt 総合ポータル
- certbot-autoをwgetしてchmod +x
- certbot-autoを初回起動して必要なライブラリを取得
- certbot-autoをドメイン名つき起動し、証明書発行してもらう
- (途中でメールアドレスを求められるので入力)
- ローカルに生成された証明書をconfに書く
- portをあける
- 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あるあるですが、忘れやすいのでご注意を(´-ω-)
Rails+Reactアプリをbrowserifyからwebpack基盤に移行した件
ちょうど一年ほど前、「チェンクロ パーティーシミュレーター」(以下ccpts)を、
jQueryを使った制御から、React.js + Bacon.jsで大幅に書き換えました...φ(・ω・`)
そもそも、ccpts自体、
「モバイルで動くように*1、できるだけ今風の技術でクライアント側を構築する」
という目的を持って設計してました
昨年の時点ではReactが十分にメインストリームに乗ったと判断して、
基本的なアーキテクチャをReact+browserifyに載せ替えたわけですが、
「Reactで動くようにする」ことが目的だったので、細かいところは置いておいたわけです
それを今回、フレームワークを見直してきれいにした(している)というお話です(´・ω・)っ
解決したい問題とwebpack
昨年末、チェンクロで第3部が実装され、
システムが大幅にアップデートされました
こうなると、ccptsのシステムも3部対応で大幅に書き換えがいるわけですが、
以前構築してからここまで運用してみて、
最大の問題は「デプロイに時間がかかりすぎる」ということです
サーバサイドのデータを書き換える分には問題ないのですが、
クライアント側のjsをいじるとデプロイ時にbuildが必要になり、
私が使っているVPSだと10分以上かかってしまいますΣ(゚Д゚)ガーン
3部対応するにあたり、クライアント側を頻繁にいじる必要があるので、
さすがにこれは許容できません
そもそも、これだけコストがかかると、
「クライアントをいじらずにどうにかできないか?」と考えるようになってしまい、
本来的な設計から遠ざかってしまいます(´-ω-)
そこで、2016年末の時点での「今風のやり方」を探すことにしましたのですが、
その時点で主流になっていた(ように見えた)のがwebpackです
そもそも、Railsは「Asset Pipeline」がよくできすぎています
これを自前で実装するとコストがかかるので、
何とかして今風のコードと組み合わせて乗っけたい・・・というのがポイントになります
調べた結果、ざっくりとこんな感じのやり方が主流になっているように見えました...φ(・ω・`)
- クライアント側のコードを閉じた形で独立して構築する(Railsの管理外)
- webpackで一つのjsにまとめて、Railsのassets管理下に置く(これはpureなjs)
- あとはRailsのprecompile等に素直に任せる
当初、これを自前で書こうと思ったのですが、
もっと楽ができないのか・・・と思ったら、
「React on Rails」という良いものがあったわけです
1年前の記事でも最後に取り上げていましたが、
この時点では何が主流になるのかさっぱりわからなかったので、
そこまでは踏み込みませんでした(´-ω-)
このやり方の最大の決定打は、Rails5.1におけるDHHの方針です
Rails用に仕組みを構築しちゃうあたりがさすがですが、
このwebpackerにreactのオプションも入っているくらいで、
もう迷う要素はなくなりました( ゚д゚)o彡゚
まだRails5.1はリリースまで時間がかかりますが、リリースされたとしても、
React on Railsにはreactのサーバサイドレンダリングという仕組みがありますので、
最終的にはRails5.1の上でReact on Railsという形になると思います
とはいえ、サーバサイドレンダリングをいきなり使うと、
React on Railsに依存しすぎてしまうので、まずは使わずに、
「今風の開発フローを構築するための基盤」として利用しております
実際の段取り
CoffeeScriptの排除とnpmからyarnへの移行
「できるだけクライアントのコードを client 以下に移す」というのが方針になりますが、
es6でクラス定数が使えない等々で、CoffeeScriptで書いたmodelが残っている箇所がありました
これらはAsset Pipelineとbrowserifyで動作していたわけですが、
webpackで一つにまとめるためには、全てes6で書き換える必要があります(´-ω-)
そこで、es6で書くことを優先して、クラス定数の方をあきらめました
(例としてmodelを一つ)
https://github.com/parrot-studio/cc-pt-viewer/blob/69c8e88185536e9c9c8af67c2b5e5caa53069f66/client/app/bundles/ccpts/model/Favorites.js
今回は目的が違うので、この手の妥協は仕方ありません(´・ω・`)
ついでに、npmによるパッケージ管理をやめて、yarnに切り替えました
Railsでbundlerを使っているわけで、この仕組みは全く違和感がありません
import/exportを正しく使う
以前は各modelやcomponentを呼び出す前に、
ブラウザのグローバルにBaconやReactのような各ライブラリを突っ込んでおき、
暗黙的に参照するというやり方でとりあえずやってました
これはCoffeeScriptとes6を共存させるのに必要だったのですが、
CoffeeScriptを排除したので、もう不要です
全部import/exportで書き換えました...φ(・ω・`)
問題は、どうしてもimport漏れが出てくることです
当初、動かしてみてはエラーを見て追加・・・みたいな、
超絶に効率の悪いことをしていたのですが、
ESLintで全部解決しました(`・ω・´)
ルールは自分の直感に反しないレベルでこんな感じに(´・ω・)っ
cc-pt-viewer/.eslintrc at master · parrot-studio/cc-pt-viewer · GitHub
これでエラーが出ないようにガンガン修正していきました
rucobopと同じく、自動で修正する機能もあるので、とても楽です
jQueryの取り扱い
これは1年前と何も変わってませんが、Reactを使うからといって、
jQueryを "使うべきではない" という意見には賛同できません( ゚Д゚)y─~~
たしかに、jQueryで "DOMの操作をすべきではない" ですが、
jQuery界隈には大量のライブラリやノウハウの蓄積があり、
それを全てReactの文脈に落とし込めるとは思えないわけです*2
実際の現場で、jQueryなら数分でできたことが、
Reactだと3日かかります・・・といって、
企画側に納得してもらえませんよね(´・ω・`)
ギリギリまでjQueryを使わないようにしつつ、
どうしてもjQueryが優位な箇所だけ使っていく・・・
それは1年前の時点と変わっていません
しかし、以前はライブラリを全部グローバルに定義していたので問題ありませんでしたが、
全部閉じた環境に持っていこうとすると、jQueryの取り扱いがとたんに難しくなります(lll゚Д゚)
ここに関しては情報も少なく、相当試行錯誤したのですが、
結果的に「古いライブラリは古いやり方、新しいライブラリは新しいやり方」と、
完全に切り分けることにしました
具体的にはこんな感じです...φ(・ω・`)
どうしてもjQueryの文脈が必要なものだけassets以下で管理して、
Reactを含む他の一切をwebpackで一つにまとめたわけです
React側からは「$」がグローバルアクセスできるので、
あとはReactのrefで生DOMにアクセスしてjQueryのアニメーションを適用するだけです
(1年前の記事参照)
グローバル変数があるとESLintが警告を出すのですが、
.eslintrcで「$というグローバル変数だけ許可して」と定義しているので問題ありません
むしろこれにより、「$というグローバル変数が存在する」ということが明確になります(`・ω・´)
置き換えた結果と今後
方針を決めたのが11月で、12月に入ってからは実装されたキャラデータの暫定登録を進めて、
そこから基板入れ替え作業を進めましたが、12月末にはざっくり作業が完了しました
それ以降の開発速度は目に見えて速くなりまして、
やはり心理的な障壁がぐっと下がったのはでかいです(`・ω・´) b *3
コストが下がったことで、棚上げしていた問題、
例えば、Becon.jsのStreamをもっと整理したいとか、
そもそもUIを大きくいじりたいとか、そういうところにも手を出せるようになりました
でも、一番改善したいのはReact on Railsの機能である、
「Reactのサーバサイドレンダリング」なのですが、
そのあたりは今後作業予定なので、できあがったらまた書きます...φ(・ω・`)