LiveView Chat Tutorial 
Мы действительно хотели бесплатный пример с открытым исходным кодом с полным кодом, тестами и AUTH.
Мы написали это, чтобы мы могли указать людям в нашей команде/сообществе, Phoenix LiveView на это.
Этот пример LiveView /Учебное пособие перенесет вас из приложения с нулевым до полнофункционального приложения за 20 минут .
Вот содержимое таблицы того, что вы можете ожидать в этом примере/Учебном пособии:
LiveView Chat TutorialPhoenixlive каталог, контроллер LiveView и шаблонrouter.exmount/3mount/3handle_event/3handle_info/2AUTH_API_KEYauth_plugrouter.exAuthControlleron_mount/4 Любой, кто изучает Phoenix LiveView желающий автономного учебника, включая: Setup , Testing , Authentication , Presence ,
Рекомендуется , хотя и не требуется , чтобы вы следили за учебником по счетчику LiveView , поскольку этот более продвинутый. По крайней мере, проверьте список предпосылок, чтобы вы знали, что вам нужно, чтобы установить на своем компьютере, прежде чем начать это приключение!
При условии, что у вас установлены Elixir , Phoenix и Postgres , все готово!
Phoenix Начните с создания нового приложения liveview_chat Phoenix :
mix phx.new liveview_chat --no-mailer --no-dashboard Нам не нужны функции email или dashboard , поэтому мы исключаем их из нашего приложения. Вы можете узнать больше о создании новых приложений Phoenix, работая: mix help phx.new
Запустите mix deps.get , чтобы получить зависимости. Затем создайте базу данных liveview_chat_dev postgres , выполнив команду:
mix ecto.setupВы должны увидеть выход, аналогичный следующему:
The database for LiveviewChat.Repo has been created
14:20:19.71 [info] Migrations already upКак только эта команда преуспевает, теперь сможете запустить приложение, запустив команду:
mix phx.serverВы увидите вывод терминала, аналогичный следующему:
[info] Running LiveviewChatWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.29.tgz
[info] Access LiveviewChatWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes... Когда вы открываете URL: http://localhost:4000 в вашем веб -браузере, вы должны увидеть что -то похожее на:

live каталог, контроллер LiveView и шаблон Создайте папку lib/liveview_chat_web/live и контроллер на lib/liveview_chat_web/live/message_live.ex :
defmodule LiveviewChatWeb.MessageLive do
use LiveviewChatWeb , :live_view
def mount ( _params , _session , socket ) do
{ :ok , socket }
end
def render ( assigns ) do
LiveviewChatWeb.MessageView . render ( "messages.html" , assigns )
end
endПримечание : ни имя файла, ни код не имеют слова « контроллер » нигде. Надеюсь, это не сбивает с толку. Это «контроллер» в том смысле, что он контролирует то, что происходит в приложении.
Контроллер LiveView требует определения функций mount/3 и render/1 .
Чтобы контроллер был простым, mount/3 просто возвращает {:ok, socket} без каких -либо изменений. render/1 вызывает LiveviewChatWeb.MessageView.render/2 (включен в Phoenix ), который отображает template messages.html.heex , который мы определим ниже.
Создать файл lib/liveview_chat_web/views/message_view.ex :
defmodule LiveviewChatWeb.MessageView do
use LiveviewChatWeb , :view
end Это похоже на обычный view Phoenix ; Ничего особенного/интересного здесь.
Далее создайте каталог lib/liveview_chat_web/templates/message
lib/liveview_chat_web/templates/message/messages.html.heex файл и добавьте следующую строку HTML :
< h1 > LiveView Message Page </ h1 > Наконец, чтобы упростить макет корня , откройте файл lib/liveview_chat_web/templates/layout/root.html.heex и обновите содержимое <body> to:
< body >
< header >
< section class =" container " >
< h1 > LiveView Chat Example </ h1 >
</ section >
</ header >
< %= @inner_content % >
</ body > router.ex Теперь, когда вы создали необходимые файлы, откройте маршрутизатор lib/liveview_chat_web/router.ex замените контроллер PageController маршрута по умолчанию:
get "/" , PageController , :index с контроллером MessageLive :
scope "/" , LiveviewChatWeb do
pipe_through :browser
live "/" , MessageLive
endТеперь, если вы обновите страницу, вы увидите следующее:

