19
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Riot.jsAdvent Calendar 2016

Day 5

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

Posted at

Riot.js Advent Calendar 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します。

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

不要なファイルを削除

別に残しておいても構いませんが、わかりやすいように削除しておきます。

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

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

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

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

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

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

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

$ 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/に設置します。

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.jsindex.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間で同期したりするにはobservableRiotControlを使うと実現できるのですが、ここでは割愛します。

Herokuへデプロイ

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

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

node.jsミドルウェア

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

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

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

$ 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を追加します。

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

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

$ 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

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

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

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

$ 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しておき、下記コマンドでデプロイします。

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

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

以上です!


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

19
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?