Sinatra+Vue.jsでSPA(Hot Module Replacementつき)

Sinatra+Vue.jsでSPAを作っていきます。
SinatraやVue.jsが初めてという方は
[sinatra+vue+webpackで作成したアプリをHerokuで公開する(初心者向け) - Qiita](http://qiita.com/isuke/items/6597f11bbd09cd0f5157)
こちらも合わせてどうぞ。

完成したコードは[こちら](https://github.com/isuke/sinatra-vuejs-template/tree/v0.8.0)で見ることができます。

## フロントエンドの準備

vue-cliを使うのでインストールします。

```shell-session
$ npm install -g vue-cli
```

vue templateを使います。

```shell-session
$ vue init isuke/vuejs-template#v0.8.0 sinatara-vue-sample
```

`isuke/vuejs-template`は著者が、公式の`vuejs-templates/webpack-simple`をもとに作ったvue.jsのテンプレートです。
`vuejs-templates/webpack-simple`とくらべて以下の違いがります。

- babelは使わずにcoffeescriptを使う
- scssは使わずにstylusを使う
- vue-routerが入っている
- axiosが入っている

基本的には同じなのでbabelが使いたい方などは`vuejs-templates/webpack-simple`を使ってもらっても大丈夫です。


```shell-session
$ yarn # npm installでも可
$ npm run dev
```

http://localhost:8080/ をブラウザで開いてみましょう。
一行もコードを書かずにこれだけでwebページができていることがわかると思います。
[Bye]リンクをクリックしてページ遷移ができていることも確認してみてください。

## サーバーサイドの作成

ここからsinatraを使っていきます。

```shell-session
$ bundle init
```

```rb:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

ruby '2.4.1'

gem 'rack-proxy'
gem 'sinatra'
gem 'sinatra-contrib'
```

```shell-session
$ bundle install
```

`rack-proxy`という見慣れないものがありますが、これは後で説明します。

sinatraのコードを書いていきます。

```rb:server.rb
require 'sinatra/base'
require "sinatra/reloader"

class Server < Sinatra::Base
  configure :development do
    register Sinatra::Reloader
  end

  get '/companies.json' do
    companies = []
    1.upto(50) do |i|
      companies << {
        id: i,
        name: "Company #{i}"
      }
    end
    companies.to_json
  end
end
```

```rb:config.ru
require './server'

run Server
```

何の変哲もない　APIが一つあるだけです。

立ち上げて、確認してみましょう。

```shell-session
$ bundle exec rackup
```

http://localhost:9292/companies.json

jsonが返ってきました。
次のステップに進みましょう。

# フロントとサーバーの連携

フロントは`npm run dev`でlocalhost:8080
サーバーは`bundle exec rackup`でlocalhost:9292
にそれぞれアクセスできるようになりました。
これをくっつけましょう。

先程の`config.ru`を`rack-proxy`をつかって書き換えます。

```rb:config.ru
require './server'
require 'rack-proxy'

class AppProxy < Rack::Proxy
  def rewrite_env(env)
    env['HTTP_HOST'] = 'localhost:8080'
    env
  end
end

run Rack::URLMap.new(
 '/api' => Server,
  '/' => AppProxy.new
)
```

サーバーを２つとも立ち上げてlocalhost:9292を見てみましょう。

```shell-session
$ npm run dev
$ bundle exec rackup
```

http://localhost:9292/

ここでトップページが見れたらOKです。
なぜlocalhost:9292でトップページが見れたのかというと、先程の`config.ru`の設定によりlocalhost:9292がlocalhost:8080にマップされたからですね。

さて、これでフロントからAPIが叩けるようになったので、ちょっと試してみましょう。

Bye.vueを以下のように書き換えます。

```html:Bye.vue
<template lang="pug">
.bye
  button(@click="load") load
  ul(v-for="company in companies")
    li {{company.name}}
  router-link(:to="{ name: 'top'}") TOP
</template>

<script lang="coffee">
module.exports = {
  name: 'bye'
  data: ->
    companies: []
  methods:
    load: ->
      axios.get('/api/companies.json')
        .then (responce) =>
          @companies = responce.data
}
</script>

<style lang="stylus">
.bye {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;

  h1 {
    font-weight: normal;
  }

  a {
    color: #42b983;
  }
}
</style>
```

[load]ボタンを押してデータが表示されていれば成功です。

ついでにHot Module Replacementが動作しているかもためしてみましょう。

Compnayデータが表示された状態で`Bye.vue`のstyleを適当に変えてみてください。

```diff
  .bye {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
+   background-color: red;

    h1 {
      font-weight: normal;
    }

    a {
      color: #42b983;
    }
  }
```

ブラウザを**更新せずに**反映されたら成功です。
Hot Module Replacementが動作しています。

## Productionモードで動かす

最後にProductionでの動かし方です。

まず`npm run build`を実行して`src`ディレクトリの中をコンパイルします。
コンパイルした結果は`dist`に吐かれます。

```shell-session
$ npm run build
```

次にURLが

- `/api`で始まる場合はsinatra server
- `/dist`で始まる場合は`dist`ディレクトリ
- `/`の場合は`index.html`

を返すように`config.ru`を書き換えます。

```rb:config.ru
require './server'
require 'rack-proxy'

class Index
  def call(env)
    [
      200,
      { 'Content-Type' => 'text/html' },
      [File.read('./index.html')]
    ]
  end
end

class AppProxy < Rack::Proxy
  def rewrite_env(env)
    env['HTTP_HOST'] = 'localhost:8080'
    env
  end
end

if ENV["APP_ENV"] == "production"
  run Rack::URLMap.new(
    '/api' => Server,
    '/dist' => Rack::Directory.new("./dist"),
    '/' => Index.new
  )
else
  run Rack::URLMap.new(
    '/api' => Server,
    '/' => AppProxy.new
  )
end
```

あとは環境変数をつけて実行してあげるだけです。

```shell-session
$ APP_ENV=production bundle exec rackup 
```

以上です。お疲れ様でした。

## 参考

http://adamjamesbull.co.uk/words/combining-webpack-sinatra