На этом этапе мы внесли несколько изменений, которые означают, что наш автоматический тестовый набор больше не будет проходить ... Запустите тесты в вашей командной строке со следующей командой:
mix testВы увидите вывод, похожий на следующее:
Generated liveview_chat app
..
1) test GET / (LiveviewChatWeb.PageControllerTest)
test/liveview_chat_web/controllers/page_controller_test.exs:4
Assertion with = ~ failed
code: assert html_response(conn, 200) = ~ " Welcome to Phoenix! "
left: " <!DOCTYPE html><html lang= " en " > <head> <meta charset= " utf-8 " > <meta http-equiv= " X-UA-Compatible " content= " IE=edge " >
<title data-suffix= " · Phoenix Framework " >LiveviewChat · Phoenix Framework</title> <link phx-track-static rel= " stylesheet " href= " /assets/app.css " > <script defer phx-track-static type= " text/javascript " src= " /assets/app.js " ></script> </head>
<body> <header> <section class= " container " >
<h1>LiveView Chat Example</h1></section> </header>
<h1>LiveView Message Page</h1></main></div> </body></html> "
right: " Welcome to Phoenix! "
stacktrace:
test/liveview_chat_web/controllers/page_controller_test.exs:6: (test)
Finished in 0.03 seconds (0.02s async, 0.01s sync)
3 tests, 1 failure Это связано с тем, что page_controller_test.exs все еще ожидает, что домашняя страница содержит "Welcome to Phoenix!" текст.
Давайте обновим тесты! Создать папку test/liveview_chat_web/live и файл message_live_test.exs в нем: test/liveview_chat_web/live/message_live_test.exs
Добавьте в него следующий тестовый код:
defmodule LiveviewChatWeb.MessageLiveTest do
use LiveviewChatWeb.ConnCase
import Phoenix.LiveViewTest
test "disconnected and connected mount" , % { conn: conn } do
conn = get ( conn , "/" )
assert html_response ( conn , 200 ) =~ "LiveView Message Page"
{ :ok , _view , _html } = live ( conn )
end
end Мы проверяем, что / конечная точка доступна, и на странице есть текстовая "LiveView Message Page" .
См. Также модуль LiveViewTest для получения дополнительной информации о тестировании и LiveView.
Наконец, вы можете удалить весь сгенерированный по умолчанию код, связанный с PageController :
rm test/liveview_chat_web/controllers/page_controller_test.exsrm lib/liveview_chat_web/controllers/page_controller.exrm test/liveview_chat_web/views/page_view_test.exsrm lib/liveview_chat_web/views/page_view.exrm -r lib/liveview_chat_web/templates/page Теперь вы можете запустить тест снова с помощью команды mix test . Вы должны увидеть следующее (прохождение тестов):
Generated liveview_chat app
...
Finished in 0.1 seconds (0.06s async, 0.1s sync)
3 tests, 0 failures
Randomized with seed 841084 С определенной структурой LiveView мы можем сосредоточиться на создании сообщений. База данных сохранит сообщение и имя отправителя. Давайте создадим новую схему и миграцию:
mix phx.gen.schema Message messages name:string message:stringПримечание . Не забудьте запустить
mix ecto.migrate, чтобы создать новую таблицуmessagesв базе данных.
Теперь мы можем обновить схему Message , чтобы добавить функции для создания новых сообщений и перечисления существующих сообщений. Мы также обновим набор изменений, чтобы добавить требования и проверки в текст сообщения. Откройте файл lib/liveview_chat/message.ex и обновите код следующим образом:
defmodule LiveviewChat.Message do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias LiveviewChat.Repo
alias __MODULE__
schema "messages" do
field :message , :string
field :name , :string
timestamps ( )
end
@ doc false
def changeset ( message , attrs ) do
message
|> cast ( attrs , [ :name , :message ] )
|> validate_required ( [ :name , :message ] )
|> validate_length ( :message , min: 2 )
end
def create_message ( attrs ) do
% Message { }
|> changeset ( attrs )
|> Repo . insert ( )
end
def list_messages do
Message
|> limit ( 20 )
|> order_by ( desc: :inserted_at )
|> Repo . all ( )
end
end Мы добавили функцию validate_length в ввод сообщения, чтобы убедиться, что сообщения имеют не менее 2 символов . Это всего лишь пример, чтобы показать, как валидация changeset работает с формой на странице LiveView .
Затем мы создали функции create_message/1 и list_messages/0 . Подобно Феникс-Чате-Пример, мы limit количество сообщений, возвращаемых до последних 20 .
mount/3 Откройте файл lib/liveview_chat_web/live/message_live.ex и добавьте следующую строку в строке 3:
alias LiveviewChat.Message Следующее обновление функции mount/3 в файле lib/liveview_chat_web/live/message_live.ex для использования функции list_messages :
def mount ( _params , _session , socket ) do
messages = Message . list_messages ( ) |> Enum . reverse ( )
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , changeset: changeset , messages: messages ) }
end mount/3 теперь получит список messages и создаст changeset , который будет использоваться для формы сообщения. Затем мы назначаем changeset и messages в сокет, которые будут отображать их на странице LiveView.
Обновите шаблон messages.html.heex для следующего кода:
< ul id =' msg-list ' phx-update =" append " >
< %= for message < - @messages do % >
< li id = { "msg-#{message.id}"} >
< b > < %= message.name % > : </ b >
< %= message.message % >
</ li >
< % end % >
</ ul >
< .form let={f} for={@changeset} id="form" phx-submit="new_message" phx-hook="Form" >
< %= text_input f, :name, id: "name", placeholder: "Your name", autofocus: "true" % >
< %= error_tag f, :name % >
< %= text_input f, :message, id: "msg", placeholder: "Your message" % >
< %= error_tag f, :message % >
< %= submit "Send"% >
</ .form > Сначала он отображает новые сообщения, а затем предоставляет для людей форму для create нового сообщения.
Если вы обновите страницу, вы должны увидеть следующее:

