ゼロから作るPhoenix 1.4 + Vue.jsによるSPA

# はじめに

先日，[Erlang & Elixir Fest 2019](https://elixir-fest.jp/)に参加し，とても刺激を受けたのでペーペーな私ですが記事を書いてみようと思ったので書きます．
下図に示すような感じで，Phoenix + Vue.jsでSPA(Single page application)をゼロから構成するお話です．チュートリアルですね．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/f4472d3a-e6e7-273d-e56f-e7cb9fd213ad.png)

Phoenixではjsonを返すAPIを作成し，vueからはaxiosを使ってAPIを叩いて表示します．

私自身がPhoenixもVueも始めてから日が浅いので，実際に作ってみた所感を記事に起こしたって感じです(笑)
ゼロから作るとか謳ってますが，phoenixの魔法や色々なライブラリに頼りまくりなので全然ゼロからではありません．
初心者向けくらいの意味でとらえていただけると幸いです．ご容赦ください🙇‍♂️

ここで作成するプロジェクトのソースコードはGitHubで公開しています．(絶賛更新中です...)
全てのコードを解説はできないので是非以下も参照してください．
[sample_spa リポジトリ](https://github.com/wf-yamaday/sample_spa/tree/6dfa060cd0f97388166299da701c3e3764ba6b83)

# プロジェクトの作成

今回は，掲示板システムを想定して作成します．
下図の様な感じです．1つの掲示板内には複数の投稿があるというだけです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/83cbad8a-17a7-3a96-c5ed-6cb0e62d971d.png)


一応，実行環境は以下の通りです．

- macOS Mojave
- Elixir 1.8.1
- Phoenix 1.4.
- MySQL 5.7


## API (Phoenix)の作成

それでは，みんな大好き`mix phx.new`でPhoenixのプロジェクトを作成します．
プロジェクト名は`sample_spa`とします．

```
$  mix phx.new sample_spa --database mysql
```

### MySQLの設定

ここは各々によりますが，ユーザーやパスワードの設定をしておく場合はしておきましょう．
MySQLの設定は`config/dev.exs`に記述します．
適宜，`username`や`password`を設定します．

```elixir:config/dev.exs
# Configure your database
config :sample, Sample.Repo,
  username: "root",
  password: "",
  database: "sample_spa_dev",
  hostname: "localhost",
  pool_size: 10
```

ここまで完了したら，dbのセットアップをしてみましょう．

```
$ mysql.server start
$ mix ecto.setup
```

MySQLに`sample_spa_dev`というデータベースが作成されていれば問題ないです🙆‍♂️

### modelの作成

続いてmodelの作成をします．
今回のboardとpostの2つのモデルは掲示板のお話なので，bbsというコンテキストで定義します．

#### boardの作成

boardモデルは以下のような要素を持っています．

|board||
|:--|:--|
|id|mysql側でauto_increment|
|title|掲示板のタイトル|
|description|掲示板の概要|


あんまり魔法は使いたくないのですがboardモデルの作成には`mix phx.gen.json`という魔法を使わせていただきます🙇‍♂️

```
$ mix phx.gen.json Bbs Board boards title:string description:string
```

色々とファイルが作成されますが，注目すべきファイルの簡単な説明を以下に載せます．

- `lib/sample_spa/bbs/board.ex` ... 作成したモデルの定義
- `lib/sample_spa/bbs.ex` ... bbsコンテキストのビジネスロジックを記述
- `lib/sample_spa_web/controllers/board_controller.ex` ... リクエストに対応したビジネスロジックを呼び出す
- `lib/sample_spa_web/views/board_view.ex` ... レスポンスの整形を行う

`ecto.migrate`コマンドでdbにboardsテーブルを作成しましょう．

```
$ mix ecto.migrate
```

MySQLに`boards`テーブルができてれば問題ないです🙆‍♂️

#### postの作成

|post||
|:--|:--|
|id|mysql側でauto_increment|
|body|投稿内容|
|board_id|掲示板のid|

postモデルの作成には`mix phx.gen.schema`で作成します．

```
$ mix phx.gen.schema Post posts body:string
```

`lib/sample_spa/post.ex`と`priv/repo/migrations/~_create_posts.exs`が作成されます．

