LiveView聊天教程
我们真的想要一个免费和开源的现实世界示例,其中包含完整的代码,测试和auth。
我们写了这篇文章,以便我们可以将我们的团队/社区学习Phoenix LiveView的人指向它。
这个LiveView示例/教程将您从零到20分钟的应用程序。
这是您可以期望在此示例/教程中涵盖的目录:
LiveView聊天教程Phoenix应用live目录, LiveView控制器和模板router.exmount/3功能mount/3handle_event/3handle_info/2AUTH_API_KEYauth_plugrouter.ex中创建可选的验证管道AuthControlleron_mount/4功能任何学习Phoenix LiveView想要一个独立教程的人,包括: Setup , Testing , Authentication , Presence ,
建议您遵循LiveView Counter教程,因为该教程更为先进。至少,请查看先决条件列表,以便在开始这次冒险之前就知道需要在计算机上安装的内容!
只要您安装了Elixir , Phoenix和Postgres ,那就可以了!
Phoenix应用首先创建新的liveview_chat Phoenix应用程序:
mix phx.new liveview_chat --no-mailer --no-dashboard我们不需要email或dashboard功能,因此我们将它们排除在应用程序之外。您可以通过运行: mix help phx.new了解有关创建新的Phoenix应用程序的更多信息
运行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 ),该词显示messages.html.heex template ,我们将在下面定义。
创建lib/liveview_chat_web/views/message_view.ex文件:
defmodule LiveviewChatWeb.MessageView do
use LiveviewChatWeb , :view
end这类似于常规的Phoenix view 。这里没什么特别的/有趣的。
接下来,创建lib/liveview_chat_web/templates/message Directory,然后创建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>的内容更新为:
< 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 Controller:
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" 。
有关测试和LiveView的更多信息,另请参见LiveViewTest模块。
最后,您可以删除链接到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下一个更新lib/liveview_chat_web/live/message_live.ex文件中的mount/3函数以使用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>语法是如何使用表单函数组件的方法。
函数组件是接收
assigns映射作为参数并返回使用~Hsigil构建的渲染struct的任何函数。
最后,让我们确保测试仍在通过更新test/liveview_chat_web/live/message_live_test.exs文件中的assert来通过:
assert html_response ( conn , 200 ) =~ "LiveView Chat"当我们删除了LiveView Message Page H1标题时,我们可以在根布局中测试标题,并确保该页面仍然正确显示。
目前,如果我们运行Phoenix App 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" >
但是,该事件尚未在服务器上管理,我们可以通过在lib/liveview_chat_web/live/message_live.ex中添加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 , _message } ->
changeset = Message . changeset ( % Message { } , % { "name" => params [ "name" ] } )
{ :noreply , assign ( socket , changeset: changeset ) }
end
end create_message函数与表单中的值一起调用。如果在尝试将信息保存在数据库中时发生error ,例如,如果名称或message为空,则changeset可能会返回错误,或者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 Sub Scribe)通知所有已连接的客户,而不必重新加载页面才能查看新创建的消息,而是创建了新消息并更新UI以显示新消息。
打开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 }当客户端正确显示LiveView页面并收听新消息时,将调用subscribe/0它只是Phoenix.pubsub.subscribe的包装功能。
每次创建新消息将消息传播给连接的客户端时,都会调用notify/2 。 Repo.insert可以返回{:ok, message}或{:error, reason} ,因此我们需要定义notify/2处理两种情况。
在message.ex中更新create_message/1函数以调用我们新创建的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最后一步是通过在lib/liveview_chat_web/live/message_live.ex中定义handle_info/2函数来处理:message_created事件:
def handle_info ( { :message_created , message } , socket ) do
messages = socket . assigns . messages ++ [ message ]
{ :noreply , assign ( socket , messages: messages ) }
end收到事件后,将新消息添加到现有消息列表中。然后将新列表分配给套接字,该列将更新UI以显示新消息。
将以下测试添加到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或更多的浏览器,并向自己发送一些消息!

