В настоящее время очень важно иметь интерактивность на наших веб-сайтах.

Я лично не люблю пользоваться формой поиска без обратной связи в режиме реального времени. Введите что-нибудь, нажмите кнопку поиска, подождите ... Ничего не найдено. Хорошо. Попробуйте снова. Измените текст внутри поля ввода, снова нажмите кнопку поиска. И ждать. После нескольких итераций я замечаю, что стараюсь сократить количество поисковых запросов, потому что не хочу ждать и снова видеть страницу загрузки. Как разработчик программного обеспечения, я использую js-фреймворки, такие как React, когда требуется интерактивность. Но давайте будем честными: после их добавления ваше приложение становится намного сложнее разрабатывать и поддерживать. Кроме того, увеличивается время разработки и сложность проекта. Вот почему я дважды думал, прежде чем принять решение о добавлении одного из них, когда для небольшого проекта требуется функция реального времени. Но благодаря Phoenix LiveView вам не придется дважды думать, прежде чем добавить интерактивность! С помощью этой библиотеки вы можете иметь динамическое поведение с серверным HTML, который обновляется с помощью веб-сокетов под капотом.

Невероятно, насколько легко создать интерактивное приложение реального времени: это почти то же самое, что создать обычную статическую страницу! Я использовал LiveView для интерактивного поиска своего проекта под названием Heroku Lighthouse. Этот проект собирает все приложения Heroku и их пользовательские домены в одном месте и может быть очень полезным, когда вы должны взаимодействовать с большим количеством из них одновременно и не можете точно вспомнить, какой домен принадлежит какому приложению.

Поле поиска помогает найти нужные приложения по имени или собственному домену.

Позвольте мне показать вам, как легко добавить эту функцию в ваше приложение Phoenix!

Во-первых, нам нужно добавить LiveView в наш mix.exs:

def deps
   [  
     ...
     {:phoenix_live_view, "~> 0.3.0"}
   ]
end

Затем сгенерируйте секрет с помощью mix phx.gen.secret и обновите config.exs:

use Mix.Config
...
config :heroku_lighthouse, HerokuLighthouseWeb.Endpoint,
  pubsub: [name: HerokuLighthouse.PubSub, adapter: Phoenix.PubSub.PG2],
  live_view: [
    signing_salt: "<secret goes here>"
  ]

В моем случае я также добавил конфигурацию для pubsub, потому что она будет использоваться позже.

Добавьте код в lib/heroku_lighthouse_web.ex:

defmodule HerokuLighthouseWeb do
  def controller do
    quote do
      ...
      import Phoenix.LiveView.Controller
    end
  end
def view do
    quote do
      ...
      import Phoenix.LiveView,
        only: [
          live_render: 2, 
          live_render: 3, 
          live_link: 1, 
          live_link: 2
        ]
    end
  def router do
    quote do
      ...
      import Phoenix.LiveView.Router
    end
  end
end

Хорошо, мы почти закончили с приготовлениями. Просто нужно добавить код в assets/js/app.js:

import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"
let liveSocket = new LiveSocket("/live", Socket)
liveSocket.connect()

и assets/package.json:

"dependencies": {
    ...
    "phoenix_live_view": "file:../deps/phoenix_live_view"
},
...

При желании для перезагрузки страницы в реальном времени добавьте это в config/dev.exs:

config :heroku_lighthouse, HerokuLighthouseWeb.Endpoint,
  live_reload: [
    patterns: [
      ...
      ~r{lib/heroku_lighthouse_web/live/.*(ex)$}
    ]
  ]

Теперь мы готовы создать нашу страницу с LiveView.

Во-первых, давайте добавим новый маршрут к router.ex:

scope "/", HerokuLighthouseWeb do
  pipe_through [:browser, :authenticate_user]
  get "/dashboard", DashboardController, :index
end

Пришло время создать представление и контроллер.

lib/heroku_lighthouse_web/views/dashboard_view.ex выглядит так:

defmodule HerokuLighthouseWeb.DashboardView do
  use HerokuLighthouseWeb, :view
end

А это dashboard_controller.ex:

defmodule HerokuLighthouseWeb.DashboardController do
  use HerokuLighthouseWeb, :controller
  alias HerokuLighthouse.Dashboard
  alias Phoenix.LiveView
  alias HerokuLighthouseWeb.DashboardLive.Index
  def action(conn, _) do
    apply(__MODULE__, action_name(conn), [conn, conn.params, conn.assigns.current_user])
  end
  def index(conn, _params, user) do
    LiveView.Controller.live_render(conn, Index, session: %{user: user})
  end
end

Здесь ничего особенного. live_render отображает наш вид в реальном времени HerokuLighthouseWeb.DashboardLive.Index. Пора создать lib/heroku_lighthouse_web/live/dashboard_live/index.ex:

defmodule HerokuLighthouseWeb.DashboardLive.Index do
  use Phoenix.LiveView
  alias HerokuLighthouse.Dashboard
  alias HerokuLighthouseWeb.DashboardView
  def mount(session, socket) do
    apps_by_team = Dashboard.cached_grouped_apps(user)
    {:ok, assign(socket, apps_by_team: apps_by_team, current_user: session[:user])}
  end
  def render(assigns) do
    DashboardView.render("index.html", assigns)
  end