postもbbsコンテキストのmodelなのでわかりやすいように`lib/sample_spa/bbs`ディレクトリに移動しておきましょう．

```
$ mv lib/sample_spa/post.ex lib/sample_spa/bbs
```

移動したら，defmoduleの部分を変更しておきましょう．

```elixir:lib/sample_spa/bbs/post.ex
defmodule SampleSpa.Post do # 変更前
defmodule SampleSpa.Bbs.Post do # 変更後
```

#### boardとpostの関係を追加

boardとpostが一対多の関係であることを追記します．
まずはboard.ex側にpostを複数持つことを記述します．

```elixir:lib/sample_spa/bbs/board.ex
  schema "boards" do
    field :description, :string
    field :title, :string

    # postとの関係
    has_many :posts, SampleSpa.Bbs.Post, on_delete: :delete_all

    timestamps()
  end
```

次にpost.ex側にboardを一つ持つことを記述します．

```elixir:lib/sample_spa/bbs/post.ex
  schema "posts" do
    field :body, :string

    # boardとの関係
    belongs_to :board, SampleSpa.Bbs.Board, foreign_key: :board_id

    timestamps()
  end
```

最後にマイグレーションファイルに外部キーの`board_id`を加えておきましょう．

```elixir:priv/repo/migrations/~_create_posts.exs
    create table(:posts) do
      add :body, :string
      
      # board_idを追加する
      add :board_id, references(:boards, on_delete: :delete_all), null: false

      timestamps()
    end
```

ここまでしたら，`ecto.migrate`コマンドでdbにpostsテーブルを作成しましょう．

```
$ mix ecto.migrate
```

MySQLに`posts`テーブルができてれば問題ないです🙆‍♂️


### ルーティングの設定

ルーティングの設定の指針を先に示しておきます．
今回は以下の表に示すようなエンドポイントになります．

|path|httpメソッド|内容|
|:--|:--|:--|
|/api/boards|GET|掲示板を全件取得|
|/api/boards|POST|掲示板を新規作成|
|/api/boards/:id|GET|指定した掲示板内の投稿を全件取得|
|/api/boards/:id|POST|指定の掲示板内に新規投稿を作成|

`lib/sample_aps_web/route.ex`に記述を追加してルーティングの設定を行います．
まずは掲示板の全件取得と掲示板を新規作成するルーティングを追加しましょう．

```elixir:lib/sample_aps_web/route.ex
  scope "/api", SampleSpaWeb do
    pipe_through :api
    
    scope "/boards" do
      get "/", BoardController, :index
      post "/", BoardController, :create
      get "/:id", BoardController, :show
      post "/:id", BoardController, :create_post
    end
  end
```

ルーティングの設定ができてるか確認しましょう．

```
$ mix phx.routes
board_path  GET   /api/boards                            SampleSpaWeb.BoardController :index
board_path  POST  /api/boards                            SampleSpaWeb.BoardController :create
board_path  GET   /api/boards/:id                        SampleSpaWeb.BoardController :show
board_path  POST  /api/boards/:id                        SampleSpaWeb.BoardController :create_post
 page_path  GET   /                                      SampleSpaWeb.PageController :index
 websocket  WS    /socket/websocket                      SampleSpaWeb.UserSocket
```

ちゃんとできてますね！

### postの新規作成，全件取得の処理の作成

boardの作成・取得の処理は自動生成されていますが，postに関してはないので追加してきます．

#### viewの用意

最初に，postモデルをレスポンスでjsonとして返すときのviewを用意しておきましょう．

```
$ touch lib/sample_spa_web/views/post_view.ex
```

```elixir:lib/sample_spa_web/views/post_view.ex
defmodule SampleSpaWeb.PostView do
    use SampleSpaWeb, :view
    alias SampleSpaWeb.PostView

    def render("index.json", %{posts: posts}) do
        %{data: render_many(posts, PostView, "post.json")}
    end

    def render("show.json", %{post: post}) do
        %{data: render_one(post, PostView, "post.json")}
    end

    def render("post.json", %{post: post}) do
        %{id: post.id,
          body: post.body}
    end

end
```

#### bbs.exにビジネスロジックの記述

