LiveView الدردشة التعليمية 
لقد أردنا حقًا مثالًا حقيقيًا ومفتوحًا في العالم الحقيقي مع الكود والاختبارات والمكتوبة.
لقد كتبنا هذا حتى نتمكن من توجيه الأشخاص في فريقنا/المجتمع الذي يتعلم Phoenix LiveView .
يأخذك مثال/برنامج تعليمي LiveView من الصفر إلى تطبيق يعمل بالكامل في 20 دقيقة .
فيما يلي جدول محتويات ما يمكنك توقعه في هذا المثال/البرنامج التعليمي:
LiveView الدردشة التعليميةPhoenixlive ووحدة تحكم LiveView والقالبrouter.exmount/3 وظيفةmount/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> إلى:
< 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 على إدخال الرسالة للتأكد من أن الرسائل تحتوي على حرفين على الأقل . هذا مجرد مثال لإظهار كيف يعمل التحقق من صحة changeset مع النموذج في صفحة LiveView .
ثم أنشأنا وظائف create_message/1 و list_messages/0 . على غرار مثال Phoenix-Chat-Example ، فإننا 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> بناء جملة هو كيفية استخدام مكون دالة النموذج.
مكون الوظيفة هو أي وظيفة تتلقى خريطة
assignsكوسيطة وتُرجعstructمقدمة مصممة باستخدام sigil~H.
أخيرًا ، دعونا نتأكد من أن الاختبار لا يزال يمر عن طريق تحديث assert في ملف test/liveview_chat_web/live/message_live_test.exs إلى:
assert html_response ( conn , 200 ) =~ "LiveView Chat" نظرًا لأننا قمنا بحذف عنوان LiveView Message Page H1 ، يمكننا بدلاً من ذلك اختبار العنوان في تخطيط الجذر والتأكد من عرض الصفحة بشكل صحيح.
في الوقت الحالي ، إذا قمنا بتشغيل Mix 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" >
ومع ذلك ، لم تتم إدارة هذا الحدث على الخادم حتى الآن ، يمكننا إصلاح هذا عن طريق إضافة وظيفة 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) لإبلاغ جميع العملاء المتصلين بأنه قد تم إنشاء رسالة جديدة وتحديث واجهة المستخدم لعرض الرسالة الجديدة.
افتح ملف lib/liveview_chat/message.ex وأضف السطر التالي بالقرب من الأعلى:
alias Phoenix.PubSubأضف بعد ذلك الوظائف الثلاثة التالية:
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 هو دائمًا مصدر الحقيقة لقيم الإدخال الحالية. بالنسبة لأي مدخلات معينة مع Focus ، لن تقوم
LiveViewأبدًا بالكتابة على القيمة الحالية للمدخلات ، حتى لو كانت تنحرف من التحديثات المقدمة من الخادم. راجع: https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-specifics
يتمثل حلنا في استخدام phx-hook لتشغيل بعض JavaScript على العميل بعد واحدة من عمليات الاسترداد لدورة الحياة LiveView (مثبتة ، قبل الاستغناء عنها ، تحديثها ، تدمير ، منفصلة ، إعادة الاتصال).
دعنا نضيف خطافًا لمراقبة عند updated نموذج الرسالة. في ملف message.html.heex ، أضف سمة phx-hook إلى عنصر <.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 > راجع أيضًا صفحة الوثائق temporary-assigns في Phoenix:
حاليًا يتم ترك حقل 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 ، قم بتحديث وظيفة deps الخاصة بك وأضف:
{ :auth_plug , "~> 1.4.10" } ستنشئ هذه التبعية جلسات جديدة لك والتواصل مع تطبيق Dwyl auth .
لا تنسى:
source .envmix deps.get تأكد من الوصول إلى AUTH_API_KEY قبل تجميع التبعية الجديدة.
يمكنك إعادة ترجمة التبعيات باستخدام mix deps.compile --force .
الآن يمكننا البدء في إضافة ميزة المصادقة.
router.ex للسماح لمستخدمي "ضيف" غير مصادقين] بالوصول إلى الدردشة ، نستخدم قابس AuthPlugOptional . اقرأ المزيد في المصادقة الاختيارية.
في ملف 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 إنشاء 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 }
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
لدينا الآن كل المنطق للسماح للأشخاص بالمصادقة ، نحتاج فقط إلى تحديث ملف التخطيط الجذر الخاص بنا 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 :
أولاً ، نقوم بإنشاء MODULE @presence_topic لتحديد topic سنستخدمه مع وظائف التواجد.
يعرّف الجزء التالي من الرمز توبًا يحتوي على id للشخص واسمه. سوف يكون الاسم افتراضيًا لـ "الضيف" إذا لم يتم تسجيل الشخص.
{ id , name } =
if socket . assigns . loggedin do
{ socket . assigns . person [ "id" ] , socket . assigns . person [ "givenName" ] }
else
{ socket . id , "guest" }
endثانياً ، نستخدم وظيفة Track/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 قائمة بمستخدمي تسجيل الدخول وإذا كان عدد مستخدمي "الضيف".
أضف الكود التالي في نهاية وحدة 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.presence.html#c
حتى الآن ، قمنا بتتبع أشخاص جدد باستخدام صفحة الدردشة في وظيفة mount ، وقد استخدمنا PubSub للاستماع إلى تغييرات التواجد. الخطوة الأخيرة هي التعامل مع هذه التغييرات عن طريق إضافة وظيفة handle_info :
def handle_info ( % { event: "presence_diff" , payload: _diff } , socket ) do
{ :noreply , assign ( socket , presence: get_presence_names ( ) ) }
endأخيرًا ، سيتم إرسال مجموعة مختلفة من التواجد وسيتم إرسال أحداث الإجازة إلى العملاء عند حدوثها في الوقت الفعلي مع حدث "التواجد _diff".
تلتقط وظيفة handle_info حدث presence_diff ويعيد تعيينه إلى المقبس قيمة presence مع نتيجة استدعاء وظيفة get_presence_names .
لعرض الأسماء ، نضيف ما يلي في lib/liveview_chat_web/templates/message/messages.html.heex file:
< 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 المستخدمة ، فيرجى قضاء دقيقتين من googling ثم إذا كنت لا تزال عالقًا ، افتح مشكلة.
إذا وجدت هذا المثال مفيدًا ، فيرجى مستودع GitHub ، لذلك نحن ( والآخرين ) نعرفك!
فيما يلي بعض المستودعات الأخرى التي قد ترغب في قراءتها:
أي أسئلة أو اقتراحات؟ لا تتردد في فتح قضايا جديدة!
شكرًا لك!