end

Когда визуализируется просмотр в реальном времени, вызывается обратный вызов mount. В моем случае я получаю данные из Heroku API, кэширую их, сохраняю результат в переменную apps_by_team и назначаю данные сокету. После этого вызывается render. Он возвращает браузеру простой HTML. Клиент подключается к просмотру в реальном времени через сокет и поддерживает постоянное соединение.

Далее нам нужно создать шаблон с полем поиска.
lib/heroku_lighthouse_web/templates/dashboard/index.html.leex:

<form phx-change="search" class="search-form">
  <%= text_input :search_field, :query, placeholder: "Search for application name, web url or custom domain", autofocus: true, "phx-debounce": "300" %>
</form>
<%= for {team, apps} <- @apps_by_team do %>
  <h2><%= team.name %></h4>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Web URL</th>
        <th>Domains</th>
      </tr>
    </thead>
    <tbody>
      <%= for app <- apps do %>
        <tr>
          <td><%= app.name %></td>
          <td><%= app.web_url %></td>
          <td>
            <%= for domain <- app.domains do %>
              <div><%= domain %></div>
            <% end %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% end %>

Атрибут формы phx-change="search" обрабатывает входные изменения и отправляет на сервер событие со значениями из всех входных данных внутри формы. Таким образом, каждый раз, когда ввод обновляется, сервер будет получать эти изменения. Чтобы уменьшить количество запросов к серверу, я также добавил "phx-debounce": "300" в поле поиска.
В настоящее время сервер не может обрабатывать эти события. Давай исправим.

lib/heroku_lighthouse_web/live/dashboard_live/index.ex:

defmodule HerokuLighthouseWeb.DashboardLive.Index do
  ...
  def handle_event("search", %{"search_field" => %{"query" => query}}, socket) do
    filtered_apps = Dashboard.filter_grouped_apps(socket.assigns.current_user, query)
    {:noreply, assign(socket, :apps_by_team, filtered_apps)}  
  end
end

Когда на сервер приходит событие, я просто фильтрую приложения по значению из поиска и присваиваю их apps_by_team. Phoenix live view замечает, что значение изменилось, и повторно отображает часть модели DOM с новыми отфильтрованными приложениями.

Как видите, с LiveView мы можем легко добавить интерактивности. Когда новый пользователь приходит в Heroku Lighthouse и у него есть несколько приложений на Heroku, рендеринг первой страницы может занять некоторое время. Давайте переместим эту операцию в фон, быстро отобразим страницу и обновим ее после получения всех данных. Мы можем сделать это, отправив сообщения на HerokuLighthouseWeb.DashboardLive.Index.

lib/heroku_lighthouse/dashboard/dashboard.ex:

defmodule HerokuLighthouse.Dashboard do
   ...
  def cached_grouped_apps(user) do
    Cachex.get!(:cache_warehouse, "user_#{user.id}_apps") || fetch_apps_async(user)
  end
  defp fetch_apps_async(user) do
    Phoenix.PubSub.broadcast(HerokuLighthouse.PubSub, "dashboard:#{user.id}", :fetching_apps)
    Task.start_link(fn ->
      fetch_and_cache_apps(user)
      Phoenix.PubSub.broadcast(HerokuLighthouse.PubSub, "dashboard:#{user.id}", :apps_fetched)
    end)
    []
  end

Если в кеше нет пользовательских приложений, мы публикуем событие с темой "dashboard:#{user.id}" и сообщением fetching_apps и начинаем получать приложения асинхронно. После загрузки приложений будет опубликовано новое сообщение :apps_fetched. Теперь нам нужно обработать эти сообщения в режиме реального времени.

lib/heroku_lighthouse_web/live/dashboard_live/index.ex:

defmodule HerokuLighthouseWeb.DashboardLive.Index do
  ...
  def mount(session, socket) do
    user = session[:user]
    is_fetching = Map.get(socket.assigns, :fetching_apps, false)
    Phoenix.PubSub.subscribe(HerokuLighthouse.PubSub, "dashboard:#{user.id}")
    apps_by_team = Dashboard.cached_grouped_apps(user)
    {
      :ok, 
      assign(
        socket, 
        apps_by_team: apps_by_team, 
        current_user: session[:user], 
        fetching_apps: is_fetching
      )
    }
  end
  def handle_info(:fetching_apps, socket) do
    {:noreply, assign(socket, fetching_apps: true)}
  end
  def handle_info(:apps_fetched, socket) do
    apps = Dashboard.cached_grouped_apps(socket.assigns.current_user)
    {:noreply, assign(socket, apps_by_team: apps, fetching_apps: false)}
  end
end

Последний шаг - обновить шаблон.

lib/heroku_lighthouse_web/templates/dashboard/index.html.leex:

<%= if @fetching_apps do %>
  <h2> Fetching apps...</h2>
<% else %>
  <form phx-change="search" class="search-form">
    <%= text_input :search_field, :query, "phx-debounce": "300", placeholder: "Search for application name, web url or custom domain", autofocus: true %>
  </form>
<% end %>

Страница будет обновлена ​​автоматически, как только все данные будут получены из Heroku!

Это все на сегодня!
Спасибо, что прочитали мой пост, надеюсь, он был вам полезен!