`lib/sample_spa/bbs.ex`を編集して，dbにpostを登録する処理とdbから全件取得する処理を追加します．

はじめにaliasにpostを追加してきます．

```elixir:lib/sample_spa/bbs.ex
 alias SampleSpa.Bbs.Board
 # postを追加
 alias SampleSpa.Bbs.Post
```

次に処理を追加しましょう．

```elixir:lib/sample_spa/bbs.ex
  # 掲示板に投稿する
  def create_post(board_id, attrs \\ %{}) do
    board = get_board!(board_id)
    %Post{}
    |> Post.changeset(attrs)
    |> Ecto.Changeset.put_assoc(:board, board)
    |> Repo.insert()
  end

  # 掲示板内の投稿を全て取得する．
  def get_posts_by_board_id!(board_id) do
    from(p in Post,
      where: p.board_id == ^board_id
    )
    |> Repo.all()
  end
```


#### controllerに処理を追加

最後にcontrollerを編集します．

先ほどと同様にaliasにpostを追加してきます．

```elixir:lib/sample_spa_web/controllers/board_controller.ex
  alias SampleSpa.Bbs.Board
  # postを追加
  alias SampleSpa.Bbs.Post
```

次に，show関数で先ほどのbbs.exの`get_posts_by_board_id!`を呼び出すように編集します．

```elixir:lib/sample_spa_web/controllers/board_controller.ex
  # 掲示板の投稿を全件取得
  def show(conn, %{"id" => board_id}) do
    posts = Bbs.get_posts_by_board_id!(board_id)
    conn
    |> put_view(SampleSpaWeb.PostView)
    |> render("index.json", posts: posts)
  end

  # 掲示板に投稿する
  def create_post(conn, %{"id" => id, "post" => post_params}) do
    board = Bbs.get_board!(id)
    with {:ok, %Post{} = post} <- Bbs.create_post(board, post_params) do
      conn
      |> put_status(:created)
      |> put_view(SampleSpaWeb.PostView)
      |> render("show.json", post: post)
    end
  end
```




### APIの確認

では実際にサーバを立ち上げてリクエストを送ってきちんと処理できるか確認しましょう．

```
$ mix phx.server
```

#### boardの登録と取得

curlで掲示板の情報をjsonを送ってみましょう．

```
$ curl -XPOST  -H "Content-type: application/json" -d '{"board": { "title": "テスト", "description": "テスト用の掲示板です．" }}' http://localhost:4000/api/boards
{"data":{"description":"テスト用の掲示板です．","id":15,"title":"テスト"}}% 
```

次に全件取得してみましょう．

```
$ curl http://localhost:4000/api/boards
{"data":[{"description":"テスト用の掲示板です．","id":1,"title":"テスト"}]}%       
```

取得できました．これで登録できていることがわかります．

#### postの登録と取得

boardと同様にcurlで登録して，取得します．

```
$ curl -XPOST  -H "Content-type: application/json" -d '{"post": { "body": "テスト登録"}}' http://localhost:4000/api/boards/7
"data":{"body":"テスト登録","id":1}}%
$ curl http://localhost:4000/api/boards/1
{"data":[{"body":"テスト登録","id":1}]}%    
```



## SPA(Vue)の作成

ここからが本番ですね．Vueを使ってSPAを作成します．
Phoenix 1.4からはassets配下はwebpackなのでそちらを利用します．
assetsディレクトリに移動して以下のライブラリをインストールします．

- Vue ... jsフレームワーク
- Vue-loader ... vueを使うためのやつ
- vue-template-compiler ... vueを使うためのやつ
- Vuex ... 状態管理
- Vue-router ... SPAのルーティング制御
- axios ... APIと通信するHTTPクライアント
- Buefy ... cssフレームワーク/Vue専用

上3つはVueを使ってSPAを実現するための基本ライブラリだと思ってください．

```
$ cd assets
$ npm install vue vue-loader vue-template-compiler vuex vue-router axios buefy
```

vueファイルを読み込んでくれるように`webpack.config.js`を修正します．
初めのconstがたくさんあるとこに以下を追加します．