<.form></.form> Syntax - это то, как использовать компонент функции формы.
Функциональный компонент - это любая функция, которая получает карту
assignsв качестве аргумента и возвращает визуализированнуюstruct, построенную с сигилом~H
Наконец, давайте убедитесь, что тест все еще прошел, обновив assert в файле test/liveview_chat_web/live/message_live_test.exs по адресу:
assert html_response ( conn , 200 ) =~ "LiveView Chat" Поскольку мы удалили заголовок LiveView Message Page H1, мы можем вместо этого проверить заголовок в макете корня и убедиться, что страница все еще отображается правильно.
На данный момент, если мы запустим Mix App Phoenix mix phx.server и отправим форму в браузере, ничего не произойдет. Если мы посмотрим на журнал сервера, мы увидим следующее:
** (UndefinedFunctionError) function LiveviewChatWeb.MessageLive.handle_event/3
is undefined or private
(liveview_chat 0.1.0) LiveviewChatWeb.MessageLive.handle_event("new_message",
%{"_csrf_token" => "fyVPIls_XRBuGwlkMhxsFAciRRkpAVUOLW5k4UoR7JF1uZ5z2Dundigv",
"message" => %{"message" => "", "name" => ""}}, #Phoenix.LiveView.Socket
При отправке форма создает новое событие, определенное с помощью phx-submit :
< . form let = { f } for = { @ changeset } id = "form" phx - submit = "new_message" >
Однако это событие еще не управляется на сервере, мы можем исправить это, добавив функцию handle_event/3 в lib/liveview_chat_web/live/message_live.ex :
def handle_event ( "new_message" , % { "message" => params } , socket ) do
case Message . create_message ( params ) do
{ :error , changeset } ->
{ :noreply , assign ( socket , changeset: changeset ) }
{ :ok , _message } ->
changeset = Message . changeset ( % Message { } , % { "name" => params [ "name" ] } )
{ :noreply , assign ( socket , changeset: changeset ) }
end
end Функция create_message вызывается со значениями из формы. Если возникает error при попытке сохранить информацию в базе данных, например, changeset может вернуть ошибку, если имя или message пустое или если message слишком короткое, changeset присваивается снова в сокет. Это позволит форме отобразить информацию error :

Если сообщение сохраняется без каких -либо ошибок, мы создаем новый набор изменений, который содержит имя из формы, чтобы избежать людей снова вводить свое имя в форме, и мы назначаем новый набор изменений в сокет.

Теперь отображается форма, мы можем добавить следующие тесты для test/liveview_chat_web/live/message_live_test.exs :
import Plug.HTML , only: [ html_escape: 1 ]
test "name can't be blank" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "" , message: "hello" } )
|> render_submit ( ) =~ html_escape ( "can't be blank" )
end
test "message" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "Simon" , message: "" } )
|> render_submit ( ) =~ html_escape ( "can't be blank" )
end
test "minimum message length" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "Simon" , message: "h" } )
|> render_submit ( ) =~ "should be at least 2 character(s)"
end Мы используем функцию form/3 , чтобы выбрать форму и запустить событие отправки различными значениями для имени и сообщения. Мы проверяем, что ошибки отображаются должным образом.
Вместо того, чтобы перезагрузить страницу, чтобы увидеть недавно созданные сообщения, мы можем использовать PubSub ( Pub Lish Subs Scribe), чтобы информировать всех подключенных клиентов, что было создано новое сообщение, и обновить пользовательский интерфейс для отображения нового сообщения.
Откройте файл lib/liveview_chat/message.ex и добавьте следующую строку рядом с верхом:
alias Phoenix.PubSubЗатем добавьте следующие 3 функции:
def subscribe ( ) do
PubSub . subscribe ( LiveviewChat.PubSub , "liveview_chat" )
end
def notify ( { :ok , message } , event ) do
PubSub . broadcast ( LiveviewChat.PubSub , "liveview_chat" , { event , message } )
end
def notify ( { :error , reason } , _event ) , do: { :error , reason } subscribe/0 будет вызвана, когда клиент правильно отобразил страницу LiveView и прослушивает новые сообщения. Это просто функция обертки для phoenix.pubsub.subscribe.
notify/2 вызывается каждый раз, когда создается новое сообщение, чтобы транслировать сообщение подключенным клиентам. Repo.insert может либо возвращать {:ok, message} или {:error, reason} , поэтому нам нужно определить notify/2 обрабатывать оба случая.
Обновите функцию create_message/1 в message.ex notify/2
def create_message ( attrs ) do
% Message { }
|> changeset ( attrs )
|> Repo . insert ( )
|> notify ( :message_created )
endmount/3 Теперь мы можем подключить клиент, когда будет отображаться страница LiveView . В верхней части файла lib/liveview_chat_web/live/message_live.ex добавьте следующую строку:
alias LiveviewChat.PubSub Затем обновите функцию mount/3 с помощью:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
messages = Message . list_messages ( ) |> Enum . reverse ( )
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) }
end mount/3 Теперь проверяет, что сокет подключен, затем вызывает новое Message.subscribe/0 .
handle_event/3 Поскольку возвращаемое значение create_message/1 изменилось, нам нужно обновить handle_event/3 на следующее:
def handle_event ( "new_message" , % { "message" => params } , socket ) do
case Message . create_message ( params ) do
{ :error , changeset } ->
{ :noreply , assign ( socket , changeset: changeset ) }
:ok -> # broadcast returns :ok (just the atom!) if there are no errors
changeset = Message . changeset ( % Message { } , % { "name" => params [ "name" ] } )
{ :noreply , assign ( socket , changeset: changeset ) }
end
endhandle_info/2 Последним шагом является обработка события :message_created путем определения функции handle_info/2 в lib/liveview_chat_web/live/message_live.ex :
def handle_info ( { :message_created , message } , socket ) do
messages = socket . assigns . messages ++ [ message ]
{ :noreply , assign ( socket , messages: messages ) }
endКогда событие будет получено, новое сообщение добавляется в список существующих сообщений. Новый список затем назначается в сокет, который обновит пользовательский интерфейс для отображения нового сообщения.
Добавьте следующие тесты для test/liveview_chat_web/live/message_live_test.exs чтобы убедиться, что сообщения правильно отображаются на странице:
test "message form submitted correctly" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "Simon" , message: "hi" } )
|> render_submit ( )
assert render ( view ) =~ "<b>Simon:</b>"
assert render ( view ) =~ "hi"
end
test "handle_info/2" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert render ( view )
# send :created_message event when the message is created
Message . create_message ( % { "name" => "Simon" , "message" => "hello" } )
# test that the name and the message is displayed
assert render ( view ) =~ "<b>Simon:</b>"
assert render ( view ) =~ "hello"
end Теперь вы должны иметь функциональное приложение в чате с помощью LiveView! Запустите приложение Phoenix с:
mix phx.server Посетите приложение localhost:4000 в 2 или более браузерах и отправьте себе несколько сообщений!

