【本コラムは、5分で読めて、15分くらいでお試しいただけます】
piacereです、ご覧いただいてありがとございます
前回までは、シンプルな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アプリをリリース
Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位!
例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ!
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
PhoenixでScaffoldを使わずにデータ一覧APIを作る
通常、PhoenixでAPIを作る場合、mix phx.gen.json
によるDB CRUD付きREST API、いわゆる「Scaffold」で生成するのが一般的ですが、別に使わなくてもPhoenixでのAPI作成はカンタンですので、早速作ってみましょう
まず 「Sqlex」をインストールすることで、DbMnesia
/Db
モジュールを導入します(このシリーズの第3回まではこの2つの実装も扱っていましたが、入門編を超えた範囲になるのでOSS化しました)
mix.exsの def deps do
配下の :phoenix
の直上に追記します
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
というファイルを追加し、下記のように index
で members
テーブルの一覧を返すようにします
Phoenixは、マップやリスト、リストマップを json
に渡すと、自動的にJSON化してくれるのです
前回までで、DB(Mnesia)を使うPhoenix PJを作りましたが、そこに下記ファイルを追加しましょう
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", ~
ブロックを追加します
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
にブラウザでアクセスすると、下記のように返ってくれば成功です
データ一覧APIをREST APIクライアントで直接叩いて確認
REST APIの確認には、「REST APIクライアント」が便利ですが、REST APIクライアントは下記が代表的です
Chrome向け「Postman」
Firefox向け「RESTClient」
VScode向け「REST Client」
ここではPostmanで説明しますが、下記のように「Headers」に Content-Type
を application/json
で設定してから、URLを設定し、「Send」ボタンをクリックしてください
このように、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
で定義された members
に dispatch
関数経由でAPIで取得したデータを格納します
それを members.map
で1件ずつ取り出し、各フィールドを {n.【フィールド名】}
で取り出して表示します
なお、useEffect
ハンドラの第2引数に []
を指定すると、ロード時に1度のみの実行となります(Vue.jsの mounted
と同じような挙動)
<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で書くと以下の通りです
<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>
見た目が、SSR版と変わらないので、ちゃんとSPAとして動いているか分からないかも知れませんが、ブラウザの「開発者モード」の「Network」で確認すると、APIが呼ばれていることが分かります
Phoenixを起動したコンソールでも下記の通り、API呼出である MemberController.index
の実行が、ページ呼出である PageController.index
の実行の後に行われていることが確認できます
データ追加APIを実装する
APIによるデータ参照はできたので、次は、データ追加APIを実装してみます
MemberController
モジュールに create
を追加し、members
テーブルへのinsert文を Db.query
に渡し、その後、ステータスコードとして 201 Created
を返却します
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": "竜王"
}
「Send」ボタンをクリックすると、「Status」に 201 Created
が返ってきます
データ一覧APIを叩くと、藤井名人が追加されているのが確認できます
ブラウザをリロードすると、藤井名人がチームにjoinしたことが確認できました
【参考】本コラムの検証環境
本コラムは、以下環境で検証しています(恐らく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
- 実機+Elixir 1.14.2 (Erlang/OTP 25)
終わり
今回は、Reactによるフロントサイドと、Phoenixによる内部APIを組み合わせを用いて、APIで取得したデータをWeb表示してみました
次回は、画面入力と、APIによるデータ更新 を行ってみます