```js:assets/webpack.config.js
const path = require('path');
...省略...
// 追加
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = (env, options) => ({
  ...省略
  module: {
    rules: [
      ...省略
      //追加
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      ...省略
    ],
  },
  //追加
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      vue$: 'vue/dist/vue.esm.js',
    }
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
    new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
    //追加
    new VueLoaderPlugin()
  ]
});
```

次にassets配下にVueのプロジェクトを構築するディレクトを配置します．

```
$ mkdir js/src
$ cd js/src
$ mkdir components router store
$ touch main.js App.vue
$ tree .
.
├── App.vue //Vue単一コンポーネントファイル
├── components //コンポーネントを配置するディレクトリ
├── main.js //vueの設定を行う
├── router //vue-router用のディレクトリ
└── store //vuex用のディレクトリ
```

まずはvueを読み込めているか確認したいので，`App.vue`と`main.js`を編集します．
App.vueはただ`Hello, Vue.js!`と表示するだけです．

```vuejs:assets/js/src/App.vue
<template>
    <div>
      {{ text }}
    </div>
</template>

<script>
export default {
   name: 'app',
   props: {
       text: {
           type: String,
           default: 'Hello, Vue.js!'
       }
   }
}
</script>
```

```js:asstes/js/src/main.js
import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  template: '<App/>',
  components: { App }
})
```

> 「なんとなくわかった．じゃあどうやってこのApp.vueを呼び出すの？」

そうですよね，Vue側を呼び出せるようにVueとPhoenixの連携の設定をしてきます．

### VueとPhoenixの連携

VueとPhoenixをどうやって連携させるか．
答えは`lib/sample_spa_web/controllers/page_controller`と`templates/layout/app.html.eex`を用います．

まずpage_controllerへのルーティングに少し修正を加えます．編集前はコメントアウトしておきます．

```elixir:lib/sample_aps_web/route.ex
  scope "/", SampleSpaWeb do
    pipe_through :browser

    # get "/", PageController, :index
    get "/*path", PageController, :index
  end
```

これは`route.ex`の終わりに書きます．これによって任意のリクエストを`page_cntroller`で受け取ります．

`page_controller`のコードを確認してみましょう．

```elixir:lib/sample_spa_web/controllers/page_controller.ex
defmodule SampleSpaWeb.PageController do
  use SampleSpaWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html")
  end
end
```

page_controllerは，`lib/sample_spa_web/templates/page/index.html.eex`を呼び出しています．
そしてそのレイアウトを定義しているのが，`lib/sample_spa_web/templates/layout/app.html.eex`になります．

そこで`app.html.eex`を編集して，Vue側を呼び出すように編集します．
\<body\>タグ内を全て書き換えます．

```html:lib/sample_spa_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>SampleSpa · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
  </head>
  <%# 変更箇所 %>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>
```

さっきまで編集してた`js/src/main.js`を指定しないの？と言われそうですが，これには理由があります．
assets配下の`webpack.config.js`を見るとわかります．

```js:asstes/webpack.config.js
module.exports = (env, options) => ({
  ...省略...
  entry: {
      './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
  },
```
webpackのエントリポイントはデフォルトでは`js/app.js`になっています．
なのでwebpack側には`ja/app.js`を通してしかアクセスできません．
よって，`js/app.js`を編集して`js/src/main.js`をインポートします．

```js:asstes/js/app.js
// import "phoenix_html"
import "./src/main.js"
```

ここまでできたら，アプリを起動します．

```
$ mix phx.server
```

http://localhost:4000 にアクセスして以下のように見えていれば成功です．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/b85f9982-07b2-86dc-bbcc-339e3dfa267b.png)

これでPhoenixとvueの連携ができました👏

### Componentsの作成

ここからは完全にVueのお話です．
Vueでは**コンポーネントベース**に開発を進めて行きます．
事前にコンポーネントの分割ルールを作っておくことが重要となります．
今回は，Atomic Designを意識したコンポーネントの分割を考えます．

まずはディレクトリを作りましょう．

```
$ cd assets/js/src/components
$ mkdir atoms molecules organisms
```

cssフレームワークのBuefyを使うことも考慮して，以下のようなルールで今回はコンポーネントを作成していきます．