Одна из проблем, которую мы можем заметить, заключается в том, что ввод сообщения не всегда сбрасывается до пустого значения после отправки сообщения, используя клавишу Enter в поле ввода. Это заставляет нас удалить предыдущее сообщение вручную перед написанием и отправкой нового.
Причина в:
Клиент JavaScript всегда является источником истины для текущих значений ввода. Для любого данного ввода с фокусом
LiveViewникогда не будет перезаписать текущее значение ввода, даже если оно отклоняется от обновлений сервера. См.: https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-специфики
Наше решение состоит в том, чтобы использовать phx-hook для запуска JavaScript на клиенте после одного из обратных вызовов LiveView Life-Cycle (подключенные, перед обновленными, разрушенными, отключенными, переподключенными).
Давайте добавим крючок для мониторинга, когда форма сообщения updated . phx-hook файле message.html.heex <.form>
< .form let={f} for={@changeset} id="form" phx-submit="new_message" phx-hook="Form" > Затем в файле assets/js/app.js добавьте следующую логику JavaScript :
// get message input element
let msg = document . getElementById ( 'msg' ) ;
// define "Form" hook, the name must match the one
// defined with phx-hoo="Form"
let Hooks = { }
Hooks . Form = {
// Each time the form is updated run the code in the callback
updated ( ) {
// If no error displayed reset the message value
if ( document . getElementsByClassName ( 'invalid-feedback' ) . length == 0 ) {
msg . value = '' ;
}
}
}
let csrfToken = document . querySelector ( "meta[name='csrf-token']" ) . getAttribute ( "content" )
let liveSocket = new LiveSocket ( "/live" , Socket , { params : { _csrf_token : csrfToken } , hooks : Hooks } ) // Add hooks: Hooks Основная логика для сброса значения сообщения содержится внутри функции updated() вызова
if ( document . getElementsByClassName ( 'invalid-feedback' ) . length == 0 ) {
msg . value = '' ;
} Прежде чем установить значение на пустую строку, сначала мы проверяем, что в форме не отображаются ошибки, проверяя на класс CSS invalid-feedback . (Подробнее о обратной связи: https://hexdocs.pm/phoenix_live_view/form-bindings.html#phx-feedback-for)
Последний шаг - установить hooks на liveSocket с hooks: Hooks . Ввод сообщения теперь должен быть сброшен при добавлении нового сообщения!
На данный момент функция mount/3 сначала инициализирует список сообщений, загрузив последние 20 сообщений из базы данных:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
messages = Message . list_messages ( ) |> Enum . reverse ( ) # get the list of messages
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) } ## assigns messages to socket
end Затем каждый раз, когда создается новое сообщение, функция handle_info Добавляет сообщение в список сообщений:
def handle_info ( { :message_created , message } , socket ) do
messages = socket . assigns . messages ++ [ message ] # append new message to the existing list
{ :noreply , assign ( socket , messages: messages ) }
endЭто может вызвать проблемы, если список сообщений становится слишком длинным, поскольку все сообщения хранятся в памяти на сервере.
Чтобы свести к минимуму использование памяти, мы можем определить сообщения как временное assign :
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
messages = Message . list_messages ( ) |> Enum . reverse ( )
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) ,
temporary_assigns: [ messages: [ ] ] }
endСписок сообщений извлекается один раз, затем он сбрасывается в пустой список.
Теперь handle_info/2 должен только назначить новое сообщение в розетку:
def handle_info ( { :message_created , message } , socket ) do
{ :noreply , assign ( socket , messages: [ message ] ) }
end Наконец, шаблон сообщений heex прослушивает любые изменения в списке сообщений с phx-update и добавляет новое сообщение в существующий отображаемый список.
< ul id =' msg-list ' phx-update =" append " >
< %= for message < - @messages do % >
< li id = {message.id} >
< b > < %= message.name % > : </ b >
< %= message.message % >
</ li >
< % end % >
</ ul > См. Также страница документации Phoenix temporary-assigns : https://hexdocs.pm/phoenix_live_view/dom-patching.html#temporary-assigns
В настоящее время поле name оставлено человеку, чтобы определить вручную, прежде чем отправлять сообщение. Это хорошо в базовом демонстрационном приложении, но мы знаем, что можем сделать лучше. В этом разделе мы добавим аутентификацию с помощью auth_plug . Это позволит людям, использующим приложение для аутентификации с помощью своей учетной записи GitHub или Google , а затем предварительно заполнить name в форме сообщения.
AUTH_API_KEYСогласно инструкциям сначала создайте новый ключ API по адресу https://authdemo.fly.dev/ Eg:

