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.html
とbundle.js
をpublic/
に投げ入れるようにします。これにより、routes.rb
が特に設定されていない場合、常にpublic/index.html
を読み込むようになります。
webpackのビルドの起点となるエンドポイントはfrontend/src/index.js
とします。index.html
はhtml-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/
に設置します。
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を使ってフロント側でルーティング出来るようにします。
<app>
<menu></menu>
<main></main>
</app>
<menu>
<ul>
<li each={items}><a href="/#{path}">{title}</a></li>
</ul>
const items = [
{path: '/', title: 'ホーム'},
{path: '/about', title: '概要'},
{path: '/contact', title: 'お問い合わせ'}
]
</menu>
<home>
<h1>ここはホームです</h1>
</home>
<about>
<h1>サイト概要です</h1>
</about>
<contact>
<h1>お問い合わせ</h1>
</contact>
ルーティング
riot-routeはシンプルでわかりやすいAPIを提供しているので特に説明は不要かと思います。注意点としては、route.start()
で引数にtrueを与えないとブラウザでの初回読み込み時にルーティングイベントが発火しないことぐらいでしょうか。
// 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やRiotControlを使うと実現できるのですが、ここでは割愛します。
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
確認用の初期データを作成します。
Article.create(
[
{ date: '2016/12/05', title: 'サイト閉鎖のお知らせ', body: 'これまでお世話になりました。' },
{ date: '2016/12/04', title: 'サイトリニューアル!', body: 'サイトをリニューアルしました。これからもよろしくお願いいたします。' }
]
)
マイグレーションして初期データを挿入します。
$ bin/rails db:migrate db:seed
Tag作成
Article一覧を取得、表示するための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>
<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>
// 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)
<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アクセスが適度に遅いため非同期な感じがわかりやすいですね。
以上です!
僕自身まだまだ初心者の域を出ないので、「それ間違ってる」とか「こっちの方がよくね?」等々アドバイスいただけると嬉しいです。質問もお待ちしてます。
ありがとうございました。