Rails/Riot.jsで作ったSPAをHerokuで動かす

[Riot.js Advent Calendar](http://qiita.com/advent-calendar/2016/riotjs) 5日目、Riot.js、Rails初心者向けです。アドベントカレンダー初参加！


本稿では、フロントエンドとバックエンドのファイルを分離したシングルページアプリケーションをRailsとRiot.jsで作っていきます。最終的にそれをHerokuにデプロイします。

ソースと動作サンプル
https://github.com/d-mato/rails-riot-spa
https://rails-riot-spa.herokuapp.com/

## Railsをインストール
とりあえずいつものようにrails newします。

```bash
$ mkdir rails-riot-spa; cd $_
$ bundle init # Gemfile生成、エディタで開きgem 'rails'をアンコメントする
$ bundle i --path vendor/bundle
$ bundle exec rails new . -f
```

### 不要なファイルを削除
別に残しておいても構いませんが、わかりやすいように削除しておきます。

```bash
$ rm -r app/assets/ app/views/layouts/application.html.erb
```

## フロントエンドのビルド環境を構築

フロントエンド用のファイルは全て``frontend/``に置き、webpackでビルドした``index.html``と``bundle.js``を``public/``に投げ入れるようにします。これにより、``routes.rb``が特に設定されていない場合、常に``public/index.html``を読み込むようになります。

webpackのビルドの起点となるエンドポイントは``frontend/src/index.js``とします。``index.html``は``html-webpack-plugin``プラグインによって自動生成します。

```bash
$ mkdir -p frontend/src
$ touch frontend/src/index.js
```

### node.jsモジュールをインストール

Webpack、Riot.jsだけでなく、ES6に対応するためbabel関連も入れておきます。

```bash
$ npm init
$ npm i -D riot riot-route webpack html-webpack-plugin riotjs-loader babel-core babel-loader babel-preset-es2015
```

### webpack.config.js

Rails.rootをあまり散らかしたくないので``frontend/``に設置します。

```js:frontend/webpack.config.js
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const babel_options = { presets: ['es2015'] }

module.exports = {
  entry: `${__dirname}/src/index.js`,
  output: {
    path: `${__dirname}/../public`,
    filename: 'bundle.js'
  },

  plugins: [
    new HtmlWebpackPlugin({title: 'Rails/Riot.js SPA'}),
    new webpack.ProvidePlugin({riot: 'riot'})
  ],
  module: {
    loaders: [
      { test: /\.tag$/, loader: `babel?${JSON.stringify(babel_options)}!riotjs`, exclude: /node_modules/ },
      { test: /\.js$/, loader: `babel?${JSON.stringify(babel_options)}`, exclude: /node_modules/ },
      { test: /index.html$/, loader: 'file' }
    ]
  },

  devtool: "#source-map"
}
```

``package.json``に下記ビルド用のコマンドを追加しておきます。

```
"scripts": {
  "build": "webpack -c frontend/webpack.config.js -w"
}
```

``$ npm run build``を実行すると``bundle.js``と``index.html``が生成されます。
``$ bin/rails s``でRailsを起動し、ブラウザでhttp://localhost:3000/ にアクセスしてみます。インストール直後のサンプルページではなく、空っぽのページがエラー無く表示されたらひとまず環境構築は完了です。

## Riot.jsのTagファイルを作成

いくつかのビュー(Tag)を作成し、riot-routeを使ってフロント側でルーティング出来るようにします。

```frontend/src/app.tag
<app>
  <menu></menu>
  <main></main>
</app>
```

```frontend/src/menu.tag
<menu>
  <ul>
    <li each={items}><a href="/#{path}">{title}</a></li>
  </ul>

  const items = [
    {path: '/', title: 'ホーム'},
    {path: '/about', title: '概要'},
    {path: '/contact', title: 'お問い合わせ'}
  ]
</menu>
```

```frontend/src/home.tag
<home>
  <h1>ここはホームです</h1>
</home>
```

```frontend/src/about.tag
<about>
  <h1>サイト概要です</h1>
</about>
```

```frontend/src/contact.tag
<contact>
  <h1>お問い合わせ</h1>
</contact>
```

### ルーティング
riot-routeはシンプルでわかりやすいAPIを提供しているので特に説明は不要かと思います。注意点としては、``route.start()``で引数にtrueを与えないとブラウザでの初回読み込み時にルーティングイベントが発火しないことぐらいでしょうか。

```frontend/src/index.js
// tags
require('./app.tag')
require('./menu.tag')
require('./home.tag')
require('./about.tag')
require('./contact.tag')

document.body.appendChild(document.createElement('app'))
riot.mount('app')

// routes
import route from 'riot-route'

route('/', () => riot.mount('main', 'home'))
route('/about', () => riot.mount('main', 'about'))
route('/contact', () => riot.mount('main', 'contact'))

route.start(true) // trueを渡さないと初回読み込み時にルーティングイベントが発火しない
```  

ここまでで簡単なSPAは完成しました。表示中のメニュー項目をCSSでハイライトしたり、Tag間で同期したりするには[observable](http://riotjs.com/api/observable/)や[RiotControl](https://github.com/jimsparkman/RiotControl)を使うと実現できるのですが、ここでは割愛します。

## Herokuへデプロイ
作成したアプリケーションをHerokuで動かしてみます。まずHerokuリポジトリを新規作成します。

```bash
$ heroku create (適当なアプリ名)
```


### node.jsミドルウェア

ここまでの変更をcommitしておき、``$ git push heroku master`` するとRailsのデプロイは成功しますが、フロントエンド用のファイルをビルドするnpmのコマンドが実行されないため、アプリケーションは正常に立ち上がりません。rubyの他にnode.jsのbuildpackを追加する必要があります。

```bash
$ heroku buildpacks:clear
$ heroku buildpacks:add heroku/nodejs
$ heroku buildpacks:add heroku/ruby
```

これでpush後に``npm install``が実行されるようになりますが、デフォルト(Production環境)ではpackage.jsonのdevDependenciesに追加したnodeモジュールがインストールされないため環境変数を追加します (devDependenciesではなく、dependenciesとして追加しておけばインストールされるはずですが、それ以外にベストプラクティスがあれば知りたいです)。

```bash
$ heroku config:set NPM_CONFIG_PRODUCTION=false
```

また、``npm install``と同時にwebpackによるビルドが自動で行われるように``postinstall``という設定項目を``package.json``に追加します。

```
"scripts": {
  "postinstall": "npm run build",
   "build": "webpack --config frontend/webpack.config.js"
}
```

### DB変更

HerokuではSqlite3を扱えないため、Postgresql用のgemを追加します。

```ruby
gem 'sqlite3', groups: [:development, :test]
gem 'pg', group: :production
```

一旦pushした後にマイグレーションしておきます。

```bash
$ git push heroku master
$ heroku run rails db:migrate
```

これで``$ heroku open``するとheroku上でアプリケーションが立ち上がるはずです。


## おまけ: データベース利用
せっかくRailsを使っているので、モデルを追加し、RESTfulに取得したJSONをRiot.js側で表示してみます。

### リソース作成

```
$ bin/rails g scaffold Article title:string body:text
```

確認用の初期データを作成します。

```ruby:db/seeds.rb
Article.create(
  [
    { date: '2016/12/05', title: 'サイト閉鎖のお知らせ', body: 'これまでお世話になりました。' },
    { date: '2016/12/04', title: 'サイトリニューアル！', body: 'サイトをリニューアルしました。これからもよろしくお願いいたします。' }
  ]
)
```

マイグレーションして初期データを挿入します。

```bash
$ bin/rails db:migrate db:seed
```

### Tag作成

Article一覧を取得、表示するためのTagを作成し、それに伴ってルーティングも追加します。

```frontend/src/articles.tag
<articles>
  <h1>記事一覧</h1>
  <ul>
    <li each={articles}>{date} <a href="/#/articles/{id}"><strong>{title}</strong></a></li>
  </ul>

  fetch('/articles.json').then( (res) => res.json() ).then( (json) => {
    this.update({articles: json})
  })
</articles>
```

```frontend/src/article.tag
<article>
  <virtual if={article}>
    <h1>記事</h1>
    <h2>{article.title}</h2>
    <i>{article.date}</i>
    <p>{article.body}</p>
  </virtual>
  <a href="/#/articles">戻る</a>

  fetch(`/articles/${this.opts.id}.json`).then( (res) => res.json() ).then( (json) => {
    this.update({article: json})
  })
</article>
```

```frontend/src/index.js
// tags
require('./app.tag')
require('./menu.tag')
require('./home.tag')
require('./about.tag')
require('./contact.tag')
require('./articles.tag') // 追加
require('./article.tag') // 追加

document.body.appendChild(document.createElement('app'))
riot.mount('app')

// routes
import route from 'riot-route'

route('/', () => riot.mount('main', 'home'))
route('/about', () => riot.mount('main', 'about'))
route('/contact', () => riot.mount('main', 'contact'))
route('/articles', () => riot.mount('main', 'articles'))  // 追加
route('/articles/*', (id) => riot.mount('main', 'article', {id}))  // 追加

route.start(true)
```

```frontend/src/menu.tag
<menu>
  <ul>
    <li each={items}><a href="/#{path}">{title}</a></li>
  </ul>

  this.items = [
    {path: '/', title: 'ホーム'},
    {path: '/about', title: '概要'},
    {path: '/contact', title: 'お問い合わせ'},
    {path: '/articles', title: '記事一覧'} // 追加
  ]
</menu>
```

``$ npm run build`` してブラウザをリロードすると記事一覧ページと個別ページが表示されるようになります。
webpackに``-w``オプションをつけたタスクを``package.json``に追加しておくと、.tag .jsファイルを保存した際に自動でビルドが再実行されるので便利です (本当はwebpack-dev-serverのリロード機能を使いたかったのですが、ポートが異なるRailsとの同時使用の方法がわかりませんでした) 。

### デプロイ

変更分をcommitしておき、下記コマンドでデプロイします。

```bash
$ git push heroku master
$ heroku run rails db:migrate db:seed
```

余談ですがHerokuだとDBアクセスが適度に遅いため非同期な感じがわかりやすいですね。

以上です！

---

僕自身まだまだ初心者の域を出ないので、「それ間違ってる」とか「こっちの方がよくね？」等々アドバイスいただけると嬉しいです。質問もお待ちしてます。
ありがとうございました。