Затем создайте файл .env и добавьте свой новый созданный ключ API:
export AUTH_API_KEY = 88SwQGzaZoJYXs6ihvwMy2dRVtm6KVeg4tSCjRKtwDvMUYUbi/88SwQDatWtSTMd2rKPnaZsAWFNpbf4vv2ZK7JW2nwuSypMeg/authdemo.fly.devПримечание . По соображениям безопасности это не является действительным ключом API. Пожалуйста, создайте свой собственный, это бесплатно и занимает меньше минуты.
auth_plug Добавьте пакет Auth_plug в свои зависимости. В mix.exs file Обновите функцию deps и добавьте:
{ :auth_plug , "~> 1.4.10" } Эта зависимость создаст для вас новые сеансы и свяжется с приложением Dwyl auth .
Не забудьте:
source .envmix deps.get Убедитесь, что AUTH_API_KEY доступен до того, как новая зависимость будет составлена.
Вы можете перекомпилировать зависимости с помощью mix deps.compile --force .
Теперь мы можем начать добавлять функцию аутентификации.
router.ex Чтобы позволить [Unaultenticated] «гостевым» пользователям получить доступ к чату, мы используем AuthPlugOptional Plug. Узнайте больше на дополнительной аудитории.
В файле router.ex мы создаем новый трубопровод Plug :
# define the new pipeline using auth_plug
pipeline :authOptional , do: plug ( AuthPlugOptional ) Далее обновите scope "/", LiveviewChatWeb do Block до следующего:
scope "/" , LiveviewChatWeb do
pipe_through [ :browser , :authOptional ]
live "/" , MessageLive
get "/login" , AuthController , :login
get "/logout" , AuthController , :logout
endСейчас мы позволяем аутентификации быть необязательной для всех маршрутов в маршрутизаторе. Легко, эй?
AuthController Создайте AuthController с функциями login/2 и logout/2 .
Создайте новый файл: lib/liveview_chat_web/controllers/auth_controller.ex и добавьте следующий код:
defmodule LiveviewChatWeb.AuthController do
use LiveviewChatWeb , :controller
def login ( conn , _params ) do
redirect ( conn , external: AuthPlug . get_auth_url ( conn , "/" ) )
end
def logout ( conn , _params ) do
conn
|> AuthPlug . logout ( )
|> put_status ( 302 )
|> redirect ( to: "/" )
end
end Функция login/2 перенаправляет приложение Dwyl Auth. Узнайте больше о том, как использовать функцию AuthPlug.get_auth_url/2 . После аутентификации пользователь будет перенаправлен на / конечную точку, а в клиенте создается сеанс jwt .
Функция logout/2 вызывает AuthPlug.logout/1 , который удаляет сеанс (JWT) и перенаправляет обратно на домашнюю страницу.
on_mount/4 LiveView предоставляет обратный вызов on_mount , который позволяет нам запускать код перед mount . Мы будем использовать этот обратный вызов, чтобы проверить сеанс jwt и назначить значения person ( Map ) и loggedin ( boolean ) в socket .
В файле lib/liveview_chat_web/controllers/auth_controller.ex Добавьте следующий код для определения двух версий mount/4 :
# import the assign_new function from LiveView
import Phoenix.LiveView , only: [ assign_new: 3 ]
# pattern match on :default auth and check session has jwt
def on_mount ( :default , _params , % { "jwt" => jwt } = _session , socket ) do
# verify and retrieve jwt stored data
claims = AuthPlug.Token . verify_jwt! ( jwt )
# assigns the person and the loggedin values
socket =
socket
|> assign_new ( :person , fn ->
AuthPlug.Helpers . strip_struct_metadata ( claims )
end )
|> assign_new ( :loggedin , fn -> true end )
{ :cont , socket }
end
# when jwt is not defined just returns the current socket
def on_mount ( :default , _params , _session , socket ) do
socket = assign_new ( socket , :loggedin , fn -> false end )
{ :cont , socket }
endussys_new/3 присваивает значение сокету, если оно не существует.
Как только обратный вызов on_mount/2 , мы можем позвонить в наш файл lib/liveview_chat_web/live/message_live.ex :
defmodule LiveviewChatWeb.MessageLive do
use LiveviewChatWeb , :live_view
alias LiveviewChat.Message
# run authentication on mount
on_mount LiveviewChatWeb.AuthController
Теперь у нас есть вся логика, чтобы позволить людям аутентифицировать подлинность, нам просто нужно обновить наш файл корневого макета lib/liveview_chat_web/templates/layout/root.html.heex чтобы отобразить ссылку login (или logout ):
< body >
< header >
< section class =" container " >
< nav >
< ul >
< %= if @loggedin do % >
< li >
< img width =" 40px " src = {@person.picture}/ >
</ li >
< li > < %= link "logout", to: "/logout" % > </ li >
< % else % >
< li > < %= link "Login", to: "/login" % > </ li >
< % end % >
</ ul >
</ nav >
< h1 > LiveView Chat Example </ h1 >
</ section >
</ header >
< %= @inner_content % >
</ body > Если человек еще не loggedin мы отображаем ссылку login в противном случае отображается ссылка logout .
Последним шагом является отображение имени зарегистрированного лица в поле «Имя формы сообщения». Для этого мы можем обновить форму изменения формы в функции mount , чтобы установить параметры имени:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
# add name parameter if loggedin
changeset =
if socket . assigns . loggedin do
Message . changeset ( % Message { } , % { "name" => socket . assigns . person [ "givenName" ] } )
else
Message . changeset ( % Message { } , % { } )
end
messages = Message . list_messages ( ) |> Enum . reverse ( )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) ,
temporary_assigns: [ messages: [ ] ] }
endТеперь вы можете запустить приложение и иметь возможность входить в систему/вход!

