ExcelからElixir入門⑥：SPAからPhoenix製APIを呼び出す（表示編）【Vue.js版】

【本コラムは、3分で読めて、10分くらいでお試しいただけます】
[piacere](https://twitter.com/piacere_ex)です、ご覧いただいてありがとございます :bow:

[前回](https://qiita.com/piacerex/items/290b76b76d5ff8e019bf)までは、シンプルなPhoenixのサーバサイドレンダリング（SSR）により、Web上にDBデータや外部APIデータを表示してきました

今回からは、Vue.jsを使ったフロントサイドと、Phoenixによる内部APIを組み合わせを用いて、よりElixir／Phoenixらしさを実感できるWeb開発へとステージアップします

なお、本コラムと全く同じ内容のVue.js版とReact版もあります

**LiveView版**
https://qiita.com/piacerex/items/64e77b85bfb4c6832662

**React版**
https://qiita.com/piacerex/items/27052ac628ec394c7560

**■「ExcelからElixir入門」シリーズの目次**
[**①データ並替え／絞り込み**](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1)
|> [**②データ列抽出、Web表示**](https://qiita.com/piacere_ex/items/b7787580fce5f148242f)
|> [<b>③WebにDBデータ表示</b>](https://qiita.com/piacere_ex/items/a7558adc6856e3577dc6)
|> [<b>④Webに外部APIデータ表示</b>](https://qiita.com/piacere_ex/items/4c212615a4eb699dd109)
|> [<b>⑤Webにグラフ表示</b>](https://qiita.com/piacere_ex/items/290b76b76d5ff8e019bf)
|> [**⑥SPAからPhoenix製APIを呼び出す（表示編）【Vue.js版】**](https://qiita.com/piacere_ex/items/50d847170291c41fef64)
|> [**⑦SPAからPhoenix製APIを呼び出す（更新編）【Vue.js版】**](https://qiita.com/piacerex/items/7cd1162ce6d66a334a07)
|> [<b>⑧Gigalixirに本番リリース</b>](https://qiita.com/piacerex/items/1a9cbcc740ca3707eaec)
|> [**⑨「LiveView」ElixirサーバサイドのみでReact的SPA／リアルタイムUIが作れる**](https://qiita.com/piacerex/items/3f8ee18c9443d63955bf)
|> [**⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース**](https://qiita.com/piacerex/items/583174d72e6fc0cd289e)
|> [**⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開**](https://qiita.com/piacerex/items/b61348c282395167fd8c)
|> [**⑫Elixir／PhoenixのCRUD Webアプリをリリース**](https://qiita.com/piacerex/items/511f24888c7350639d0b)

<br>:stars::stars::stars:<b>　お知らせ：Elixir MeetUpを来週末10/26（金）に開催します（リモート視聴も）　</b>:stars::stars::stars:

<strike>[<b>「fukuoka.ex#16：蔵出し始めました！Elixir実践テクニック公開します① 」</b>](https://fukuokaex.connpass.com/event/105154/)を10/26（金）19時に開催します</strike>
<font color="red">開催終了しました</font>

今回のMeetUpから、「座談会＋モブプログラミング」という形で、fukuoka.exアドバイザーズ／キャストが、過去会で登壇したり、日頃研鑽している数々のElixir実践テクニックをダイレクトに伝えていきます

今回は、「Elixir＋Vue.jsで2倍速開発」と「ElixirでElixirコンパイラ開発」の2トラックにて、集まった皆さまと一緒にElixir実践テクニックをシェアしたいと思います

https://fukuokaex.connpass.com/event/105154
[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/98aabdbd-8cd7-426b-0062-05d5863ce3b8.png)](https://fukuokaex.connpass.com/event/105154)

一応、リモート視聴にも対応していますが、福岡近辺にお住まいの方であれば、遊びに来ていただいて、参加型コンテンツを120%楽しんでいただくのが絶対オススメです

# PhoenixでScaffoldを使わずにデータ一覧APIを作る

通常、PhoenixでAPIを作る場合、`mix phx.gen.json` によるDB CRUD付きREST API、いわゆる「Scaffold」で生成するのが一般的ですが、別に使わなくてもPhoenixでのAPI作成はカンタンですので、早速作ってみましょう

まず 「Sqlex」をインストールすることで、`DbMnesia`／`Db` モジュールを導入します（このシリーズの第3回まではこの2つの実装も扱っていましたが、入門編を超えた範囲になるのでOSS化しました）

https://github.com/piacerex/sqlex

mix.exsの `def deps do` 配下の `:phoenix` の直上に追記します

```diff_elixir:mix.exs
defmodule Basic.Mixfile do
  use Mix.Project
…
  defp deps do
    [
      {:req, "~> 0.3"}, 
+     {:sqlex, "~> 0.1.0"}, 
      {:phoenix, "~> 1.6.15"},
      …
    ]
  end
…
```

PhoenixをCtrl+C2回で止めて、ライブラリを取得（要ネット接続）し、Phoenixを起動します

```sh
mix deps.get
iex -S mix phx.server
```

次に、`controllers` フォルダ配下に `member_controller.ex` というファイルを追加し、下記のように `index` で `members` テーブルの一覧を返すようにします

Phoenixは、マップやリスト、リストマップを `json` に渡すと、自動的にJSON化してくれるのです

前回までで、DB（Mnesia）を使うPhoenix PJを作りましたが、そこに下記ファイルを追加しましょう

```diff_elixir:lib/basic_web/controllers/member_controller.ex
defmodule BasicDbWeb.MemberController do
  use BasicDbWeb, :controller

  def index(conn, _p) do
    conn
    |> json(
      "select * from members"
      |> Db.query
      |> Db.columns_rows 
      |> Enum.sort(fn current, next -> current["id"] < next["id"] end)
    )
  end
end
```

次に、ルーティングにAPI用エントリーとして、CRUD全部（GET／POST／PUT／DELETE）のエンドポイントを一括生成する `resources "/", MemberController` を含む `scope "/members", ～` ブロックを追加します

```diff_elixir:lib/vue_sample_web/router.ex
defmodule BasicWeb.Router do
  use BasicWeb, :router
…
+ scope "/members", BasicWeb do
+   pipe_through :api
+ 
+   resources "/", MemberController
+ end
  …
```

`http://localhost:4000/members` にブラウザでアクセスすると、下記のように返ってくれば成功です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7937903d-fe0a-c3bd-2c77-688123253470.png)

## データ一覧APIをREST APIクライアントで直接叩いて確認

REST APIの確認には、「REST APIクライアント」が便利ですが、REST APIクライアントは下記が代表的です

**Chrome向け「Postman」**

https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop

**Firefox向け「RESTClient」**

https://addons.mozilla.org/ja/firefox/addon/restclient/

**VScode向け「REST Client」**

https://marketplace.visualstudio.com/items?itemName=humao.rest-client

ここではPostmanで説明しますが、下記のように「Headers」に `Content-Type` を `application/json` で設定してから、URLを設定し、「Send」ボタンをクリックしてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/abc5f278-3e4f-d1ee-2886-ae1d2430fc24.png)

すると、下記のように返ってきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/28e62dad-84a2-25b6-152e-7c60017f2cf3.png)

このように、PhoenixでAPIを作成するのは、とてもカンタンです

# SPAからデータ一覧APIを呼んだ後、表示する

Vue.jsとaxiosを使って、先ほど作ったAPIを呼ぶSPA（Single Page Application）を作ってみましょう

Vue.jsとaxiosは、`<script src=～>` でロードします（「CDN（Content Delivery Network）」という形式です）

`import` で指定した `createApp` 関数が、Vue.jsのメイン処理です

Vue.jsで初期表示時に実行される `mounted` ハンドラでは、`get` を呼び出すことでaxiosによるAPI呼出を行い、`deta()` 部で定義された `members` 変数にAPIで取得したデータを格納します

それを `tr` タグに指定した `v-for` で1件ずつ取り出し、各フィールドを `{{n.【フィールド名】}}` で取り出して表示します

```html:lib/basic_web/templates/page/index.html.heex
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.global.js"></script>
<script type="importmap">
  {
    "imports": {
      "vue": "https://cdn.jsdelivr.net/npm/vue/dist/vue.esm-browser.prod.js"
    }
  }
</script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> 

<div id="app">

<h1>Members</h1>
<table>
  <tr>
    <th>id</th>
    <th>name</th>
    <th>age</th>
    <th>team</th>
    <th>position</th>
  </tr>
  <tr v-for="n in members">
    <td>{{n.id}}</td>
    <td>{{n.name}}</td>
    <td>{{n.age}}</td>
    <td>{{n.team}}</td>
    <td>{{n.position}}</td>
  </tr>
</table>

</div>

<script type="module">
import {createApp} from 'vue'
createApp
({
  data()
  {
    return {
      members: [], 
    }
  }, 
  mounted(){
    this.get()
  }, 
  methods:
  {
    get: function() {
      axios.get('/members').then(response => {this.members = response.data})
    },
  },
}).mount('#app')
</script>
```

なお、Vue.js 2系だと以下の通りです

`el` で指定したidと、HTML中の `div` タグが一致した `div` タグ内でVue.jsが有効です

```html:lib/basic_web/templates/page/index.html.heex
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.14/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.2.2/axios.min.js"></script>

<div id="app">

<h1>Members</h1>
<table>
  <tr>
    <th>id</th>
    <th>name</th>
    <th>age</th>
    <th>team</th>
    <th>position</th>
  </tr>
  <tr v-for="n in members">
    <td>{{n.id}}</td>
    <td>{{n.name}}</td>
    <td>{{n.age}}</td>
    <td>{{n.team}}</td>
    <td>{{n.position}}</td>
  </tr>
</table>

</div>

<script>
var app = new Vue
({
  el: '#app',
  data: 
  {
    members: [], 
  }, 
  mounted()
  {
    axios.get('/members')
    .then(response => {this.members = response.data})
  }, 
})
</script>
```

APIで取得したデータがテーブル表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6315519d-98d6-c6f5-556f-efea315c4328.png)

見た目が、SSR版と変わらないので、ちゃんとSPAとして動いているか分からないかも知れませんが、ブラウザの「開発者モード」の「Network」で確認すると、APIが呼ばれていることが分かります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/839a1033-7ac6-ac3c-0fad-e26852c5a8aa.png)

Phoenixを起動したコンソールでも下記の通り、API呼出である `MemberController.index` の実行が、ページ呼出である `PageController.index` の実行の後に行われていることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/08f10e87-0d8d-278f-ee38-2bab2312abb4.png)

# データ追加APIを実装する

APIによるデータ参照はできたので、次は、データ追加APIを実装してみます

`MemberController` モジュールに `create` を追加し、`members` テーブルへのinsert文を `Db.query` に渡し、その後、ステータスコードとして `201 Created` を返却します

```diff_elixir:lib/basic_web/controllers/member_controller.ex
defmodule BasicWeb.MemberController do
  use BasicWeb, :controller

  def index(conn, _p) do
    conn
    |> json(
      "select * from members"
      |> Db.query
      |> Db.columns_rows 
      |> Enum.sort(fn current, next -> current["id"] < next["id"] end)
    )
  end
+ def create(conn, p) do
+   "insert into members values('#{p["name"]}', #{p["age"]}, '#{p["team"]}', '#{p["position"]}')"
+   |> Db.query
+   send_resp(conn, :created, "")
+ end
end
```

## データ追加APIから直接データを追加し、Web上で確認

では、APIでデータ追加してみましょう

「GET」を「POST」に変更し、「Body」を「raw」で設定した後、下記内容をbodyに入力して、藤井名人をチームにjoinしてもらい、大幅戦力増強しましょうｗ

```json
{
  "name": "藤井 聡太", 
  "age": 20, 
  "team": "杉本昌隆八段門下", 
  "position": "竜王"
}
```

入力後は、こんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6b58bfce-70e8-d504-3a74-c6ae6ca775f4.png)

「Send」ボタンをクリックすると、「Status」に `201 Created` が返ってきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/200ba380-ece1-ae8e-1c67-82b1494052c2.png)

データ一覧APIを叩くと、藤井名人が追加されているのが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7471791e-f6e8-8ff6-d935-d9bcb002d372.png)

ブラウザをリロードすると、藤井名人がチームにjoinしたことが確認できました :wink:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/cbdd26eb-88d7-6b39-788d-6a32c441da43.png)

# 【参考】本コラムの検証環境

本コラムは、以下環境で検証しています（恐らくUbuntu実機やMacでも動きます）

- Windows 10
  - 実機＋Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15
  - WSL2／Ubuntu 20.04＋Elixir 1.14.2 (Erlang/OTP 25)　※[**最新版のインストール手順はコチラ**](https://qiita.com/piacerex/items/01e89435af0f7a454ad2)
    - Phoenix 1.6.15
  - Docker／Debian 11.6＋Elixir 1.14.2 (Erlang/OTP 25)
    - Phoenix 1.6.15

- Windows11
  - WSL2／Ubuntu 22.04＋Docker Compose＋Elixir 1.13.4 (Erlang/OTP 25)
    - Phoenix 1.6.15

# 終わり

今回は、Vue.jsによるフロントサイドと、Phoenixによる内部APIを組み合わせを用いて、APIで取得したデータをWeb表示してみました

[次回は、**画面入力と、APIによるデータ更新** を行ってみます](https://qiita.com/piacere_ex/items/7cd1162ce6d66a334a07)