我们可以注意到的一个问题是,在使用输入字段上的Enter键发送消息后,消息输入并不总是重置为空值。这迫使我们在编写并发送新消息之前手动删除了先前的消息。
原因是:
JavaScript客户端始终是当前输入值的真相来源。对于任何具有焦点的给定输入,
LiveView也永远不会覆盖输入的当前值,即使它与服务器的渲染更新偏离。请参阅:https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-xtregifics
我们的解决方案是在一个LiveView Life-Cycle回调(安装,更新,更新,破坏,断开,重新连接)之后,使用phx-hook在客户端上运行一些JavaScript。
让我们添加一个挂钩以在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 = '' ;
}在将值设置为空字符串之前,我们首先检查表格上没有显示invalid-feedback CSS类,在表单上显示错误。 (阅读有关反馈的更多信息: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#tempormorary-assigns
当前, name字段留给了该人在发送消息之前手动定义。这在基本的演示应用程序中很好,但是我们知道我们可以做得更好。在本节中,我们将使用auth_plug添加身份验证。这将允许使用该应用程序的人们使用其GitHub或Google帐户进行身份验证,然后在消息表格中预填充name 。
AUTH_API_KEY根据说明,首先在https://authdemo.fly.dev/ eg中创建一个新的API密钥:

然后创建一个.env文件并添加您的新创建的API密钥:
export AUTH_API_KEY = 88SwQGzaZoJYXs6ihvwMy2dRVtm6KVeg4tSCjRKtwDvMUYUbi/88SwQDatWtSTMd2rKPnaZsAWFNpbf4vv2ZK7JW2nwuSypMeg/authdemo.fly.dev注意:出于安全原因,这不是有效的API密钥。请创建自己的,它是免费的,需要不到一分钟的时间。
auth_plug将auth_plug软件包添加到您的依赖项中。在mix.exs文件中更新您的deps函数并添加:
{ :auth_plug , "~> 1.4.10" }此依赖性将为您创建新的会议,并与DWYL auth应用程序进行通信。
不要忘记:
source .envmix deps.get在编译新依赖性之前,请确保可以访问AUTH_API_KEY 。
您可以将依赖项重新编译为mix deps.compile --force 。
现在,我们可以开始添加身份验证功能。
router.ex中创建可选的验证管道为了允许[未经身份验证]“访客”用户访问聊天,我们使用AuthPlugOptional插件。在可选的auth上阅读更多信息。
在router.ex文件中,我们创建一个新的Plug管道:
# define the new pipeline using auth_plug
pipeline :authOptional , do: plug ( AuthPlugOptional )接下来更新scope "/", LiveviewChatWeb do阻止以下内容:
scope "/" , LiveviewChatWeb do
pipe_through [ :browser , :authOptional ]
live "/" , MessageLive
get "/login" , AuthController , :login
get "/logout" , AuthController , :logout
end现在,我们允许对路由器中所有路线的身份验证是可选的。容易,嘿?
AuthController创建具有login/2和logout/2功能的AuthController 。
创建一个新文件: 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验证应用程序。阅读有关如何使用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 }
end如果不存在,则分配_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
现在,我们拥有所有逻辑来让人们进行身份验证,我们只需要更新我们的root布局文件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及其名称的元组。如果该人不是loggedin,则该名称将默认为“来宾”。
{ 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) 。列表/1功能返回使用该应用程序的用户列表。函数group_names和guest_names在这里只是为了操纵list返回的存在数据,请参见https://hexdocs.pm/phoenix/phoenix/phoenix.presence.html#c:list/1-presence/1-presence-data-scrupture
到目前为止,我们已经使用mount功能中的聊天页面跟踪了新朋友,并且我们一直在使用PubSub来收听在场更改。最后一步是通过添加handle_info函数来处理这些更改:
def handle_info ( % { event: "presence_diff" , payload: _diff } , socket ) do
{ :noreply , assign ( socket , presence: get_presence_names ( ) ) }
end最后,在“存在”事件实时发生时,将会将一定的存在加入和休假事件发送给客户。
handle_info函数捕获了presence_diff事件,并以get_presence_names函数调用的结果重新分配到presence 。
要显示名称,我们在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 >现在,您应该拥有一个看起来像这样的UI/布局:

如果您对使用的任何Tailwind班有疑问,请花2分钟的时间谷歌搜索,然后如果您仍然卡住,请打开问题。
如果您发现此示例有用,请github存储库,所以我们(和其他人)知道您喜欢它!
这是您可能想阅读的其他一些存储库:
有任何问题或建议吗?请随时开放新问题!
谢谢你!