7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ExcelからElixir入門⑥:SPAからPhoenix製APIを呼び出す(表示編)【React版】

Last updated at Posted at 2023-01-08

【本コラムは、5分で読めて、15分くらいでお試しいただけます】
piacereです、ご覧いただいてありがとございます :bow:

前回までは、シンプルなPhoenixのサーバサイドレンダリング(SSR)により、Web上にDBデータや外部APIデータを表示してきました

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

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

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

Vue.js版
https://qiita.com/piacerex/items/50d847170291c41fef64

■「ExcelからElixir入門」シリーズの目次
①データ並替え/絞り込み
|> ②データ列抽出、Web表示
|> ③WebにDBデータ表示
|> ④Webに外部APIデータ表示
|> ⑤Webにグラフ表示
|> ⑥SPAからPhoenix製APIを呼び出す(表示編)【React版】
|> ⑦SPAからPhoenix製APIを呼び出す(更新編)【React版】
|> ⑧Gigalixirに本番リリース
|> ⑨「LiveView」ElixirサーバサイドのみでReact的SPA/リアルタイムUIが作れる
|> ⑩ElixirサーバサイドSPAをスマホで見るためにGigalixirリリース
|> ⑪Gigalixir上のLiveViewアプリに独自ドメイン名を付与して正式なアプリ公開
|> ⑫Elixir/PhoenixのCRUD Webアプリをリリース

:ocean::ocean::ocean: Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位! :ocean::ocean::ocean:

例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ! :qiita: :tada: :confetti_ball:

https://qiita.com/advent-calendar/2022/elixir
https://qiita.com/advent-calendar/2022/ranking/feedbacks
https://qiita.com/advent-calendar/2022/ranking/feedbacks/categories/programming_languages
image.png

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

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

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

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

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を起動します

mix deps.get
iex -S mix phx.server

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

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

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

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
end

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

lib/vue_sample_web/router.ex
defmodule BasicWeb.Router do
  use BasicWeb, :router

+ scope "/members", BasicWeb do
+   pipe_through :api
+ 
+   resources "/", MemberController
+ end
  

Phoenixを起動してください

iex -S mix phx.server

http://localhost:4000/members にブラウザでアクセスすると、下記のように返ってくれば成功です
image.png

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

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

Chrome向け「Postman」

Firefox向け「RESTClient」

VScode向け「REST Client」

ここではPostmanで説明しますが、下記のように「Headers」に Content-Typeapplication/json で設定してから、URLを設定し、「Send」ボタンをクリックしてください
image.png

すると、下記のように返ってきます
image.png

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

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

Reactとaxiosを使って、先ほど作ったAPIを呼ぶSPA(Single Page Application)を作ってみましょう

Reactとaxiosは、<script src=~> でロードします(「CDN(Content Delivery Network)」という形式です)

App 関数が、Reactのメイン処理です

useEffect ハンドラから呼び出される get で初期表示時のaxiosによるAPI呼出を行い、React.useReducer で定義された membersdispatch 関数経由でAPIで取得したデータを格納します

それを members.map で1件ずつ取り出し、各フィールドを {n.【フィールド名】} で取り出して表示します

なお、useEffect ハンドラの第2引数に [] を指定すると、ロード時に1度のみの実行となります(Vue.jsの mounted と同じような挙動)

lib/basic_web/templates/page/index.html.heex
<script src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@mui/material/umd/material-ui.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> 

<script type="text/babel">
const App = () => {
  const reducer = (members, action) => {
    return action
  }
  const [members, dispatch] = React.useReducer(reducer, [])
  React.useEffect(() => {get()}, [])
  const get = async () => {
    await axios.get('/members').then(response => dispatch(response.data))
  }
  return (
    <div>
    <h1>Members</h1>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>team</th>
        <th>position</th>
      </tr>
      {members.map((n) => (
      <tr>
        <td>{n.id}</td>
        <td>{n.name}</td>
        <td>{n.age}</td>
        <td>{n.team}</td>
        <td>{n.position}</td>
      </tr>
      ))}
    </table>
    </div>
  )
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>

<div id="root"></div>

なお、useStateで書くと以下の通りです

lib/basic_web/templates/page/index.html.heex
<script src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@mui/material/umd/material-ui.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> 

<script type="text/babel">
const App = () => {
  const [members, setMembers] = React.useState([])
  React.useEffect(() => {get()}, [])
  const get = async () => {
    await axios.get('/members').then(response => setMembers(response.data))
  }
  return (
    <div>
    <h1>Members</h1>
    <table>
      <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>team</th>
        <th>position</th>
      </tr>
      {members.map((n) => (
      <tr>
        <td>{n.id}</td>
        <td>{n.name}</td>
        <td>{n.age}</td>
        <td>{n.team}</td>
        <td>{n.position}</td>
      </tr>
      ))}
    </table>
    </div>
  )
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>

<div id="root"></div>

APIで取得したデータがテーブル表示されます
image.png

見た目が、SSR版と変わらないので、ちゃんとSPAとして動いているか分からないかも知れませんが、ブラウザの「開発者モード」の「Network」で確認すると、APIが呼ばれていることが分かります
image.png

Phoenixを起動したコンソールでも下記の通り、API呼出である MemberController.index の実行が、ページ呼出である PageController.index の実行の後に行われていることが確認できます
image.png

データ追加APIを実装する

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

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

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してもらい、大幅戦力増強しましょうw

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

入力後は、こんな感じになります
image.png

「Send」ボタンをクリックすると、「Status」に 201 Created が返ってきます
image.png

データ一覧APIを叩くと、藤井名人が追加されているのが確認できます
image.png

ブラウザをリロードすると、藤井名人がチームにjoinしたことが確認できました :wink:
image.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) ※最新版のインストール手順はコチラ
      • Phoenix 1.6.15
    • Docker/Debian 11.6+Elixir 1.14.2 (Erlang/OTP 25)
      • Phoenix 1.6.15

終わり

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

次回は、画面入力と、APIによるデータ更新 を行ってみます

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?