ニートのプログラミング未経験者がRailsとVueでTodoアプリを作ってみた

# はじめに

Vuejs と Rails API を使って Todo アプリを作りました。
まずは、ローカル環境で動かし
最終的に Heroku へデプロイするところまで書きました。

最初に作ったものを載せておきます。

### デモ

https://vue-rails-api-todo.herokuapp.com/

![vue.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29699/8fa82738-27a6-6195-65c7-1b3d70483a8b.gif)

### コード

https://github.com/youbeer/vue-rails-api-todo

### ディレクトリ構成

frontend ディレクトリに Vue のファイルをまとめてあります

```
vue-rails-api-todo/
├── app
│   ├── channels
│   ├── controllers
│   ├── jobs
│   ├── mailers
│   ├── models
│   └── views
├── bin
├── config
├── db
├── docs
├── frontend
│   ├── dist
│   ├── node_modules
│   ├── public
│   └── src
├── lib
├── log
├── public
├── storage
├── test
├── tmp
└── vendor

```

### 対象読者(こんな方に読んでいただけたら)

Rails と Vue のチュートリアルを勉強してなにか作ってみたい方

### 事前準備

Rails と VueCLI3 のインストールを行なってください

### 自分の環境です

Mac MoJava
ruby 2.6.1
Rails 5.2.3
Vue 3.7.0

# 【Rails】サーバーサイドの作成

### Rails プロジェクトを API モードで作る

```bash:terminal
rails new vue-rails-api-todo --api
```

### Gemfile を修正

```ruby:Gemfile

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.1'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'puma', '~> 3.11'
gem 'rack-cors'
gem 'rails', '~> 5.2.3'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

group :development, :test do
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
  gem 'sqlite3'
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'pry-byebug'
  gem 'pry-doc'
  gem 'pry-rails'
  gem 'pry-stack_explorer'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :production do
  gem 'pg'
end
```

### Gem をインストール

```bash:terminal
bundle install
```

### Model を作る

フィールドは２つだけです

1. title: タスクの内容
2. completed: 完了・未完了

```bash:terminal
rails g model Todo title:string completed:boolean
```

### migration ファイルの修正

Not Null 制約 と デフォルト値を追記してます

```ruby:db/migrate/20190525063511_create_tasks.rb
class CreateTodos < ActiveRecord::Migration[5.2]
  def change
    create_table :todos do |t|
      t.string :title, null: false
      t.boolean :completed, default: false, null: false

      t.timestamps
    end
  end
end
```

### マイグレーション

```bash:terminal
rails g model Task title:string completed:boolean
```

### Model にバリデーションを追加

```ruby:app/models/todo.rb
class Todo < ApplicationRecord
  validates :title, presence: true
end
```

### ルーティングの修正

`resources :todos, except: :show`
以外に２つルーティングを追加しました

1. `patch 'check_all', to: 'todos#check_all'`： タスクの完了・未完了
2. `delete 'delete_completed', to: 'todos#delete_completed'`： 完了タスクを全削除

```ruby:config/routes.rb
Rails.application.routes.draw do
  root 'api/v1/todos#index'

  namespace :api do
    namespace :v1, format: :json do
      patch 'check_all', to: 'todos#check_all'
      delete 'delete_completed', to: 'todos#delete_completed'
      resources :todos, except: :show
    end
  end
end
```

ルーティングは詳細は、こんな感じです

````bash:terminal
                   Prefix Verb   URI Pattern                                                                              Controller#Action
                     root GET    /                                                                                        api/v1/todos#index
         api_v1_check_all PATCH  /api/v1/check_all(.:format)                                                              api/v1/todos#check_all
  api_v1_delete_completed DELETE /api/v1/delete_completed(.:format)                                                       api/v1/todos#delete_completed
             api_v1_todos GET    /api/v1/todos(.:format)                                                                  api/v1/todos#index
                          POST   /api/v1/todos(.:format)                                                                  api/v1/todos#create
              api_v1_todo PATCH  /api/v1/todos/:id(.:format)                                                              api/v1/todos#update
                          PUT    /api/v1/todos/:id(.:format)                                                              api/v1/todos#update
                          DELETE /api/v1/todos/:id(.:format)                                                              api/v1/todos#destroy
       rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
       rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
     rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create
