Phoenix + Vue.js入門

（この記事は、[「fukuoka.ex(その2) Elixir Advent Calendar 2017」](https://adventar.org/calendars/2871)の13日目、[自然言語処理 Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/nlp) の16日目です)

昨日は@zacky1972さんの「[ZEAM開発ログv0.1.6 Elixir から Rustler で GPU を駆動しよう〜ElixirでAI/MLを高速化](https://qiita.com/zacky1972/items/c998cad40ff9f2a42fde)」でした！

## 本題
[Phoenix](http://phoenixframework.org/)はElixirで書かれたWebフレームワークです。今回はこのPhoenixをAPIサーバーとして利用し、さらにそのAPIから取得したレコードをVue.jsを利用して画面表示するところまでを解説したいと思います。

###  環境
Phoenix環境はDockerで用意するのがオススメです。

```bash
$ docker -v

Docker version 18.03.1-ce, build 9ee9f40
```

### ※ Phoenixのタスク名について
v1.3から、Phoenixのタスク名が `mix phoenix.*` から `mix phx.*` に変更されています。

最新のmaster→[phoenix/lib/mix/tasks at master · phoenixframework/phoenix · GitHub](https://github.com/phoenixframework/phoenix/tree/master/lib/mix/tasks)

v1.2のブランチ→[phoenix/lib/mix/tasks at v1.2 · phoenixframework/phoenix · GitHub](https://github.com/phoenixframework/phoenix/tree/v1.2/lib/mix/tasks)

**v1.3以降を利用する際は`mix phoenix.*`ではなく`mix phx.*`を利用するようにしましょう。**

## STEP1: PhoenixでJSON APIを作成する

json APIの構築は[Elixir入門「第3回：Phoenix 1.3で高速webアプリ ＆ REST APIアプリをサクッと書いてみる」](https://www.slideshare.net/piacere_ex/elixir3phoenix-13web-rest-api-81219622)のスライドでばっちりなので、こちらのスライドに則ればOKです。

今回はphoenixが入っている[こちらのdockerイメージ](https://hub.docker.com/r/marcelocg/phoenix/)を利用してみます。


```bash
# 現在開いているディレクトリをマウントしてコンテナを起動
# Phoenix serverで利用するポート4000をbindした状態で起動
$ docker run -it -v `pwd`:/code -p 4000:4000 marcelocg/phoenix
```

以下、コンテナ内での作業です。

先にpostgresqlを入れておきましょう。

```bash
# postgresqlのインストール
$ apt-get update
$ apt-get install postgresql

# 起動
$ service postgresql start

# 初期パスワードをPhoenixの初期設定に合わせて"postgres"に変更しておく
$ sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres'"
ALTER ROLE

# 再起動
$ service postgresql restart
```

続いて、本題のPhoenixプロジェクトを作成します。Phoenixフレームワークはデフォルトで[Brunch](http://brunch.io/)というビルドツールを採用しており、これを利用しない場合には--no-brunchをつけてプロジェクトを作成します。

```bash
# プロジェクトを作成
# --no-brunch: Brunch.ioを利用しない
$ mix phx.new phoenix_vue_example --no-brunch

# インストールが成功したら、フォルダへ移動
$ cd phoenix_vue_example

# DBの作成
$ mix ecto.create
```

[ecto](https://github.com/elixir-ecto/ecto)はElixirで書かれたデータベースのラッパーとクエリ操作を提供するモジュールです。`DB操作の記述やバリデーションの記述が楽になる便利なもの`ぐらいにとらえておきましょう。ectoはPhoenixに依存したものではないので、Phoenix環境下以外でも利用することができます。[Elixirで値のバリデーションをEctoで行う](https://qiita.com/niku/items/0bd63a496c9a685d7e33)の記事はその一例です。


ecto.createで

```bash
** (Mix) The database for PhoenixVueExample.Repo couldn't be created: ERROR 22023 (invalid_parameter_value): new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
```
のように怒られたら、`config/dev.exs`にtemplateの項目を追加してみてください。

```elixir
# Configure your database
config :phoenix_vue_example, PhoenixVueExample.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  password: "postgres",
  database: "phoenix_vue_example_dev",
  hostname: "localhost",
  template: "template0", # 追加
  pool_size: 10
```

データベースが作成できました！

```bash
$ sudo -u postgres psql -l
                                    List of databases
          Name           |  Owner   | Encoding  | Collate | Ctype |   Access privileges
-------------------------+----------+-----------+---------+-------+-----------------------
 phoenix_vue_example_dev | postgres | UTF8      | C       | C     |
 postgres                | postgres | SQL_ASCII | C       | C     |
 template0               | postgres | SQL_ASCII | C       | C     | =c/postgres          +
                         |          |           |         |       | postgres=CTc/postgres
 template1               | postgres | SQL_ASCII | C       | C     | =c/postgres          +
                         |          |           |         |       | postgres=CTc/postgres
(4 rows)
``` 


続けてAPIで提供するデータのモデルを作成します。今回は「タイトル」と「本文」をデータとして持つArticleモデルを用意しましょう。

```bash
# モデルを生成
# mix phx.gen.json <コンテキスト名> <スキーマのモジュール名(A)> <(A)の複数形> <フィールドと型>
$ mix phx.gen.json Blog Article articles title:string body:text
```

ファイルが生成されたら、ルーティングの追加をし、マイグレーションを実行しましょう。

```elixir
# lib/phoenix_vue_example_web/router.exを編集
defmodule PhoenixVueExampleWeb.Router do
  use PhoenixVueExampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    # plug :protect_from_forgery　← コメントアウト
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", PhoenixVueExampleWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/articles", ArticleController, except: [:new, :edit]　← 追加
  end

  # Other scopes may use custom stacks.
  # scope "/api", PhoenixVueExampleWeb do
  #   pipe_through :api
  # end
end
```

```bash
# migrateを実行
$ mix ecto.migrate

# テーブルが生成されたのを確認する
$ sudo -u postgres psql -d phoenix_vue_example_dev -c "\d articles"

phoenix_vue_example_dev=# \d articles
                                      Table "public.articles"
   Column    |            Type             |                       Modifiers
-------------+-----------------------------+-------------------------------------------------------
 id          | bigint                      | not null default nextval('articles_id_seq'::regclass)
 title       | character varying(255)      |
 body        | text                        |
 inserted_at | timestamp without time zone | not null
 updated_at  | timestamp without time zone | not null
Indexes:
    "articles_pkey" PRIMARY KEY, btree (id)
```

ココまでくれば、あとはサーバーを起動して所定のURLにアクセスするだけです！

```bash
# ルーティングを確認
$ mix phx.routes
   page_path  GET     /              PhoenixVueExampleWeb.PageController :index
article_path  GET     /articles      PhoenixVueExampleWeb.ArticleController :index
article_path  GET     /articles/:id  PhoenixVueExampleWeb.ArticleController :show
article_path  POST    /articles      PhoenixVueExampleWeb.ArticleController :create
article_path  PATCH   /articles/:id  PhoenixVueExampleWeb.ArticleController :update
              PUT     /articles/:id  PhoenixVueExampleWeb.ArticleController :update
article_path  DELETE  /articles/:id  PhoenixVueExampleWeb.ArticleController :delete

# サーバーを起動
$ iex -S mix phx.server
```

APIの準備ができたので、試しに`localhost:4000/articles`に対してPostmanからgetとpostを試してみましょう。

GETを叩くと空が返ってきて、
<img width="893" alt="Postman 2018-05-31 21-46-16.png" src="https://qiita-image-store.s3.amazonaws.com/0/99116/c138bb23-c11e-0388-924f-56565eb4f1a2.png">

POSTすると値が保存でき、
<img width="900" alt="Postman 2018-05-31 21-43-45.png" src="https://qiita-image-store.s3.amazonaws.com/0/99116/6e722b41-63c6-bcd6-dd66-74908bcf11f6.png">

再度GETすると保存したレコードが取れています。
<img width="900" alt="Postman 2018-05-31 21-45-13.png" src="https://qiita-image-store.s3.amazonaws.com/0/99116/8377d7b7-8f7d-7956-72c4-1d1af7499ca7.png">


これでAPIの準備は終わりです！

## STEP2: Vue.jsを導入しAPIのレスポンスを画面に表示する

### 準備
localhost:4000 にアクセスしたときに表示される↓↓の画面をいじりましょう。



<img width="802" alt="スクリーンショット 2018-05-31 21.57.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/99116/973ddb4c-9772-d13b-e410-6bf2967ca166.png">



事前準備としてvue.jsと、HTTPクライアントのaxiosをCDNで持ってきちゃいましょう。
app.html.eexのheadタグ内に以下を追記します。

```html:lib/phoenix_vue_example_web/templates/layout/app.html.eex
<!-- axios, vueをCDNから取得する -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
```

これでVue.jsが利用できる状態になりました。検証中にCDNでサクッと持ってこれるのはいいですね。

### Vue.jsで何を書くか
まず方針を整理します。ここからVue.jsで書いていく内容は以下の2つです。

(1) DOMがどんなデータを持つか（Model）
(2) DOMでどうデータを表示するか（View）

(1)は.jsにVueインスタンスを作成して定義します。(2)はhtmlにデータが入ってくる箇所を`{{ }}`で記述して定義します。**データ(Model）と見た目（View）をきれいに分離して記述する**のがVue.jsのポイントです。


###  (1) VueインスタンスでDOMが持つデータを定義する
まず(1)を書いていきましょう。body直前で呼ばれているapp.jsでvueインスタンスを作成します。

```javascript:priv/static/js/app.js
var app = new Vue({
    el: '[role="main"]',
    data: {
      articles: []
    },
    created: function() {
      axios.get('/articles').then(function(response){
        app.articles = response.data.data;
      });
    }
});
```
app.html.eex内のmainタグをvueインスタンスのROOT要素として登録しています。Vueインスタンスが作成された後に実行されるフック関数`created()`を利用して、インスタンスが作成されたタイミングでAPIを叩き、自身のdataにapiの返り値を格納しています。

似たフック関数に`mounted()`があり、こちらはDOMの描画が完了したタイミングで呼ばれます。APIを叩いてVueインスタンスにデータを格納する処理はDOMの構築を待たずに実行してOKなので、`created()`で書きましょう。

参考: [Vuejs APIアクセスはcreatedとmountedのどちらで行う？](https://qiita.com/kimullaa/items/2bf8948dffb8c52d2b6b)


### (2) Vueインスタンスが持つデータを表示する
最後に(2)です。Vueインスタンスが持つデータをどう表示するかをhtmlに書いてあげれば完成です。

```html:lib/phoenix_vue_example_web/templates/page/index.html.eex
<div v-for="article in articles">
  <h2>{{ article.title }}</h2>
  <pre>{{ article.body }}</pre>
  <hr>
</div>
```
  `v-for`は配列データをループさせるときに書くvueの記法です。ここでは割愛しますが、あるデータが真のときだけDOMを表示する`v-if`など、他にもたくさんの記法があります。

localhost:4000にアクセスすると、v-forでループしてDBのすべてのレコードが表示されているはずです！

![698c2ae3.png](https://qiita-image-store.s3.amazonaws.com/0/99116/51689ab6-83c8-dfd4-d973-e1a37a688a6c.png)

## まとめ
以下の流れで、Phoenix+Vueのシンプルなアプリケーションを作成しました。

① `mix phx.gen.json`でAPIを構築
② ブラウザから`axios`を利用してAPIを叩く
③ Vueインスタンスのdataに格納し、画面表示

Phoenixで「いかにデータを作成するか」、Vueで「いかにデータを表示するか」という関心の分離が出来るので、この構成は良いですね。さらに複雑な構成でどう書いていくか、今後学習して発信していけたらなと思います！

次回は、@kobatako さんの[「定期的にSlack botで記事を通知する with Qiita API」](https://qiita.com/kobatako/items/d7b404ba53f9a25ac5aa)です！お楽しみに！

---------------------------
:stars::stars::stars::stars::stars:<b>　満員御礼！Elixir MeetUpを6月末に開催します　</b>:stars::stars::stars::stars::stars:
<font color="red"><b>※応募多数により、増枠しました</b></font>
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します。特別ゲストも迎え、非常に濃い2時間になること間違いなしです！ご興味のある方はぜひご参加ください！



[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)