どれくらいこのサイトを見ている方が居るのか知らないが晴れてアップデート出来た。サーバーもCentOS6.xからこの間リリースされたUbuntu16.04に入れ替えた。
使ったスタックたち
-
フロントエンド
- babel
- css-modules + post-css
- React
- react-router
- redux
- Webpack
- webpack-isomophic-tools
-
バックエンド(これはほとんど変更なし)
- koa.js
- MongoDB
-
インフラ周り
- Docker
- ansible
これまで
Vue.jsとpage.js
でもって無理やりSPAにしていた。Vueを使った最終バージョンがこれ。SEOが激辛だった。サイトにアクセスしてきたのクローラーなら、PhantomJSでページのJSを実行してHTMLを配信するherokuサーバーに飛ばすみたいなことをしていて、幸いcurl unix socket
みたいなワードでの検索に書いた記事が引っかかっていたりしたのでちゃんと動いていは居たようだけど、いつインスタンスが死ぬのかビクビクしながら生きる日々。無料期間を限られてるので時間の問題もあった。
デブロイもfabricでsshして、git pull
してシェルスクリプトを実行、というようなあまりにもセンスのないスタイル。CentOS6.xなのでいろいろパッケージが古い。
React
ずっとJSXが気持ち悪いなーとか思って近づかないようにしていたのだが、CoffeeScript、SASS、jadeで書いてきたのも全部babel、post css、JSXに変えたりして思い切って使ってみたら、これがなかなかいい感じだった。Virtual DOMであることによって直接何かが嬉しいわけじゃないんだけど、Reactを取り巻くエコシステムが素晴らしい。Reduxとreact-routerは本当に良く出来ている。
SSR(サーバーサイドレンダリング)について
SSRするにはサーバー側でレンダリングする前にAPI fetchをして結果を得るまでにどっかで待たないといけない。そのタイミングなどをサーバー側でどうやって知るか、ということが一定の問題になっている。方法は幾つかあるんだけど、大体この記事にまとまっているが、かいつまんで書けば
- Reduxで、特にredux-promiseなどを一緒に使っているばあいは、飛んできたactionがpromiseがどうかチェックして、promiseならどっかにまとめるmiddlewareを用意する
- コンポーネントのクラスで決められたスタティックメソッドを指定して、非同期処理は全部その中で行うようにする。サーバー側ではreact-routerの
macth
のコールバックのrenderProps
経由読み取る
この2通りが主なソリューションになる。1の方法ではactionの書き方に制約がかかってしまうので、今回は2の方を採用した。かるくこのサイトのコードで例示する。記事を表示するコンポーネントは
// app/pages/memo/show.js
class MemoShow extends Component {
static loadProps({ dispatch, params }) {
return dispatch(getMemo(params.path))
}
componentWillMount() {
this.constructor.loadProps(this.props)
}
こんな感じに、Reduxのdispatch
とparams
を引数に持つloadProps
という特定のメソッドを定義しておく。アクションはPromiseを返す必要がある。サーバー側では、Expressのハンドラー関数内で
// server/hander.js
match({routes, location: req.originalUrl}, (error, redirectLocation, renderProps) => {
const render = ()=> {
/*
この中で res.send() する
*/
}
const params = {
dispatch: store.dispatch,
params: renderProps.params,
}
const promises = renderProps.components.map(c => {
const hasLoadProps = c && c.loadProps && typeof c.loadProps === 'function'
return hasLoadProps
? c.loadProps(params)
: Promise.resolve()
})
Promise.all(promises).then(render, onError)
というようにdispatch
とparams
を与えてやればAPI fetchのプロミスを待機することができる。
css-modulesについて
概要
説明を書いてもわかりづらいと思うので例示すると、
こういうCSSファイルは
// styles.css
.header {
/* SNIP */
}
.title {
/* SNIP */
}
コンパイルされバンドル時に
._2lIB3 {
/* SNIP */
}
._1AHRg {
/* SNIP */
}
このようになる。このままではDOMにクラスを適応ないが、その代わりに
import styles from './styles.css'
としたときの戻り値に
{
header: '_2lIB3',
title: '_1AHRg',
date: '_17eX4'
}
というオブジェクトが渡されてくるようになるので、例えばReactであれば
<heaer className={styles.header}>
...`
</header>
とすればちゃんとスタイルを適用できる。クラス名を全て意味のない名前に置き換えることで、実質的にグローバルを汚さずCSSファイルをモジュール化できるというわけである。
SSR時に.cssファイルが読み込めない問題
SSRするときはサーバー側からクライントコードを全て読み込むことになるが、
import styles from './styles'
というコードがnode.jsで実行するとエラーとなる。これを解決するは主な方法は以下の2通りある。
- SSRを行うサーバーのコードもWebpackでコンパイルしてしまう
webpack-isomophic-tools
を使う
よく見るGitHubでスターが大量についてる「ReactでSSR対応boilerplate!!」みたいなやつは大体1の方を採用しているが、あまりにもセンスがないので2のwebpack-isomophic-tools
を使っている。
大体READMEにcss-modulesと一緒に使うにあるとおりに使えばいい。仕組みとしては、Webpackのコンパイル時にプラグインとして読み込むことでCSSや画像のパスと戻り値のリスト作成して、SSRサーバー起動時にそのリストにしたがってnode.jsのmodule
を操作して対象のファイルへのrequireの戻り地を書き換えることで動作している。
Docker
バックエンドではMongoDBを使っているんだけど、こいつを扱ったものかと悩んだ。 やり方は以下の3つ考えた。
- dbpathをボリュームにして普通にコンテナ内にインストールして使う
- ホストにmasterのMongoDBを起動して、コンテナ内ではそのslaveとしてMongoDBを起動する
- ホストで動くMongoDBに直接アクセスする
自分が使うときの都合としては、sshしてちょろっとデータを見たり挿し込んだりみたいなことはよくやるので、1だとdocker exec -it <container name> mongo
っていちいちやらないといけず面倒くさいので、これは不採用。volumeにしたdbpathでコンテナでMongoDBを起動した状態で、ホストでも同じdbpathを起動しようとするとjournalがどうこうと言われ(たぶんトランザクション処理の都合)て起動できないのが大きい。
2はslaveで起動するにはmasterのホストにアクセスする必要があってそのためにrun --add-host
するなら、直接アクセスする方が速いよなーとなり、3の方法を取ることになった(Dockerコンテナ内からは、172.17.0.1
でホストにアクセスできる)。
こうなるとMongoDBに外からアクセスすることになるのでmongodb.conf
のbind_ip
にはDockerのIPアドレスを追加しておく必要がある(自分は0.0.0.0にしてしまったが……)。とにかく何がベストなのかは分からない。
ごたごた書いたけど、今回いろいろ新しく導入した一番「便利になったなー」思ったのは間違いなくDockerだった。
デプロイはansible
デプロイしようと思ったらGitHubのmasterにpushして、ansibleを実行時にremoteでGitHubからpullしてきて、docker-compose
up -d --build`でビルド&再起動する感じ。キャッシュの効いてないbuildを挟むと平気で数十分応答がなくなるので、待ってる間すごい不安になる。
まとめ
どう見てもちょこっとメモを載せるだけのサイトにしてはやりすぎ。