どれくらいこのサイトを見ている方が居るのか知らないが晴れてアップデート出来た。サーバーも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をして結果を得るまでにどっかで待たないといけない。そのタイミングなどをサーバー側でどうやって知るか、ということが一定の問題になっている。方法は幾つかあるんだけど、大体この記事にまとまっているが、かいつまんで書けば

  1. Reduxで、特にredux-promiseなどを一緒に使っているばあいは、飛んできたactionがpromiseがどうかチェックして、promiseならどっかにまとめるmiddlewareを用意する
  2. コンポーネントのクラスで決められたスタティックメソッドを指定して、非同期処理は全部その中で行うようにする。サーバー側では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のdispatchparamsを引数に持つ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)

というようにdispatchparamsを与えてやれば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通りある。

  1. SSRを行うサーバーのコードもWebpackでコンパイルしてしまう
  2. 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つ考えた。

  1. dbpathをボリュームにして普通にコンテナ内にインストールして使う
  2. ホストにmasterのMongoDBを起動して、コンテナ内ではそのslaveとしてMongoDBを起動する
  3. ホストで動く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.confbind_ipにはDockerのIPアドレスを追加しておく必要がある(自分は0.0.0.0にしてしまったが……)。とにかく何がベストなのかは分からない。

ごたごた書いたけど、今回いろいろ新しく導入した一番「便利になったなー」思ったのは間違いなくDockerだった。

デプロイはansible

デプロイしようと思ったらGitHubのmasterにpushして、ansibleを実行時にremoteでGitHubからpullしてきて、docker-composeup -d --build`でビルド&再起動する感じ。キャッシュの効いてないbuildを挟むと平気で数十分応答がなくなるので、待ってる間すごい不安になる。

まとめ

どう見てもちょこっとメモを載せるだけのサイトにしてはやりすぎ。