- atoms ... Buefyで提供されるデザインを細かくコンポーネント化したものを配置する．  
- molecules ... atomsのコンポーネントを組み合わせて作成する．(ただし状態管理を担うvuexとやりとりしてはいけない)
- organisms .... atoms, moleculesのコンポーネントを組み合わせて作成する．(ここでvuexとやりとりする)  

ここら辺は実際に作ってみてわかる部分でもあるので進めてみましょう．

まずはBuefyのインポートの設定をmain.jsに追記します．

```js:assets/js/src/main.js
import Buefy from 'buefy'
import 'buefy/dist/buefy.css'

Vue.use(Buefy)
```

ちなみにBuefyはBulmaをVue向けにしたフレームワークであり，Bulmaを包括しています．
なのでBulmaのデザインを全て使えるという利点があります．
(実はBulmaを使用したかったのですが，webpackでの設定が上手く行かなかったの代用しています．．．)

### atoms

BulmaのHPで紹介されてるcardをコンポーネントをに分けて作成してみましょう．

![スクリーンショット 2019-06-03 0.41.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/c7d67479-6ddf-66c4-5dfa-592d840916fb.png)

赤枠で囲っている部分をatomsのコンポーネントとして構築します．

全体を`TheCard.vue`，中身を`TheCardContent.vue`，下のボタンを`TheCardFooterItem.vue`として作成します．

```vue:assets/js/src/components/atoms/TheCard.vue
<template>
    <div class="card">
        <header class="card-header">
             <p class="card-header-title">
                {{ title }}
             </p>
        </header>

        <div class="card-content">
            <slot name="content" />
        </div>

         <footer class="card-footer">
            <slot name="footer"/>
         </footer>
    </div>
</template>

<script>
export default {
    name: 'TheCard',
    props: {
        title: {
            type: String,
            default: 'タイトル'
        }
    }
}
</script>

```
`TheCard.vue`ではcardコンポーネントの概形を定義する感じです．
`<slot/>`を使うことで他のコンポーネントを入れ子にすることができます．


```vue:assets/js/src/components/atoms/TheCardContent.vue
<template>
    <div class="content">
        {{ body }}
    </div>
</template>

<script>
export default {
    name: 'TheCardContent',
    props: {
        body: {
            type: String,
            default: 'コンテンツ'
        }
    }
}
</script>

```

`TheCardContent.vue`はテキストコンテンツを表示するだけです．

```vue:assets/js/src/components/atoms/TheCardFooterItem.vue
<template>
    <a @click="click" class="card-footer-item">
        {{ text }}
    </a>
</template>

<script>
export default {
    name: 'TheCardFooterItem',
    props: {
        text: {
            type: String,
            default: 'リンク'
        }
    },
    methods: {
        click() {
            this.$emit('click')
        }
    }
}
</script>
```

`TheCardFooterItem.vue`は`<a>`タグでリンクの働きをします．
atomsではクリックされたときの挙動の制御は行いません．
`this.$emit('click')`でコンポーネント内の処理を，他のコンポーネントに引き継ぐことができます．

### molecules

作成したatomsのコンポーネントを組み合わせて，掲示板の情報を表示する`TheBoard.vue`と名前のコンポーネントを作成します．


```vue:assets/js/src/components/moleculs/TheBoard.vue
<template>
    <the-card :title="board.title">
        <the-card-content
            slot="content"
            :body="board.discription"
        />
        <the-card-footer-item
            slot="footer"
            text="Opne"
            @click="click(board.id)"
         />
    </the-card>
</template>

<script>
import TheCard from '../atoms/TheCard'
import TheCardContent from '../atoms/TheCardContent'
import TheCardFooterItem from '../atoms/TheCardFooterItem'

export default {
    name: 'TheBoard',
    components: {
        TheCard,
        TheCardContent,
        TheCardFooterItem
    },
    props: {
        board: {
            type: Object,
            default() {
                return {
                    id: 1,
                    title: '掲示板',
                    discripition: 'サンプルです．'
                }
            }
        }
    },
    methods: {
        click(id) {
            this.$emit('click', id)
        }
    }
}
</script>
```

このようにコンポーネントベースでUIを組み立てて行きます．
organismsの話に移る前にVueでのデータの永続化を担当するvuexの設定を行います．

### vuex

最初に作ったPhoenixのAPIを叩いて受け取ったレスポンスをvuexを使って保持します．
なおAPIを叩くにはaxiosというライブラリを使用します．

