16
4

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 3 years have passed since last update.

fukuoka.ex Elixir/PhoenixAdvent Calendar 2020

Day 7

Phoenix LiveViewで作る web/mobile チャットアプリ 下準備編

Last updated at Posted at 2020-12-06

この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar 2020 7日目です。
前日は、@yoshitia さんの やがてelixirになるでした。

やること

Phoenix LiveViewで web、スマートフォンアプリ両方で動く認証機能付きチャットアプリケーションを作成します

全体の構成は
・LiveViewでroom CRUDとmessageの作成 <- 本記事の範囲
・ルーム内のユーザーログイン状況の表示
・メッセージの更新時にルーム内のユーザー全体のメッセージが更新される

スマートフォンアプリでは
アプリ側でsignup/signin後WebView経由でLiveViewページをログインした状態で開けるようにします

補足

Phoenixとは
https://hexdocs.pm/phoenix/overview.html

Phoenix は Elixir で書かれたウェブ開発フレームワークで、サーバーサイドの MVC (Model View Controller) パターンを実装しています。

Phoenix.LiveView
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html

LiveViewは、サーバーレンダリングされたHTMLを使用して、リッチでリアルタイムなユーザー体験を提供します。

「Elixirでフロントエンドのバリデーションとかアニメーションと書けちゃう。しかもリアルタイムに。」

これを使用することによって、Discordのようなユーザーのログイン状況、メッセージ新規作成時に他のユーザーのメッセージ一覧を更新などを容易に実装できます

またフロントエンドのデータ加工部分やイベント,viewをElixirで記述でき、テストもElixirで書くことができます
トップページにコードと実際に動いているイメージがあるのでそちらを見ていただければと思います
https://www.phoenixframework.org/

ReactやVueを置き換える!といった類ではありませんが、リアルタイム処理がとても楽にかけるというのが最大の利点だと思います

準備

下準備としてwebインターフェースとAPIでのsignup/signinは以下のページを参考に作成していただくか
Phoenix とExpoで作るスマホアプリ ①Phoenix セットアップ編 + phx_gen_auth
Phoenix とExpoで作るスマホアプリ ②JWT認証+CRUD編

こちらをcloneしてください

ではroomのLiveView CRUDとmessage作成、一覧を実装していきます

Rooms LiveView CRUD

genコマンドは mix phx.gen.liveです
データ構造はルーム名(name)、詳細(description)と外部キーのuser_idとします

 mix phx.gen.live Rooms Room rooms name:string description:string user_id:references:users
 mix ecto.migrate

そのままだとroom作成時にUserと関連付けられないので、
リレーションの設計とchangesetのcastにuser_idを追加

[edit]lib/chat/users/user.ex
defmodule Chat.Users.User do
  use Ecto.Schema
  import Ecto.Changeset

  @derive {Inspect, except: [:password]}
  schema "users" do
    field :email, :string
    field :password, :string, virtual: true
    field :hashed_password, :string
    field :confirmed_at, :naive_datetime

    has_many :rooms, Chat.Rooms.Room # <= ここ追加
    timestamps()
  end
...
end
#[edit] lib/chat/rooms/room
defmodule Chat.Rooms.Room do
  use Ecto.Schema
  import Ecto.Changeset

  schema "rooms" do
    field :description, :string
    field :name, :string
    field :user_id, :integer # <- ここ削除
    belongs_to :user, Chat.Users.User # <- ここ追加

    timestamps()
  end

  @doc false
  def changeset(room, attrs) do
    room
    |> cast(attrs, [:name, :description, :user_id]) # <- user_idを追加
    |> validate_required([:name])
  end
end

[edit]lib/chat_web/router.ex
defmodule ChatWeb.Router do
  use ChatWeb, :router
...
  scope "/", ChatWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings", UserSettingsController, :edit
    put "/users/settings/update_password", UserSettingsController, :update_password
    put "/users/settings/update_email", UserSettingsController, :update_email
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
    # 以下自動で追加
    live "/rooms", RoomLive.Index, :index
    live "/rooms/new", RoomLive.Index, :new
    live "/rooms/:id/edit", RoomLive.Index, :edit

    live "/rooms/:id", RoomLive.Show, :show
    live "/rooms/:id/show/edit", RoomLive.Show, :edit
  end
...
end

User情報をセッションから取得

room作成時にユーザーに関連付ける処理を追加します

[edit]lib/chat/users.ex
defmodule Chat.Users do
...
  def get_user_by_session_token(token) do
    {:ok, query} = UserToken.verify_session_token_query(token)
    Repo.one(query)
  end
...
end
[edit]lib/chat_web/live/room_live/form_component.html.leex
<h2><%= @title %></h2>

<%= f = form_for @changeset, "#",
  id: "room-form",
  phx_target: @myself,
  phx_change: "validate",
  phx_submit: "save" %>
....
  <%= hidden_input f, :user_id %> # <- ここ追加
  <%= submit "Save", phx_disable_with: "Saving..." %>
</form>

ログインしたユーザーの情報がトークンとしてsessionに保持されていますので、
先程実装したget_user_by_session_tokenでトークンをもとにユーザーを取得し、
マウント時にassignsにログインユーザーの情報を追加します

[edit]lib/chat_web/live/room_live/index.ex
defmodule ChatWeb.RoomLive.Index do
  use ChatWeb, :live_view

  alias Chat.Rooms
  alias Chat.Rooms.Room

  @impl true
  # 以下書き換え
  def mount(_params, session, socket) do
    user = Chat.Users.get_user_by_session_token(session["user_token"])
    {:ok, assign(socket |> assign(:user, user), :rooms, list_rooms())}
  end
