RethinkDB, React.js, horizon, Webpackでつくるリアルタイムチャットアプリ

Mediumのこちらの記事を読みました。

[React, Webpack, and Horizon Quick Start](https://medium.com/front-end-hacking/react-webpack-and-horizon-quick-start-b9335c1ece53#.7vfg82b5n)

面白そうだったので、僕もやってみました。
ほぼコピペになってしまったので、一応、[著者の許可](https://twitter.com/steedhelix/status/744219021222907904)はとってあります。

それぞれのツールやライブラリの説明は省略します。というか、RethinkDBもHorizon.jsも初めて使ってみました。[5 minutes Javascript](https://fivejs.codeschool.com/)というポッドキャストで気にはなっていたのですが、使うのは初めてです。わくわく。

# ステップ１

まず、必要なツールをインストールして、開発環境をセットアップした後、ローカル開発サーバを立ち上げるところまでやってみます。


## RethinkDBインストール

RethinkDBをインストールしておきます。

[Installing RethinkDB](https://www.rethinkdb.com/docs/install/)

自分はHomebrewでインストールしました。

```
$ brew install rethinkdb
```

RethinkDBは、リアルタイムWebアプリケーションのための、オープンソースDBらしいです。

## Horizonインストール

Horizonをインストールします。（Horizon.jsなのか、Horizonなのかいまいちわからない。。）
こちらは、npmからインストールします。

```
$ npm i -g horizon
```

horizonはパッケージというよりは、ツールなので、-gインストールみたいですね。
グローバルインストールはちょっと。。。という方は仮想環境で実行することをおすすめします。


horizonのコマンドはhzで実行できます。
```
$ hz version
```

## プロジェクト作成


プロジェクトを初期化します。

```
$ hz init [アプリ名]
$ cd [アプリ名]
$ npm init -y
```

こんなディレクトリ構成が生成されました。
<img width="175" alt="Screen Shot 2016-06-18 at 8.25.39 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/47b57059-7175-d3e9-f6d6-6deab3d11228.png">

今回、.hz/config.tomlには触ることはありませんが、一読すると面白いかもしれません。

## フロントエンド開発環境の構築

フロントエンド開発にまつわるパッケージをインストールします。webpackやreact,babelのパッケージですね。

```
$ npm install --save react react-dom webpack babel-core babel-polyfill babel-loader babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-plugin-transform-runtime
```

フロントエンド開発者にとっては、おなじみのパッケージですね。

開発ディレクトリも作成します。

```
$ mkdir src/components
$ touch src/index.jsx
$ touch src/components/app.jsx
```

## Reactコンポーネントの作成

最低限のReactコンポーネントを作成します。

```js
// ./src/index.jsx

import React from 'react'
import ReactDOM from 'react-dom'

import App from './components/app'

ReactDOM.render(
  <App />,
  document.querySelector('.attach')
)
```

```js
// ./src/components/app.jsx

import React, { Component } from 'react'

class App extends Component {
  render() {
    return (
      <div>
        ここにアプリを構築していきます。
      </div>
    )
  }
}

export default App
```

さらに、index.htmlも修正しておきます。

```html
<!-- dist/index.html -->

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="/horizon/horizon.js"></script>
    <script>
      var horizon = Horizon();
      horizon.onReady(function() {
        document.querySelector('h1').innerHTML = 'reChat works!'
      });
      horizon.connect();
    </script>
  </head>
  <body>
    <div class="attach"></div>
    <script src="./bundle.js"></script>
  </body>
</html>

```

## トランスパイル＆バンドル設定

次にトランスパイルとバンドルの準備を行います。

```
$ touch babel.rc
$ touch webpack.config.js
```
babel.rcにはbabelによるトランスパイルの設定を書きます。そして、webpack.config.jsには、babelを含めたJSのコンパイル全般の設定を書きます。


設定を書き終えたら、webpackをｗatchモードで起動します。

```
$ webpack --watch --progress --colors
```

何度も使うものですので、自分はpackage.jsonのnpm scriptsに記述しました。

```json
"scripts": {   
  "watch": "webpack --watch --progress --colors"
},
```

## Horizon devサーバ
Horizonの開発サーバを立ち上げます。

```
$ hz serve --dev
```

<img width="405" alt="Screen Shot 2016-06-19 at 12.33.25 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/7f66590e-c0db-094e-1157-81b62322611d.png">

[http://localhost:49742](http://localhost:49742) にアクセスすると、以下のような画面が立ち上がります。
<img width="1000" alt="Screen Shot 2016-06-19 at 12.34.36 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/94089922-1e7c-4639-4da5-104d4cb433ed.png">

これはAdminInterface（管理画面）というものらしいですね。

我々が作っていくアプリ画面は [http://localhost:8181/](http://localhost:8181/) です。
ここにアクセスすると、きちんとReactコンポーネントがレンダーされているのが確認できます。

<img width="364" alt="Screen Shot 2016-06-19 at 12.45.12 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/1376ebc9-df37-3605-5b7f-29ad89de9eae.png">



# ステップ２

開発環境が完成して、ブラウザで確認できるようになりました。
次は、アプリケーションをもっとReactチックにしていきます。

## メッセージコンポーネント

まずはいくつかのファイルを新規作成します。

```
$ touch src/components/messages.jsx
$ touch src/components/message.jsx
$ touch dist/style.css
```

style.cssは以下のように、最低限の見た目を規定しておきます。

```css
.center {
  margin: auto;
  border: 1px solid;
  border-radius: 2px;
  padding: 10px;
}
```

今回はチャットアプリを作成するので、メッセージのコンポーネントを作成します。

まずはメッセージ一覧のコンポーネントです。

```js
// .src/components/message.jsx

import React, { Component } from 'react'
import Message from './message'

class Messages extends Component {
  constructor(props) {
    super(props)

    this.state = {
      convo: [
        {text: 'this is text', author: '@steedhelix'},
        {text: 'this is some text', author: '@steedhelix'},
        {text: 'this is more text', author: '@steedhelix'},
        {text: 'this is other text', author: '@steedhelix'}
      ]
    }
  }

  render() {
    let msgjsx = this.state.convo.map((message, i) => {
      return <Message msg={message} key={i} />
    })
    return (
      <div className='container-fluid'>
        {msgjsx}
      </div>
    )
  }
}
export default Messages

```

mapでデータからコンポーネント一覧を生成します。
単一のメッセージコンポーネントは以下のようになります。

```js
// .src/components/message.jsx

import React, { Component } from 'react'

class Message extends Component {
  constructor(props) {
    super(props)
    this.props = props
  }
  render() {
    return(
      <div className='row'>
        <div className='col-xs-2 center'>
          {this.props.msg.author}
        </div>
        <div className='col-xs-10 center'>
          {this.props.msg.text}
        </div>
      </div>
    )
  }
}

export default Message

```

最後に、Messagesをレンダーするために、App.jsxを以下のように修正します。

```
// .src/components/app.jsx

import React, { Component } from 'react'

import Messages from './messages'

class App extends Component {
  render() {
    return (
      <div>
        <form>
          <div className='center'>
            <button>メッセージ送信</button>
            <input placeholder='By'></input>
            <input placeholder='write message here'></input>
          </div>
        </form>
        <Messages />
      </div>
    )
  }
}

export default App
```

さらに今回は、Bootstrapを使いました。
index.htmlのheadタグ内に以下の２行を追加します。

```html
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    <link rel="stylesheet" href="style.css">
```

改めて、ブラウザで確認してみます。

<img width="537" alt="Screen Shot 2016-06-19 at 1.22.13 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/a96931f8-7a4a-1dd1-6713-3e0285b89d67.png">

きちんとReactコンポーネントが表示されていますね。


# ステップ３

さて、ここからはDBとの通信を行います。

## パッケージインストール

さらにもう１つ、パッケージをインストールします。

```
$ npm install --save @horizon/client 
```

## index.htmlの完成

index.htmlから不要な箇所を削除します。index.htmlへの修正はこれが最後になります。

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    <link rel="stylesheet" href="style.css">  
  </head>
  <body>
    <div class="attach"></div>
    <script src="./bundle.js"></script>
  </body>
</html>
```

Horizonに関するscriptは、先ほどインストールしたパッケージに内包されるので、不要となりました。


## App.jsxの修正

App.jsxを大きく修正していきます。

```js
// ./src/components/app.jsx

import React, { Component } from 'react'
import Messages from './messages'

// 新しくインストールしたhorizonのクライアントをインクルードします。
const Horizon = require('@horizon/client')
const horizon = Horizon({secure: false})

// messageコレクションをRethinkDB内で初期化します。
const chat = horizon('message')

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      author: false,
      text: false
    }
  }

  // 以下の２つのhandleChange関数はformの値をウォッチします。
  handleChangeAuthor(event) {
    this.setState({author: event.target.value})
  }

  handleChangeText(event) {
    this.setState({text: event.target.value})
  }

  sendMessage() {
    // 空文字をチェックして、メッセージと書き手が入力されていないならば、早めにreturnする。
    if(this.state.text === false || this.state.author === false) {
      alert("Invalid Submission")
      return
    }
    let message = {
      text: this.state.text,
      author: this.state.author
    }

    // storeメソッドは、新しいメッセージをRethinkDB上のメッセージコレクションに追加する。
    chat.store(message)
  }


  render() {
    return (
      <div>
        <form>
          <div className='center'>
            <button onClick={this.sendMessage.bind(this)}>メッセージ送信</button>
            <input onChange={this.handleChangeAuthor.bind(this)} placeholder='By'></input>
            <input onChange={this.handleChangeText.bind(this)} placeholder='write message here'></input>
          </div>
        </form>
        {/* chatをpropとしてメッセージコンポーネントに渡す。 */}
        <Messages chat={chat} />
      </div>
    )
  }
}

export default App

```

horizonを使って、RethinkDBとのやりとりをするような処理を追加しました。


## Messages.jsxの修正

```js
// ./src/components/messages.jsx

import React, { Component } from 'react'
import Message from './message'

class Messages extends Component {
  constructor(props) {
    super(props)

    this.chat = props.chat

    // Messagesコンポーネントのstateを空配列で初期化。
    this.state = {
      convo: []
    }
  }

  // コンポーネントがマウントされた時、this.chat.watchを用いてデータベースに問い合わせを行う。
  // stateをセットして、コンポーネントにメッセージを持たせて再レンダリングする。
  componentDidMount() {
    this.chat.watch().subscribe(
      (messages) => {
        let convo = messages.map((message) => {
            return message
        })
        this.setState({convo: convo})
      },
      (err) => {
        console.log(err)
      }
    )
  }

  render() {
    let msgjsx = this.state.convo.map((message, i) => {
      return <Message msg={message} key={i} />
    })
    return (
      <div className='container-fluid'>
        {msgjsx}
      </div>
    )
  }
}
export default Messages

```

App.jsxの修正を受けて、Messages.jsxも修正しました。
詳しいことは、コメントで書きました。


# 完成！

Reactコンポーネントの修正を経て、チャットアプリが完成しました。
<img width="451" alt="Screen Shot 2016-06-19 at 2.14.06 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/2537ede0-7ad4-2af8-2be2-53ee41a9e9aa.png">

RethinkDBとの通信を行い、DBからstateにデータを追加しています。

ちなみに、メッセージが追加されるたびにAdmin画面にその旨が表示されます。
<img width="956" alt="Screen Shot 2016-06-19 at 2.23.15 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/77614/00511eb8-f36d-5040-5a07-5de55143d18d.png">


結論：たのしい