まずはvuexとaxiosを使う準備をしましょう．

storeディレクトリ配下に`index.js`と`modules`ディレクトリを作成しましょう．さらに`modules`ディレクトリには`bbs.js`というファイルを配置しましょう．

```
$ cd assets/js/src/store
$ mkdir modules
$ touch index.js modules/bbs.js
```

```js:assets/js/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// modules
import bbs from './modules/bbs'

Vue.use(Vuex)

const store = new Vuex.Store({
    modules: {
        bbs
    }
})

export default store
```

`store/index.js`はvuexの設定を記述します．

```js:assets/js/src/store/modules/bbs.js
import Vue from 'vue'

export default {
    namespaced: true,
    state: {
    },
    getters: {
    },
    mutations: {
    },
    actions: {
    }
}
```

`moudles/bbs.js`はPhoenixのbbsコンテキストのモデルを保持するように設計します．
これから追記してきますが，bbs.jsには`state`，`getters`，`mutations`，`actions`の4つの部分から成ります．

それぞれの役割は下図がわかりやすいと思います．(図は[vuex公式](https://vuex.vuejs.org/ja/)より引用)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/e7abe9a5-a189-3679-174f-3f4022ec39d5.png)

`getters`は？となりそうですが，`getters`はVueコンポーネントからstateの値を参照するのに用います．図の**Render**に相当します．

では，`modules/bbs.js`で掲示板のデータを取得できるように編集していきましょう．

```js:assets/js/src/store/modules/bbs.js
import Vue from 'vue'

export default {
    namespaced: true,
    state: {
        boards: []
    },
    getters: {
        boards: state => state.boards
    },
    mutations: {
        setUser(state, { boards }) {
            state.boards = boards
        }
    },
    actions: {
        async getBoards({ commit }) {
            await Vue.axios.get('/api/boards')
            .then(res => {
                commit('setBoards', {
                boards: res.data
                })
            })
            .catch(err => {
                throw err.response.status
            })
        }
     }
}
```

重要となるのはactionsで部分でしょうか．axiosを使ってAPIを叩きます．
`async await`はは非同期処理です．`.then`からでAPIサーバからのレスポンスを受け取りmutationsを呼び出します．
`catch`は例外を処理します．つまりAPIサーバとの通信で何かしら失敗した場合の処理です．

 > 「vuexの使い方のパターンはわかった．じゃあどうやって使うんだ？」

そうですよね，次はvuexとやりとりを実際に行うorganismsのコンポーネントを作成していきましょう．

### organisms

organismsではatoms, moleculesのコンポーネントを組み合わせて作成します．
今回はvuexで取得したboardsを全て描画する`TheBoardList.vue`コンポーネントを作成します．

一応，作成の指針としては，コンポーネントを読み込んだタイミングでAPIにアクセスし，その結果をimportしたmolculetsのコンポーネントに流し込みます．

```vue:assets/js/src/components/organisms/TheBoardList.vue
<template>
    <div >
        <the-board
            v-for="board in boards"
            :key="board.id"
            :board="board"
        />
    </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

import TheBoard from '../molecules/TheBoard'

export default {
    name: 'TheBoardlist',
    components: {
        TheBoard
    },
    computed: {
        ...mapGetters({
            boards: 'bbs/boards'
        })
    },
    methods: {
        ...mapActions({
            getBoards: 'bbs/getBoards'
        })
    },
    created() {
        this.getBoards().catch(() => {
            console.error('Vuex Error')
        })
    }
}
</script>
```

vuexとのやりとりはヘルパー関数`mapGetters`，`mapActions`を用います．

- mapGetters ... storeのgettersを呼び出す．computedに記述する．
- mapActions ... storeのactionsを呼び出す．methodsに記述する．

このヘルパー関数を用いることでgettersやactionsに別名をつけられる上に，methodsはコンポーネント内で`this`を使って呼び出すことができます．

`created()`はコンポーネントが呼び出されてはじめに呼び出されるメソッドです．ここでactionsを呼び出しvuex側でAPIと通信が行われます．

まあ論より証拠ってことで動かすのが早いですよね😅
このコンポーネントをApp.vueにimportして表示してみましょう．

```vue:App.vue
<template>
    <div>
      {{ text }}
      <the-board-list/>
    </div>
</template>

<script>
import TheBoardList from './components/organisms/TheBoardList'

export default {
   name: 'app',
   components: {
       TheBoardList
   },
   props: {
       text: {
           type: String,
           default: 'Hello, Vue.js!'
       }
   }
}
</script>
```

この状態で実行してみます．

```
$ mix phx.server
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/5de1b057-e3a6-427f-cafc-65e702e78534.png)

あら，真っ白でエラー吐かれちゃいました．

どうやらbabelでasync awaitを使用するには設定を加える必要があるようです．


#### babelでasync/awaitを使えるように修正

以下の記事を参考に修正を加えます．
[babelでasyncを使おうとしたらregeneratorRuntime is not definedが出た](http://mzqvis6akmakplpmcjx3.hatenablog.com/entry/2018/02/07/161051)


```:assets/.babelrc
{
    "presets": [
        [
          "@babel/preset-env", {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
}
```

![スクリーンショット 2019-06-03 22.28.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/36b97a23-9b10-a313-82c1-40322d05e816.png)

問題なく起動できました．データは適当に加えてある状態になってます．
ちなみにchromeに[Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=ja)を入れておくと開発者は幸せになれます．入れておきましょう．

### Vue-routerの導入

レイアウトを整えつつ，Vue-routerを導入します．
Vue-routerは簡単に言ってしまえば，ルーティングによって描画するコンポーネントを書き換えるものです．

まずはVue-routerを使う準備をしましょう．`assets/js/src/router`ディレクトリ配下にindex.jsを置きます．

```
$ touch assets/js/src/router/index.js
```

```js:assets/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

// components
import TheBoardList from '../components/organisms/TheBoardList'


Vue.use(VueRouter)

const router = new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '/',
            component: TheBoardList,
        }
    ]
})

export default router
```

ここで設定しているのは，パスとそれに対応するコンポーネントです．
ここでは`/`に`TheBoardList`を割り当てています．

次に`assets/js/src/main.js`にこのrouterの設定をインポートします．

```js:/assets/js/src/main.js
// importにrouterを追加
import router from './router'
...省略
new Vue({
  el: '#app',
  store,
  router, // 追加
  template: '<App/>',
  components: { App }
})
```

これでVue-routerを使う準備ができました．
では`asstes/js/src/App.vue`にVue-routerを組み込んでみましょう．ついでにレイアウトも少し整えて置きます．

```vue:asstes/js/src/App.vue
<template>
    <div class="hero">
        <div class="hero-body">
            {{ text }}
            <router-view/>
        </div>
    </div>
</template>

<script>
export default {
   name: 'app',
   props: {
       text: {
           type: String,
           default: 'Hello, Vue.js!'
       }
   }
}
</script>
```

`<router-view/>`の部分で描画されるコンポーネントがパスに応じて変化します．

実際に起動してみましょう．

```
$ mix phx.server
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225901/4bf3ce6a-336f-6ad6-6575-b2284d359b30.png)

上手くいってますね！
無事，Vue-routerが導入できました．

あとやらなければならないこととしては，

- 投稿を全て描画するコンポーネントの作成
- 掲示板を新規作成するFormを含むコンポーネントの作成
- 掲示板に投稿するFormを含むコンポーネントの作成

が残ります．

これらは後日，追記していきます🙇‍♂️

## まとめ

この記事の扱ったトピックのまとめです．

- Phoenixでjsonを返すAPIの作成
    - リレーションシップを持ったモデル構造
    - phooenixのルーティング制御でVueのプロジェクトに繋ぐ
- Vue.jsを使ったSPAの作成
    - コンポーネントベースのSPAの設計
    - axiosによるAPIアクセスとvuexによる状態管理
    - Vue-routerによるSPAのルーティング


## おわりに

最初にも述べましたが，Erlang & Elixir Fest 2019に参加して熱量が上がったので，とにかく冷めないうちに記事を書こうと思って書きました．
全て完成してから，記事を出そうと思っていたのですが先に公開して更新していくことにしました．
未完成の部分はこれから更新していきます，申し訳ありません🙇‍♂️