В этом разделе мы будем использовать присутствие Phoenix для отображения списка людей, которые в настоящее время используют приложение.
Первым шагом является создание файла lib/liveview_chat/presence.ex :
defmodule LiveviewChat.Presence do
use Phoenix.Presence ,
otp_app: :liveview_chat ,
pubsub_server: LiveviewChat.PubSub
end Затем в lib/liveview_chat/application.ex мы добавляем недавно созданный модуль Presence в список приложений для начала супервизора:
def start ( _type , _args ) do
children = [
# Start the Ecto repository
LiveviewChat.Repo ,
# Start the Telemetry supervisor
LiveviewChatWeb.Telemetry ,
# Start the PubSub system
{ Phoenix.PubSub , name: LiveviewChat.PubSub } ,
# Presence
LiveviewChat.Presence ,
# Start the Endpoint (http/https)
LiveviewChatWeb.Endpoint
# Start a worker by calling: LiveviewChat.Worker.start_link(arg)
# {LiveviewChat.Worker, arg}
]
...
Теперь мы готовы использовать функции присутствия в нашей конечной точке LiveView.
В файле lib/liveview_chat_web/live/message_live.ex обновите функцию mount следующим образом:
@ presence_topic "liveview_chat_presence"
def mount ( _params , _session , socket ) do
if connected? ( socket ) do
Message . subscribe ( )
{ id , name } =
if socket . assigns . loggedin do
{ socket . assigns . person [ "id" ] , socket . assigns . person [ "givenName" ] }
else
{ socket . id , "guest" }
end
{ :ok , _ } = Presence . track ( self ( ) , @ presence_topic , id , % { name: name } )
Phoenix.PubSub . subscribe ( PubSub , @ presence_topic )
end
changeset =
if socket . assigns . loggedin do
Message . changeset ( % Message { } , % { "name" => socket . assigns . person [ "givenName" ] } )
else
Message . changeset ( % Message { } , % { } )
end
messages = Message . list_messages ( ) |> Enum . reverse ( )
{ :ok ,
assign ( socket ,
messages: messages ,
changeset: changeset ,
presence: get_presence_names ( )
) , temporary_assigns: [ messages: [ ] ] }
end Давайте вспомним основные изменения в функции mount/3 :
Сначала мы создаем атрибут модуля @presence_topic для определения topic которую мы используем с функциями присутствия.
Следующая часть кода определяет кортеж, содержащий id человека и его имя. Имя по умолчанию будет «гость», если человек не зарегистрирован.
{ id , name } =
if socket . assigns . loggedin do
{ socket . assigns . person [ "id" ] , socket . assigns . person [ "givenName" ] }
else
{ socket . id , "guest" }
endВо -вторых, мы используем функцию трека/4, чтобы присутствие знала, что новый клиент смотрит на приложение:
{ :ok , _ } = Presence . track ( self ( ) , @ presence_topic , id , % { name: name } )В -третьих, мы используем Pubsub для прослушивания изменений в присутствии (человек, присоединяющийся к приложению):
Phoenix.PubSub . subscribe ( PubSub , @ presence_topic ) Наконец, мы создаем новое presence в розетке:
presence : get_presence_names ( ) Функция get_presence_names вернет список пользователей Loggedin и, если есть количество пользователей «гостей».
Добавьте следующий код в конце модуля MessageLive :
defp get_presence_names ( ) do
Presence . list ( @ presence_topic )
|> Enum . map ( fn { _k , v } -> List . first ( v . metas ) . name end )
|> group_names ( )
end
# return list of names and number of guests
defp group_names ( names ) do
loggedin_names = Enum . filter ( names , fn name -> name != "guest" end )
guest_names =
Enum . count ( names , fn name -> name == "guest" end )
|> guest_names ( )
if guest_names do
[ guest_names | loggedin_names ]
else
loggedin_names
end
end
defp guest_names ( 0 ) , do: nil
defp guest_names ( 1 ) , do: "1 guest"
defp guest_names ( n ) , do: " #{ n } guests" Важный вызов функции в приведенном выше коде - Presence.list(@presence_topic) . Функция List/1 возвращает список пользователей, использующих приложение. Функция group_names и guest_names находятся здесь, чтобы манипулировать данными о присутствии, возвращаемых list , см.
До сих пор мы отслеживали новых людей, используя страницу чата в функции mount , и мы использовали Pubsub для прослушивания изменений в присутствии. Последним шагом является обработка этих изменений, добавив функцию handle_info :
def handle_info ( % { event: "presence_diff" , payload: _diff } , socket ) do
{ :noreply , assign ( socket , presence: get_presence_names ( ) ) }
endНаконец, клиентам будет отправлена различие присутствия и оставить события, поскольку они произойдут в режиме реального времени с событием "presesact_diff".
Функция handle_info завоевывает событие presence_diff и переназначает в сокет значение presence с результатом вызова функции get_presence_names .
Чтобы отобразить имена, мы добавляем следующее в файл шаблона lib/liveview_chat_web/templates/message/messages.html.heex :
< b > People currently using the app: </ b >
< ul >
< %= for name < - @presence do % >
< li >
< %= name % >
</ li >
< % end % >
</ ul >Теперь вы должны иметь возможность запустить приложение и увидеть пользователей Loggedin и количество приглашенных пользователей.
Мы можем проверить, что шаблон был должным образом обновлен, добавив эти два теста в test/liveview_chat_web/live/message_live_test.exs :
test "1 guest online" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert render ( view ) =~ "1 guest"
end
test "2 guests online" , % { conn: conn } do
{ :ok , _view , _html } = live ( conn , "/" )
{ :ok , view2 , _html } = live ( conn , "/" )
assert render ( view2 ) =~ "2 guests"
end Если вы новичок в Tailwind , см.: Https://github.com/dwyl/learn-tailwind
Замените содержимое lib/liveview_chat_web/templates/layout/root.html.heex с:
<!DOCTYPE html >
< html lang =" en " >
< head >
< meta charset =" utf-8 " />
< meta http-equiv =" X-UA-Compatible " content =" IE=edge " />
< meta name =" viewport " content =" width=device-width, initial-scale=1.0 " />
< meta name =" csrf-token " content = {csrf_token_value()} >
< %= live_title_tag assigns[:page_title] || "LiveviewChat", suffix: " · Phoenix Framework" % >
< script defer phx-track-static type =" text/javascript " src = {Routes.static_path(@conn, "/assets/app.js")} > </ script >
< script src =" https://cdn.tailwindcss.com " > </ script >
</ head >
< body >
< header class =" bg-slate-800 w-full min-h-[15%] pt-5 pb-1 mb-2 " >
< section >
< nav >
< div class =" text-white width-[10%] float-left ml-3 -mt-5 align-middle " >
< b > People in Chat: </ b >
< ul >
< %= for name < - @presence do % >
< li >
< %= name % >
</ li >
< % end % >
</ ul >
</ div >
< ul class =" float-right mr-3 " >
< %= if @loggedin do % >
< li >
< img width =" 42px " src = {@person.picture} class =" -mt-3 " />
</ li >
< li class =" text-white " >
< %= link "logout", to: "/logout" % >
</ li >
< % else % >
< li class =" bg-green-600 text-white rounded-xl px-4 py-2 w-full mb-2 font-bold " >
< %= link "Login", to: "/login" % >
</ li >
< % end % >
</ ul >
</ nav >
< h1 class =" text-3xl mb-4 text-center font-mono text-white " > LiveView Chat Example </ h1 >
</ section >
</ header >
< %= @inner_content % >
</ body >
</ html > И затем замените содержимое lib/liveview_chat_web/templates/message/messages.html.heex с:
< ul id =' msg-list ' phx-update =" append " >
< %= for message < - @messages do % >
< li id = { "msg-#{message.id}"} class="px-5" >
< small class =" float-right text-xs align-middle " >
< %= message.inserted_at % >
</ small >
< b > < %= message.name % > : </ b >
< %= message.message % >
</ li >
< % end % >
</ ul >
< footer class =" fixed bottom-0 w-full bg-slate-300 pb-2 px-5 pt-2 " >
< .form let={f} for={@changeset} id="form" phx-submit="new_message" phx-hook="Form" >
< %= if @loggedin do % >
< %= text_input f, :name, id: "name", value: @person.givenName,
class: "hidden" % >
< % else % >
< %= text_input f, :name, id: "name", placeholder: "Name", autofocus: "true",
class: "border p-2 w-9/12 mb-2 mt-2 mr2" % >
< span class =" italic text-2xl ml-4 " > or </ span >
< span class =" bg-green-600 text-white rounded-xl px-4 py-2 mb-2 mt-3 float-right " >
< %= link "Login", to: "/login" % >
</ span >
< %= error_tag f, :name % >
< % end % >
< %= text_input f, :message, id: "msg", placeholder: "Message",
class: "border p-2 w-10/12 mb-2 mt-2 float-left" % >
< p class =" text-amber-600 " >
< %= error_tag f, :message % >
</ p >
< %= submit "Send", class: "bg-sky-600 text-white rounded-xl px-4 py-2 mt-2 float-right" % >
</ .form >
</ footer >Теперь у вас должен быть пользовательский интерфейс/макет, который выглядит так:

Если у вас есть вопросы по поводу любого из используемых Tailwind , пожалуйста, потратьте 2 минуты в Google, а затем, если вы все еще застряли, откройте проблему.
Если вы нашли этот пример полезным, пожалуйста, ️ репозиторий GitHub, чтобы мы ( и другие ) знали, что вам понравилось!
Вот несколько других репозиториев, которые вы, возможно, захотите прочитать:
Есть вопросы или предложения? Не стесняйтесь открывать новые проблемы!
Спасибо!