Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
66
Help us understand the problem. What is going on with this article?
@piacerex

LiveViewでSPA開発①: Elixirのみでフロントのリアルタイム入力/反映するSPAを実現(APIとJavaScriptは書いていない)

Elixir Digitalization Implementors/fukuoka.ex/kokura.ex の piacere です
ご覧いただいて、ありがとうございます :bow:

2019/3/15(金)、昨年からずっと待ち続けてたPhoenix LiveViewが正式リリースされたので、早速、試してみました

通常のPhoenix PJを、LiveView化した後、「Excelから関数型言語マスター」4回目で作ったQiita検索UIに、リアルタイム検索を追加したSPAとして構築し直す手順を解説します

なお、「Phoenix」は、ElixirのWebフレームワークです

内容が、面白かったり、役に立ったら、「LGTM」よろしくお願いします :wink:

:ocean::ocean::ocean: Advent Calendar、fukuoka.ex1位、Elixir2位達成ヽ(=´▽`=)ノ :ocean::ocean::ocean:

fukuoka.ex Advent Calendar、Webテクノロジーカテゴリで堂々1位 … 各コラムぜひお読みください
https://qiita.com/advent-calendar/2020/fukuokaex
image.png

そして、プログラミング言語カテゴリは、1位がRust、2位がElixir、3位がGoとモダン言語揃い踏みでのトップ3、熱いネー:laughing:
https://qiita.com/advent-calendar/2020/elixir
image.png

本コラムの検証環境、事前構築のコマンド

本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)

LiveViewは、こんなリアルタイム検索アプリが開発できます

今回のセッティングは、LiveViewが未だリリースされたばかりのためか、割と手順が多く、途中で心が折れてしまうかも知れないため、完成したときに、何ができるかを先に動画で紹介しておきます
image.png

サーバサイドで書かれたコードにも関わらず、反応速度がスピーディで、もたつく感じは一切ありません(Vue.js等で書かれたフロントと同じフィーリングです)

LiveViewを試すためのPhoenix PJの作成

LiveViewを動かすためのPhoenix PJを作成します

mix phx.new lv_sample --no-ecto
 …(ファイル作成ログが続く)…
Fetch and install dependencies? [Yn] (←n、Enterを入力)
cd lv_sample

LiveView(とSmallex)のインストール

ライブラリとして、LiveView(phoenix_live_view)を追加します

あと、LiveViewのサンプルとして、外部API呼出をするので、smallexも追加します

mix.exs
defmodule LvSample.MixProject do

  defp deps do
    [
      { :phoenix_live_view, github: "phoenixframework/phoenix_live_view" },  # <- add here
      { :smallex, "~> 0.0" },  # <- add here
      {:phoenix, "~> 1.4.2"},
      {:phoenix_pubsub, "~> 1.1"},
      

ライブラリをインストールします

mix deps.get

LiveView環境の設定

①セッション用saltの設定

LiveViewが利用するセッションをハッシュするためのsaltを設定します

まず、32バイトのsaltを生成します

mix phx.gen.secret 32
Gduxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

上記で生成した32バイトsaltを、下記の通り、LiveView向けに設定します

config/config.exs
# This file is responsible for configuring your application

config :lv_sample, LvSampleWeb.Endpoint,
  live_view: [ signing_salt: "Gduxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ], 
  # ^--- add here
  url: [host: "localhost"],
  secret_key_base: "AcPkxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  

②ビューとルータのマクロ設定

ビューとルータのマクロに、LiveView用の設定を追加します

【2020/03/09追加】
LiveView 0.5以降では、「def controller do」配下への追加が増え、「def view do」の追加内容が変わりました

lib/lv_sample_web.ex
defmodule LvSampleWeb do

  def controller do
    quote do
      use Phoenix.Controller, namespace: LvSampleWeb

      import Plug.Conn
      import LvSampleWeb.Gettext
      alias  LvSampleWeb.Router.Helpers, as: Routes
      import Phoenix.LiveView.Controller  # <- add here
    end
  end

  def view do
    quote do
    
      import LvSampleWeb.ErrorHelpers
      import LvSampleWeb.Gettext
      alias  LvSampleWeb.Router.Helpers, as: Routes
      import Phoenix.LiveView.Helpers  # <- add here
    end
  end

  def router do
    quote do
      use Phoenix.Router
      import Plug.Conn
      import Phoenix.Controller
      import Phoenix.LiveView.Router  # <- add here
    end
  end
  

③LiveViewファイルのための設定

LiveViewファイルを置くためのフォルダを作成します

mkdir lib/lv_sample_web/live

LiveViewファイルが更新された際のLiveReloadを2箇所、設定します

config/dev.exs
use Mix.Config

config :lv_sample, LvSampleWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{lib/lv_sample_web/live/.*(ex)$},  # <- add here
      ~r{lib/lv_sample_web/templates/.*(leex)$},  # <- add here
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      

④LiveView用エンドポイントの追加

LiveView用のエンドポイントとして、「/live」を追加します

【2020/03/09追加】
LiveView 0.5以降では、Plug.Session情報をエンドポイントに設定することができます(下記ではコメントアウトしています)

lib/lv_sample_web/endpoint.ex
defmodule LvSampleWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :lv_sample

// v- add here
#  @session_options [
#    store: :cookie,
#    key: "_lv_sample_key",
#    signing_salt: "xxxxxxxxx"
#  ]
#  plug Plug.Session, @session_options
#  socket "/live", Phoenix.LiveView.Socket, websocket: [ connect_info: [ session: @session_options ] ]
  socket "/live", Phoenix.LiveView.Socket, websocket: true
// ^- add here
    

⑤JavaScript側のLiveViewソケット接続設定の追加

app.jsに、「phoenix_live_view」をインポートし、「/live」にソケット接続するための設定を追加します

【2020/03/09追加】
LiveView 0.5以降では、CSRFトークンをLiveSocketのパラメータとして指定することができます(下記ではコメントアウトしています)

assets/js/app.js
// We need to import the CSS so that webpack will load it.

// v- add here
import { Socket } from "phoenix"
import LiveSocket from "phoenix_live_view"

let liveSocket = new LiveSocket( "/live" )
//let csrfToken = document.querySelector( "meta[name='csrf-token']" ).getAttribute( "content" )
//let liveSocket = new LiveSocket( "/live", Socket, { params: { _csrf_token: csrfToken } } )
liveSocket.connect()
// ^- add here
lib/lv_sample_web/templates/layout/app.html.eex
<head>
  <%= csrf_meta_tag() %>

⑥Nodeモジュールのインストール

package.jsonの「dependencies」に、「phoenix_live_view」のエントリー(下記dependenciesの直下1行)を追加します

assets/package.json
{
  "dependencies": {
    "phoenix_live_view": "file:../deps/phoenix_live_view",
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html"
  },
  

assets配下で、Nodeモジュールのインストールを行います

【2020/03/09追加】
LiveView用WebSocketが開かないことがあるため、Phoenix関連モジュールの強制アップデートをします

cd assets
npm install --force phoenix_live_view phoenix_html phoenix
cd ..

LiveViewサンプルの作成

さぁ、LiveView環境が構築できたので、LiveViewの機能を使って、リアルタイムにQiitaを検索するSPAを作ってみます

検索用の入力フィールドをフロントUIに持ち、LiveViewの機能でサーバ側に自動連携させ、入力が変わるたびにサーバからQiitaを検索し、その検索結果をフロントに反映させます

そのコードは、以下の通りです

【2020/03/09追加】
LiveView 0.6以降では、mountの引数が、2つから3つに変更(第1引数paramsが追加)されました

lib/lv_sample_web/live/qiita_search_realtime.ex
defmodule LvSampleWeb.QiitaSearchRealtime do
    use Phoenix.LiveView

    def render( assigns ) do
        ~L"""
            <p>
            <%= if @message do %><%= @message %><% end %>
            </p>

            <form phx-change="change">
            <input type="text" name="query" value="<%= @query %>" placeholder="empty" />
            Query: <%= @query %><br>
            </form>

            <table>
            <tr>
                <th>ID</th>
                <th>タイトル</th>
                <th>作成日</th>
            </tr>
            <%= for result <- @results do %>
            <tr>
                <td><%= result[ "id" ] %></td>
                <td><%= result[ "title" ] %></td>
                <td><%= result[ "created_at" ] %></td>
            </tr>
            <% end %>
            </table>
        """
    end

    def mount( _params, _session, socket ) do
        { :ok, assign( socket, query: "", message: "[Init]", results: [] ) }
    end

    def handle_event( "change", %{ "query" => query }, socket ) do
        send( self(), { :submit, query } )
        { :noreply, assign( socket, message: "[Searching...]", query: query ) }
    end

    def handle_info( { :submit, query }, socket ) do
        results = Json.get( "https://qiita.com", "/api/v2/items?query=#{ query }" )
        { :noreply, assign( socket, message: "[Complete!!]", results: results ) }
    end
end

①LiveViewサンプルにおける初期画面表示

LiveViewのコードは、大枠としては、画面初期時に、render()とmount()が呼ばれ、render()に配置されたパーツが、mount()でassignされた内容で画面が初期化されます

mount()は、Vue.jsで言うところの「mounted」に相当します

③LiveViewサンプルにおけるハンドラ

その後は、render()で配置されたパーツ毎に設定されたハンドラが動きます

たとえば、「phx-change」を使うことで、入力フィールドの変更があるたびに、handle_event()が呼び出されます

ここは、Vue.jsで言うところの「v-on:change」に相当する訳ですが、Vue.jsとの違いは、フロントサイドの関数を呼ぶのでは無く、サーバサイドの関数が呼ばれる点です

③LiveViewサンプルにおけるハンドラ内処理

handle_event()に渡される引数であるqueryは、フロントUIの入力フィールドに入力された値がサーバ側に渡されており、サーバサイドで処理が進められます

その中では、メッセージパッシングであるsend()でhandle_info()を呼び出し、その裏で、messageの更新を行っています

そして、サーバサイドで行われたmessageの更新は、フロントUI上のメッセージフィールドに反映されます

handle_info()では、Qiita APIを呼び出し、その結果をresultsに格納し、messageを更新します

LiveViewの最大の特徴:フロントとサーバの通信は記述不要

handle_event()/handle_info()共に、呼び出された時点で、フロントサイドの入力値や状態が、サーバサイドに渡され、以降の処理はサーバサイドで実行され、その結果をフロントサイドに反映できる…これを、サーバサイドのコードを書くだけで実現できるのが、LiveViewの最大の特徴です

これをVue.js等で開発した場合、サーバサイドにAPIを構築し、フロントサイドからAPIを呼び出す…といった手間がかかるところ、LiveViewは、通常のElixirコードをサーバサイドで書くだけで、API無にフロントサイドとサーバサイドのデータバインディングが可能です

機能規模のあるフロントを開発すると、APIがすぐに何十本、何百本と増え、管理が煩雑になるところを、LiveViewは、フロントとサーバの通信を明示的に書く必要が無く、あくまでサーバサイドのコードだけで表現できるため、非常にシンプルにまとまります

LiveView用plug、レイアウト外枠、ルーティングの追加

さて、SPAも書けたので、後はルーティングを設定したら、実行できるようになります

router.exの「pipeline :browser」中にある、「plug :fetch_flash」の直後に 「plug Phoenix.LiveView.Flash」 「:fetch_live_flash」を追加し、LiveView用のレイアウト外枠を設定し、「/realtime」でLiveView専用のルーティングを追加します

【2020/03/09追加】
LiveView 0.8以降では、「Phoenix.LiveView.Flash」の代わりに「:fetch_live_flash」の追加が必要になり、「plug :fetch_flash」はコメントアウトが必要です

次に、「:put_layout」 「:put_live_layout」「:put_root_layout」で、レイアウト外枠として「templates/layout/app.html.eex」を指定します(このレイアウト外枠から、/js/app.js中のLiveView用WebSocket接続が呼び出されます)

【2020/03/09追加】
LiveView 0.8以降では、「:put_layout」による指定が効かなくなったので、「:put_live_layout」でレイアウト外枠を指定する必要があります

【2020/04/09追加】
LiveView 0.11以降では、「:put_live_layout」による指定が効かなくなったので、「:put_root_layout」でレイアウト外枠を指定する必要があります

最後に、通常のコントローラだと、「resource」や「get」と、コントローラのモジュール名+関数を指定しますが、LiveViewでは、「live」と、LiveViewモジュール名を指定します

lib/lv_sample_web/router.ex
defmodule LvSampleWeb.Router do
  use LvSampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
#    plug :fetch_flash
    plug :fetch_live_flash  # <- add here
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :put_root_layout, { LvSampleWeb.LayoutView, :app }  # <- add here
  

  scope "/", LvSampleWeb do
    pipe_through :browser
    
    live "/realtime", QiitaSearchRealtime 
  end

Phoenix起動、LiveViewサンプル実行

iex -S mix phx.server

ブラウザで「http://localhost:4000/realtime」にアクセスすると、こんな感じのQiita検索SPA画面が表示されます
image.png

入力フィールドに文字を入れると、入力フィールド下部に入力した文字列がリアルタイム更新され、入力フィールド上部には「[searching...]」と検索中であるメッセージが表示され、裏では、Qiitaへの検索が走り始めます
image.png

少しすると、メッセージが「[complete!!]」に変わり、画面下部に検索結果が反映されます
image.png

実際に動いている動画もアップしたので、コチラをご覧いただくと、より実感が沸くかと思います

けっこう反応速度がスピーディで、もたつく感じは一切ありません(Vue.js等でフロントネイティブで書いているフィーリングと、ほぼ変わらないです)
image.png

終わり

今回は、LiveView環境を構築して、リアルタイムにQiita検索を行うSPAを作りました

サーバサイドのElixirコードを書くだけで、フロントUIのリアルタイム操作/反映が可能であり、SPAを始めとするWebアプリを作るための強力過ぎる土台だという感触が伝わったでしょうか?

リリース前から、かなりの可能性を感じていたLiveViewですが、実際に動かしてみて、Vue.js等とほぼ変わらない速度やフィーリングで、しかもフロントとサーバの書き分けが一切要らず、入力内容やデータ状態をAPI構築無かつサーバサイドで一元管理できる…なんだか魔法のような体験でした

次回は、「LiveViewにおけるsubmitボタンの制御」について解説します

p.s.「いいね」よろしくお願いします

ページ左上の image.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

66
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
piacerex
福岡でプログラマしながらIT商社とIT企業を経営してます。Elixir/Kerasをよく使う。Elixirコミュ#fukuokaex、福岡理学部#FukuokaScienceを主催。プログラマ歴36年/XPer歴19年/デジタルマーケッター/経営者/CTO/技術顧問数社。 シボと重力子放射線射出装置は別腹(^^)
fukuokaex
エンジニア/企業向けにElixirプロダクト開発・SI案件開発を支援する福岡のコミュニティ

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
66
Help us understand the problem. What is going on with this article?