(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の13日目、自然言語処理 Advent Calendar 2017 - Qiita の16日目です)

昨日は@zacky1972さんの「ZEAM開発ログv0.1.6 Elixir から Rustler で GPU を駆動しよう〜ElixirでAI/MLを高速化」でした!

本題

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

環境

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

$ 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

v1.2のブランチ→phoenix/lib/mix/tasks at v1.2 · phoenixframework/phoenix · GitHub

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

STEP1: PhoenixでJSON APIを作成する

json APIの構築はElixir入門「第3回:Phoenix 1.3で高速webアプリ & REST APIアプリをサクッと書いてみる」のスライドでばっちりなので、こちらのスライドに則ればOKです。

今回はphoenixが入っているこちらのdockerイメージを利用してみます。

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

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

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

# 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というビルドツールを採用しており、これを利用しない場合には—no-brunchをつけてプロジェクトを作成します。

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

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

# DBの作成
$ mix ecto.create

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

ecto.createで

** (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の項目を追加してみてください。

# 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

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

$ 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モデルを用意しましょう。

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

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

# 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
# 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にアクセスするだけです!

# ルーティングを確認
$ 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を叩くと空が返ってきて、

Postman 2018-05-31 21-46-16.png

POSTすると値が保存でき、

Postman 2018-05-31 21-43-45.png

再度GETすると保存したレコードが取れています。

Postman 2018-05-31 21-45-13.png

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

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

準備

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

スクリーンショット 2018-05-31 21.57.14.png

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

<!-- 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インスタンスを作成します。

// 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のどちらで行う?

(2) Vueインスタンスが持つデータを表示する

最後に(2)です。Vueインスタンスが持つデータをどう表示するかを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

まとめ

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

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

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

次回は、@kobatako さんの「定期的にSlack botで記事を通知する with Qiita API」です!お楽しみに!