```

### controller の用意

```bash:terminal
rails g controller api::v1::todos
````

### controller を修正

```ruby:app/controllers/api/v1/todos_controller.rb
class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: %i[show update destroy]

  # GET api/vi/todos/
  def index
    @todos = Todo.all.order(created_at: :asc)
    render json: @todos
  end

  # Post api/vi/todos
  def create
    @todo = Todo.new(todo_params)
    if @todo.save
      render json: @todo
    else
      render json: { status: 'error', data: @todo.errors }
    end
  end

  # Put api/vi/todos/:id
  def update
    if @todo.update(todo_params)
      render json: @todo
    else
      render json: { status: 'error', data: @todo.errors }
    end
  end

  # Delete api/vi/todos/:id
  def destroy
    @todo.destroy
    render json: @todo
  end

  # Delete api/vi/delete_completed
  def delete_completed
    todo = Todo.where(completed: true).delete_all
    render json: todo
  end

  # Put api/vi/check_all
  def check_all
    todo = Todo.update_all(completed: params['checked'])
    render json: todo
  end

  private

  def todo_params
    params.require(:todo).permit(:title, :completed)
  end

  def set_todo
    @todo = Todo.find(params[:id])
  end
end

```

### cors の設定ファイルを修正

Vue 側からのアクセスを許可するため追記

```ruby
origins 'http://localhost:8080'
```

```ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end
```

### seed ファイルを修正

テストデータ作成用

```ruby:db/seeds.rb
10.times do |i|
  Todo.create(title: "title No#{i + 1}", completed: i.even?)
end
```

### postman で確認してみる

```bash:terminal
rails s
rails db:seed
```