...
  # 新規作成時にuser_idの情報を追加
  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Room")
    |> assign(:room, %Room{user_id: socket.assigns.user.id}) # <- user_id追加
  end
end

Message作成

messageを作成、リレーションの設定をします

mix phx.gen.schema Rooms.Message messages body:string user_id:references:users room_id:references:rooms
[new]lib/chat/rooms/room.ex
defmodule Chat.Rooms.Room do
  use Ecto.Schema
  import Ecto.Changeset

  schema "rooms" do
    field :description, :string
    field :name, :string
    belongs_to :user, Chat.Users.User
    has_many :messages, Chat.Rooms.Message # <- ここ追加

    timestamps()
  end

end

[new]lib/chat/rooms/message.ex
defmodule Chat.Rooms.Message do
  use Ecto.Schema
  import Ecto.Changeset

  schema "messages" do
    field :body, :string
    #user_id,room_idをbelongs_toに書き換え
    belongs_to :user, Chat.Users.User
    belongs_to :room, Chat.Rooms.Room

    timestamps()
  end

  @doc false
  def changeset(message, attrs) do
    # user_id, :room_idを受け取り、必須になるように変更
    message
    |> cast(attrs, [:body, :user_id, :room_id])
    |> validate_required([:body, :user_id, :room_id])
  end
end

rooms.exにメッセージ取得と作成機能を追加

[edit]lib/chat/rooms.ex
defmodule Chat.Rooms do
  alias Chat.Rooms.Message
...
  def get_room_with_messages!(id) do
    Room
    |> preload(messages: :user)
    |> Repo.get!(id)
  end

  def change_message(%Message{} = message, attrs \\ %{}) do
    Message.changeset(message, attrs)
  end

  def create_message(attrs \\ %{}) do
    %Message{}
    |> Message.changeset(attrs)
    |> Repo.insert()
  end
...
end

最後にチャットルームを作成します
マウント時にuser、現在のroomと関連するmessage、
messageのchangeset、メッセージ作成後のredirect先をassignします

[new]libs/chat_web/live/room_live/show.ex
defmodule ChatWeb.RoomLive.Show do
  use ChatWeb, :live_view
  alias Chat.Rooms
  alias Chat.Rooms.Message

  @impl true
  def mount(%{"id" => id}, session, socket) do
    user = Chat.Users.get_user_by_session_token(session["user_token"])
    room = Rooms.get_room_with_messages!(id)
    changeset = Rooms.change_message(%Message{user_id: user.id, room_id: room.id})
    return_to = Routes.room_show_path(socket, :show, room)
    {:ok,
      socket
      |> assign(:page_title, page_title(socket.assigns.live_action))
      |> assign(:room, room)
      |> assign(:messages, room.messages)
      |> assign(:return_to, return_to)
      |> assign(:changeset, changeset)
      |> assign(:user, user)
    }
  end

  @impl true
  def handle_event("save", %{"message" => message_params}, socket) do
    case Rooms.create_message(message_params) do
      {:ok, _message} ->
        {:noreply, socket
          |> put_flash(:info, "Message created successfully")
          |> push_redirect(to: socket.assigns.return_to)
        }
      {:error, %Ecto.Changeset{} = changeset } ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

  defp page_title(:show), do: "Show Room"
end

view

[new]lib/chat_web/live/room_live/show.html.eex
<ul>
  <li>
    <strong>Name:</strong>
    <%= @room.name %>
  </li>

  <li>
    <strong>Description:</strong>
    <%= @room.description %>
  </li>
  <table>
    <tbody id="messages">
      <%= for message <- @messages do %>
        <tr id="message-<%= message.id %>">
          <td><%= message.body %></td>
          <td><%= message.user.email %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
  <div>
    <%= f = form_for @changeset, "#",
      id: "board-form",
      phx_submit: "save" %>

      <%= textarea f, :body, class: "form-control" %>
      <%= error_tag f, :body %>

      <%= hidden_input f, :user_id %>
      <%= hidden_input f, :room_id %>

      <%= submit "Save", phx_disable_with: "Saving..." %>
    </form>
  </div>
</ul>

<span><%= live_redirect "Back", to: Routes.room_index_path(@socket, :index) %></span>

動作確認

以上で実装が完了しましたので動作確認をしましょう
Image from Gyazo

roomが作成でき、room内でmessageが作成できることが確認できました
本記事は以上になりますありがとうございました

8日目は、引き続きPhoenix LiveViewで作る web/mobile チャットアプリ リアルタイム処理編 をお送りいたします

次回はこのままだと、他のユーザーがメッセージを送信した際にページリロードをしないと新しいメッセージは見れないので、
LiveViewの得意なリアルタイム処理で、メッセージの更新・ユーザーのログイン状況の実装と
スマホアプリでのLiveViewアプリ連携を実装していきます

今回のコード
https://github.com/thehaigo/chat/tree/realtime_ready

参考にしたサイト

https://www.870labo.com/posts/create-chat-app-with-liveview-part4/
https://qiita.com/pojiro/items/dc8c9d97be82f91560bf
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
https://qiita.com/kotar0/items/a3a886fa53dc6e0ab854
https://www.phoenixframework.org/

16
4
1

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
16
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?