Postman をインストールされていない方は、[こちら](https://www.getpostman.com/)からインストールしてください

1. プルダウンから GET を選択し http://localhost:3000/api/v1/todos を入力
2. Send をクリック
3. テストデータの json が返ってくることを確認

時間がある方はその他のアクションも試してみてください
やり方は、ここでは割愛します

![Screen Shot 2019-05-25 at 20.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29699/af7afb0f-9aa4-edae-d86f-0a1ef5d7771e.png)

# 【Vue】フロントエンドの作成

### プロジェクトを作成

Rails プロジェクトの直下に frontend という名前で Vue プロジェクトを作成します

```bash:terminal
vue create frontend
```

いくつか質問がでてくるので

- Manually select features を選択肢し
- Vuex を追加してください

その他はお好みでどうぞ

```bash:terminal
? Please pick a preset:
default (babel, eslint)
❯ Manually select features
```

```bash:terminal
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```

インストールが完了したら fronend へ移動しサーバを起動してみましょう

```bash:terminal
cd frontend && yarn serve
```

ブラウザから http://localhost:8080/ へアクセスし
こんな画面が表示されたら成功です。

![Screen Shot 2019-05-25 at 16.03.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29699/66d0175f-0c22-5057-6df0-17dd4a94f623.png)

### Bootstrap と axios を追加

- [BootstrapVue](https://bootstrap-vue.js.org/docs)： Vue 用の Bootstrap モジュール
- [axios](https://github.com/axios/axios)： 今時の ajax モジュール

```bash:terminal
yarn add bootstrap-vue bootstrap axios
```

### bootstrap の設定を追加

bootstrap を使うため main.js に追記

```javascript
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

Vue.use(BootstrapVue);
```

```javascript:frontend/src/main.js
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

Vue.use(BootstrapVue);
Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount("#app");
```

### store を編集

```javascript:frontend/src/store.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

const http = axios.create({
  baseURL:
    process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "/",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  responseType: "json"
});

export default new Vuex.Store({
  state: {
    filter: "all",
    todos: []
  },
  getters: {
    remaining(state) {
      return state.todos.filter(todo => !todo.completed).length;
    },
    completedAll(state, getters) {
      return getters.remaining === 0;
    },
    todosFiltered(state) {
      if (state.filter === "all") {
        return state.todos;
      } else if (state.filter === "active") {
        return state.todos.filter(todo => !todo.completed);
      } else if (state.filter === "completed") {
        return state.todos.filter(todo => todo.completed);
      }
      return state.todos;
    },
    showClearCompletedButton(state) {
      return state.todos.filter(todo => todo.completed).length > 0;
    }
  },
  mutations: {
    addTodo(state, todo) {
      state.todos.push({
        id: todo.id,
        title: todo.title,
        completed: false,
        editing: false
      });
    },
    clearCompleted(state) {
      state.todos = state.todos.filter(todo => !todo.completed);
    },
    updateFilter(state, filter) {
      state.filter = filter;
    },
    checkAll(state, checked) {
      state.todos.forEach(todo => {
        todo.completed = checked;
      });
    },
    deleteTodo(state, id) {
      const index = state.todos.findIndex(todo => todo.id === id);
      state.todos.splice(index, 1);
    },
    updateTodo(state, todo) {
      const index = state.todos.findIndex(item => item.id === todo.id);

      state.todos.splice(index, 1, {
        id: todo.id,
        title: todo.title,
        completed: todo.completed,
        editing: todo.editing
      });
    },
    retrieveTodos(state, todos) {
      state.todos = todos;
    }
  },
  actions: {
    retrieveTodos({ commit }) {
      http
        .get("/api/v1/todos")
        .then(response => {
          commit("retrieveTodos", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    addTodo({ commit }, todo) {
      http
        .post("/api/v1/todos", {
          title: todo.title,
          completed: false
        })
        .then(response => {
          commit("addTodo", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    clearCompleted({ commit }) {
      http
        .delete("/api/v1/delete_completed")
        .then(response => {
          commit("clearCompleted", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    checkAll({ commit }, checked) {
      http
        .patch("/api/v1/check_all", {
          checked
        })
        .then(() => {
          commit("checkAll", checked);
        })
        .catch(error => {
          console.log(error);
        });
    },
    deleteTodo({ commit }, id) {
      http
        .delete(`/api/v1/todos/${id}`)
        .then(response => {
          commit("deleteTodo", response.data.id);
        })
        .catch(error => {
          console.log(error);
        });
    },
    updateTodo({ commit }, todo) {
      http
        .patch(`/api/v1/todos/${todo.id}`, {
          title: todo.title,
          completed: todo.completed
        })
        .then(response => {
          commit("updateTodo", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    }
  }
});
```

store は大きく５つのブロックに分かれています

#### axios のデフォルト通信設定

- baseURL： API 取得のための URL
- header：リクエスト時のヘッダの値
- responseType：レスポンスの形式

```javascript
import axios from "axios";

Vue.use(Vuex);

const http = axios.create({
  baseURL:
    process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "/",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  responseType: "json"
});
```

#### state

アプリの状態管理をするための単一オブジェクトです

[ステート \| Vuex](https://vuex.vuejs.org/ja/guide/state.html)

- todo： タスクの配列
- filter：全て ・ 完了 ・ 未完了 のフィルタ

```javascript
state: {
  filter: "all",
  todos: []
},
```

#### getters

component でいう computed にあたります

[ゲッター \| Vuex](https://vuex.vuejs.org/ja/guide/getters.html)

- remaining: タスク完了の件数
- showClearCompletedButton: クリアボタンを 表示 ・ 非表示 の切替用

```javascript
getters: {
    remaining(state) {
      return state.todos.filter(todo => !todo.completed).length;
    },

    /*************** 省略 ***************/

    showClearCompletedButton(state) {
      return state.todos.filter(todo => todo.completed).length > 0;
    }
}
```

#### mutaition

state を変更するためのメソッド群です
action を経由して state を更新するために使っています

[ミューテーション \| Vuex](https://vuex.vuejs.org/ja/guide/mutations.html)

- addTodo: 新しいタスクを追加しています
- retrieveTodos: ページに最初にアクセスしたとき、タスク一覧を作成しています

```javascript
mutations: {
  addTodo(state, todo) {
    state.todos.push({
      id: todo.id,
      title: todo.title,
      completed: false,
      editing: false
    });
  },

  /*************** 省略 ***************/

  retrieveTodos(state, todos) {
    state.todos = todos;
  }
},
```

#### actions

非同期処理を行うためのメソッド群です
axios を使って Rails API を取得するために使っています

[アクション \| Vuex](https://vuex.vuejs.org/ja/guide/actions.html)

- retrieveTodos: Rails の API からタスク一覧を取得しています
- updateTodo: Rails の API からタスクの更新結果を取得しています

```javascript
 actions: {
  retrieveTodos({ commit }) {
    http
      .get("/api/v1/todos")
      .then(response => {
        commit("retrieveTodos", response.data);
      })
      .catch(error => {
        console.log(error);
      });
  },

  /*************** 省略 ***************/

  updateTodo({ commit }, todo) {
    http
      .patch(`/api/v1/todos/${todo.id}`, {
        title: todo.title,
        completed: todo.completed
      })
      .then(response => {
        commit("updateTodo", response.data);
      })
      .catch(error => {
        console.log(error);
      });
  }
}
```

### App を編集

```vue:frontend/src/App.vue
<template>
  <div id="app" class="container">
    <img alt="Vue logo" src="./assets/logo.png" class="logo" />
    <h1>VueTODO</h1>
    <todo-list></todo-list>
  </div>
</template>

<script>
import TodoList from "./components/TodoList.vue";

export default {
  name: "App",
  components: {
    TodoList
  }
};
</script>

<style lang="scss" scoped>
.logo {
  margin: 0 auto;
  display: block;
}
</style>
```

メインとなる TodoList コンポーネントを呼び出しています

```javascript
import TodoList from "./components/TodoList.vue";

export default {
  name: "App",
  components: {
    TodoList
  }
};
```

### TodoList コンポーネントを作成

新しいタスクの追加 と 子コンポーネントを束ねています

```vue:frontend/src/components/TodoList.vue
<template>
  <div>
    <b-container class="bv-example-row">
      <b-row>
        <b-col cols="12">
          <b-form @submit.prevent="addTodo">
            <b-form-group label="New todo" label-for="new-todo">
              <b-form-input
                id="new-todo"
                v-model="newTodo"
                placeholder="What needs to be done?"
              ></b-form-input>
            </b-form-group>
          </b-form>

          <b-list-group>
            <transition-group name="fade">
              <TodoItem
                v-for="(todo, index) in todosFiltered"
                :key="todo.id"
                :todo="todo"
                :index="index"
                class="todo-item"
                :check-all="completedAll"
              />
            </transition-group>
          </b-list-group>

          <b-list-group class="mt-4">
            <b-list-group-item
              class="flex-wrap d-flex justify-content-around align-items-center"
            >
              <TodoCheckAll />
              <TodoItemsRemaining />
            </b-list-group-item>
            <b-list-group-item
              class="flex-wrap d-flex justify-content-around align-items-center"
            >
              <TodoFiltered />
              <TodoClearCompleted />
            </b-list-group-item>
          </b-list-group>
        </b-col>
      </b-row>
    </b-container>
  </div>
</template>

<script>
import TodoItem from "@/components/TodoItem";
import TodoItemsRemaining from "@/components/TodoItemsRemaining";
import TodoCheckAll from "@/components/TodoCheckAll";
import TodoFiltered from "@/components/TodoFiltered";
import TodoClearCompleted from "@/components/TodoClearCompleted";
import { mapGetters } from "vuex";

export default {
  name: "TodoList",
  components: {
    TodoItem,
    TodoItemsRemaining,
    TodoCheckAll,
    TodoFiltered,
    TodoClearCompleted
  },
  data() {
    return {
      newTodo: ""
    };
  },
  computed: {
    ...mapGetters(["completedAll", "todosFiltered"])
  },
  created() {
    this.$store.dispatch("retrieveTodos");
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.$store.dispatch("addTodo", {
          id: this.idForTodo,
          title: this.newTodo
        });
      }
      this.newTodo = "";
    }
  }
};
</script>

<style lang="scss">
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>
```

### TodoItem コンポーネントを作成

親コンポーネントの TodoList から props を受け取り
個々のタスクの表示させています

```vue:frontend/src/components/TodoItem.vue
<template>
  <b-list-group-item
    class="flex-wrap d-flex justify-content-around align-items-center todo-item"
  >
    <b-col cols="2">
      <b-form-checkbox v-model="completed" @input="doneEdit"></b-form-checkbox>
    </b-col>

    <b-col cols="8">
      <label
        v-if="!editing"
        :class="{ completed: completed }"
        @dblclick="editing = true"
        >{{ title }}</label
      >
      <b-form-input
        v-else
        v-model="title"
        v-focus
        type="text"
        @blur="doneEdit"
        @keyup.enter="doneEdit"
        @keyup.escape="cancelEdit"
      />
    </b-col>

    <b-col cols="2">
      <button
        type="button"
        class="close"
        aria-label="Close"
        @click="deleteTodo(todo.id)"
      >
        <span aria-hidden="true">&times;</span>
      </button>
    </b-col>
  </b-list-group-item>
</template>
<script>
import { mapActions } from "vuex";
export default {
  name: "TodoItem",
  directives: {
    focus: {
      inserted: function(el) {
        el.focus();
      }
    }
  },
  props: {
    todo: {
      type: Object,
      required: true
    },
    index: {
      type: Number,
      required: true
    },
    checkAll: {
      type: Boolean,
      required: true
    }
  },
  data() {
    return {
      id: this.todo.id,
      title: this.todo.title,
      completed: this.todo.completed,
      editing: false
    };
  },
  watch: {
    checkAll() {
      this.completed = this.checkAll ? true : this.todo.completed;
    }
  },
  methods: {
    ...mapActions(["deleteTodo", "updateTodo"]),
    doneEdit() {
      this.editing = false;

      this.updateTodo({
        id: this.id,
        title: this.title,
        completed: this.completed,
        editing: this.editing
      });
    },
    cancelEdit() {
      this.title = this.todo.title;
      this.editing = false;
    }
  }
};
</script>

<style lang="scss" scoped>
.todo-item {
  animation-duration: 0.3s;
}

.completed {
  text-decoration: line-through;
  color: grey;
}
</style>
```

### TodoItemsRemaining コンポーネントを作成

残りのタスク件数を表示させています

```vue:frontend/src/components/TodoItemsRemaining.vue
<template>
  <b-col cols="6">
    <span class="text-danger">{{ remaining }}</span>
    {{ remaining | pluralize("item") }} left
  </b-col>
</template>

<script>
import { mapGetters } from "vuex";
export default {
  name: "TodoItemsRemaining",
  filters: {
    pluralize: (n, w) => (n === 1 ? w : w + "s")
  },
  computed: {
    ...mapGetters(["remaining"])
  }
};
</script>
```

### TodoFiltered コンポーネントを作成

All(全て) ・ Active(未完了) ・ Completed(完了)
の値によってタスクにフィルタをかけています

```vue:frontend/src/components/TodoFiltered.vue
<template>
  <b-col cols="6">
    <b-form-radio-group
      v-model="selected"
      :options="options"
      buttons
      button-variant="outline-primary"
      name="radio-btn-outline"
      @change="updateFilter"
    ></b-form-radio-group>
  </b-col>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
  name: "TodoFiltered",
  data() {
    return {
      selected: "all",
      options: [
        { text: "All", value: "all" },
        { text: "Active", value: "active" },
        { text: "Completed", value: "completed" }
      ]
    };
  },
  computed: {
    ...mapState(["filter"])
  },
  methods: {
    ...mapMutations(["updateFilter"])
  }
};
</script>
```

### TodoClearCompleted コンポーネントを作成

完了したタスクの一括クリアボタンを表示させています

```vue:frontend/src/components/TodoClearCompleted.vue
<template>
  <b-col cols="6">
    <div>
      <b-button
        v-if="showClearCompletedButton"
        variant="outline-primary"
        @click="clearCompleted"
        >Clear Completed</b-button
      >
    </div>
  </b-col>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "TodoClearCompleted",
  computed: {
    ...mapGetters(["showClearCompletedButton"])
  },
  methods: {
    ...mapActions(["clearCompleted"])
  }
};
</script>
```

### TodoCheckAll コンポーネントを作成

タスクを一括で完了 ・ 未完了に切り替えるための
チェックボックスを表示させています

```vue:frontend/src/components/TodoCheckAll.vue
<template>
  <b-col cols="6">
    <b-form-checkbox :checked="completedAll" @change="checkAll"
      >Check All</b-form-checkbox
    >
  </b-col>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "TodoCheckAll",
  computed: {
    ...mapGetters(["completedAll"])
  },
  methods: {
    ...mapActions(["checkAll"])
  }
};
</script>
```

### ブラウザで確認

Rail のサーバを起動

```bash:terminal
rails s
```

Vue のサーバを起動

```bash:terminal
cd frontend
yarn serve
```

localhost:8080 にアクセスしてこんな画面が表示されたら成功です

![Screen Shot 2019-05-27 at 11.00.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29699/050beb89-87b6-16b0-06ec-8a604bd32298.png)

# Heroku へデプロイしてみる

### 事前準備

Heroku のアカウントがない場合は[こちらから](https://jp.heroku.com/)作成してください

デプロイには heroku toolbelt が必要なので[こちらから](https://devcenter.heroku.com/articles/heroku-command-line)インストールしてください

mac の場合は Homebrew でインストール可能です

```bash:terminal
brew install heroku
```

### プロジェクトを commit

プロジェクト直下へ移動し commit を行なってください

```bash:terminal
git init
git add .
git commit -m "init"
```

### vue.config.js を作成

frontend ディレクトリの直下に vue.config.js ファイルを作成し
build ファイルの出力先をプロジェクト直下の public ディレクトリへ変更します

```javascript:frontend/vue.config.js
module.exports = {
  outputDir: "../public"
};
```

### Vue を build

```bash:terminal
yarn build
```

プロジェクト直下の public ディレクトリに
build されたファイルが作成されていることを確認してください

```bash:terminal
public/
├── css
│   ├── app.27d4506b.css
│   └── chunk-vendors.19588e8d.css
├── favicon.ico
├── img
│   └── logo.82b9c7a5.png
├── index.html
└── js
    ├── app.6635e2d3.js
    ├── app.6635e2d3.js.map
    ├── chunk-vendors.4ad97586.js
    └── chunk-vendors.4ad97586.js.map
```

### Heroku にログイン

```bash:terminal
heroku login
```

上のコマンドを実行するとブラウザに切替わるのでボタンを押してログインしてください

![Screen Shot 2019-05-27 at 11.15.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29699/6da02390-9ace-01f0-5f5a-64c1824df956.png)

### Heroku にアプリを作成

アプリ名を入力すると URL にアプリ名が反映されます
https://アプリ名.herokuapp.com/
省略すると Heroku 側で自動的に割り振られます

```bash:terminal
heroku create アプリ名
```

### Heroku のリポジトリへ push

```bash:terminal
git push heroku master
```

### データベースの migration と テストデータを追加

```bash:terminal
heroku run rails db:migrate
heroku run rails db:seed
```

### ブラウザで確認

```bash:terminal
heroku open
```

# おわりに

最後まで読んでいただきありがとうございました。
おかしな部分がありましたら、ご指摘お願いします。