author: Sophie DeBenedetto author_link: https://github.com/sophiedebenedetto categories: til tags: [‘phoenix’] date: 2019-03-19 layout: post title: Walk-Through of Phoenix LiveView excerpt: >

Learn how to use Phoenix LiveView for real-time features without complicated JS frameworks.

直通 Phoenix LiveView

它来了!利用服务器渲染的 HTML 和 Phoenix 原生 WebSocket 工具的 Phoenix LiveView 来了,您可以在不使用复杂的 JavaScript 情况下构建花哨的实时功能。如果你厌倦了写JS(我今天和 Redux 的相处的不好,别问了),这就是你要的库。

Phoenix LiveView 是一个全新的产品,所以我想我会提供一个简短的 demo,为任何想要开始尝试它的人构建一个超级简单的演示。请记住,该库仍然是一个候选版本,因此可能会发生变化。

什么是 LiveView?

Chris McCord 在 12 月的 公告 中已经阐述的很好了。

Phoenix LiveView 是一个令人兴奋的新库,它通过服务器渲染的 HTML 实现了丰富的实时用户体验。LiveView 驱动的应用程序在服务器上是有状态的,并通过 WebSockets 进行双向通信,与 JavaScript 替代品相比,它提供了一个非常简化的编程模型。

杀掉你的 JavaScript

如果您经历了过于复杂的 SPA(例如,Redux 的所有东西),那么您会感觉到所有花哨的 JavaScript 常常伴随着维护和迭代成本。

Phoenix LiveView 感觉非常适合。 90% 的时间里,您确实希望进行一些实时更新,但实际上并不需要许多现代 JS 框架的破坏球。

让我们启动并运行 LiveView,以支持一项功能,该功能在我们的服务器执行创建 GitHub 存储库的分步过程时推送实时更新。

下面是我们正在构建的功能。


起步

Phoenix LiveView 的 Readme 中详细介绍了以下步骤:

  • 在你的 mix.exs 文件中安装依赖:
  1. def deps do
  2. [
  3. {:phoenix_live_view, "~> 0.2.0"}
  4. ]
  5. end
  • 使用签名盐更新应用程序的端点配置,以供实时查看连接使用:
  1. # Configures the endpoint
  2. config :my_app, MyApp.Endpoint,
  3. ...
  4. live_view: [
  5. signing_salt: "YOUR_SECRET"
  6. ]

注: 你可以在命令行使用 mix phx.gen.secret 生成一个秘钥

  • 更新您的配置,以启用扩展名为 .leex 的 LiveView 模板。
  1. config :phoenix,
  2. template_engines: [leex: Phoenix.LiveView.Engine]
  • :fetch_flash 之后,将 LiveView.Flash 插件添加到浏览器管道中。
  1. pipeline :browser do
  2. ...
  3. plug :fetch_flash
  4. plug Phoenix.LiveView.Flash
  5. end
  • 将以下内容导入您的 lib/app_web.ex 文件中:
  1. def view do
  2. quote do
  3. ...
  4. import Phoenix.LiveView, only: [live_render: 2, live_render: 3]
  5. end
  6. end
  7. def router do
  8. quote do
  9. ...
  10. import Phoenix.LiveView.Router
  11. end
  12. end
  • 在端点模块中暴露一个 socket,以供 LiveView 使用:
  1. defmodule MyAppWeb.Endpoint do
  2. use Phoenix.Endpoint
  3. socket "/live", Phoenix.LiveView.Socket
  4. # ...
  5. end
  • 在你的 NPM 依赖中添加 LiveView
  1. # assets/package.json
  2. {
  3. "dependencies": {
  4. ...
  5. "phoenix_live_view": "file:../deps/phoenix_live_view"
  6. }
  7. }

在这一步之后你需要运行 npm install

  • app.js 中使用 LiveView JavaScript 库连接到 LiveView socket
  1. import LiveSocket from "phoenix_live_view"
  2. let liveSocket = new LiveSocket("/live")
  3. liveSocket.connect()
  • 你的 live views 应保存在 lib/my_app_web/live/ 目录中。 为了支持页面实时重新加载,请将以下正则添加到您的 config/dev.exs中:
  1. config :demo, MyApp.Endpoint,
  2. live_reload: [
  3. patterns: [
  4. ...,
  5. ~r{lib/my_app_web/live/.*(ex)$}
  6. ]
  7. ]

现在我们已经准备好构建和渲染一个 live view 了

从控制器渲染一个 Live View

您可以 直接从路由器提供实时视图。 但是,在此示例中,我们将让我们的控制器渲染 live view。 让我们来看看我们的控制器:

  1. defmodule MyApp.PageController do
  2. use MyApp, :controller
  3. alias Phoenix.LiveView
  4. def index(conn, _) do
  5. LiveView.Controller.live_render(conn, MyAppWeb.GithubDeployView, session: %{})
  6. end
  7. end

我们调用 live_render/3 函数,该函数接受 conn 的参数,要呈现的 live view 以及我们要发送到实时视图中的任何会话信息。

现在,我们准备定义自己的 live view。

定义 Live View

我们的第一个 live view 位于 my_app_web/live/github_deploy_view.ex 中。该视图负责处理用户将某些内容 “部署” 到 GitHub 的交互。 此过程涉及创建 GitHub 组织,创建存储库并向该存储库推送一些内容。 就本示例而言,我们将不在乎此过程的实现细节。

我们的 live view 将使用 Phoenix.LiveView,并且必须实现两个功能:render/1mount/2

  1. defmodule MyAppWeb.GithubDeployView do
  2. use Phoenix.LiveView
  3. def render(assigns) do
  4. ~L"""
  5. <div class="">
  6. <div>
  7. <%= @deploy_step %>
  8. </div>
  9. </div>
  10. """
  11. end
  12. def mount(_session, socket) do
  13. {:ok, assign(socket, deploy_step: "Ready!")}
  14. end
  15. end

现在我们已经有了基本的部件,让我们来分析一下 live view 的过程。

它如何工作

live view 的连接过程是这样的:

live_view

当我们的应用程序接收到 index 路由的 HTTP 请求时,它将通过渲染 live view 的 render/1 函数中定义的静态 HTML 来进行响应。它将通过首先调用我们视图的 mount/2 函数来实现这一目的,然后再渲染由 mount/2 分配给 socket 的任何默认值填充的 HTML。

渲染的 HTML 将包括已签名的会话信息。会话将使用我们在 config.exs 中提供给 live view 配置的签名盐进行签名。当客户端打开 live view socket 连接时,签名的会话将被送回服务器。如果你在浏览器中检查渲染 live view 的页面,你会看到那个签名的会话。

  1. <div
  2. id="phx-20gvOvqvFMA="
  3. data-phx-view="MyApp.GithubDeployView"
  4. data-phx-session="SFMyNTY.g3QAAAACZAAEZGF0YW0AAACQZzNRQUFBQUVaQUFDYVdS">
  5. ...
  6. </div>

一旦渲染了静态 HTML,由于以下代码段,客户端将发送实时 socket 连接请求:

  1. import LiveSocket from "phoenix_live_view"
  2. let liveSocket = new LiveSocket("/live")
  3. liveSocket.connect()

这将启动一个有状态连接,该状态连接将导致 socket 更新时重新渲染视图。 由于页面首先呈现为静态 HTML,因此即使在浏览器中禁用了JavaScript,我们也可以放心,页面将始终为用户呈现。

现在,我们了解了如何首先呈现 live view 以及如何建立实时视图 socket 连接,让我们渲染一些实时更新。

渲染实时更新

LiveView 监听套接字的更新,并且只重新渲染页面中需要更新的部分。 仔细研究我们的 render/1 函数,我们看到它渲染了分配给套接字的键的值。

mount/2 赋值 :deploy_step 的情况下,我们的 render/1 函数将其渲染为:

  1. def render(assigns) do
  2. ~L"""
  3. <div class="">
  4. <div>
  5. <%= @deploy_step %>
  6. </div>
  7. </div>
  8. """
  9. end

注意: ~L 魔符代表 Live EEx。这是内置的 LiveView 模板。与 .ex 模板不同的是,LEEx 模板只能够跟踪和渲染必要的变化。因此,如果 @deploy_step 的值发生变化,我们的模板将只重新渲染页面的那一部分。

让我们给用户提供一种方法来启动 “deploy to GitHub” 的过程,并在每个部署步骤被执行时看到页面更新。

LiveView 支持 DOM 元素绑定,让我们有能力响应客户端事件。我们将创建一个 “deploy to GitHub” 按钮,并使用 phx-click 事件绑定监听该按钮的点击事件。

  1. def render(assigns) do
  2. ~L"""
  3. <div class="">
  4. <div>
  5. <div>
  6. <button phx-click="github_deploy">Deploy to GitHub</button>
  7. </div>
  8. Status: <%= @deploy_step %>
  9. </div>
  10. </div>
  11. """
  12. end

phx-click 绑定将会把我们的点击事件发送到服务器,由 GithubDeployView 处理。在我们的实时视图中,事件由 handle_event/3 函数处理。这个函数会接收一个参数,即事件名称、一个值和 socket。

许多方法可以填充 value 参数,但我们在这个例子中不会使用这个数据点。

让我们为 “github_deploy” 事件构建 handle_event/3 函数。

  1. def handle_event("github_deploy", _value, socket) do
  2. # do the deploy process
  3. {:noreply, assign(socket, deploy_step: "Starting deploy...")}
  4. end

我们的函数负责两件事。首先,它将启动部署过程(coming soon)。然后,它将更新套接字中 :deploy_step 键的值,这将导致我们的模板重新渲染页面中 <%= @deploy_step %> 的部分。所以用户将看到 Status: Ready! 改为 Status: Starting deploy...

接下来,我们需要 “部署到 GitHub” 的过程能够在每一次更新 socket 的 :deploy_step。我们将让视图的 handle_event/3 函数向自己发送一条消息,以推送过程中的每一个连续步骤。

  1. def handle_event("github_deploy", _value, socket) do
  2. :ok = MyApp.start_deploy()
  3. send(self(), :create_org)
  4. {:noreply, assign(socket, deploy_step: "Starting deploy...")}
  5. end
  6. def handle_info(:create_org, socket) do
  7. {:ok, org} = MyApp.create_org()
  8. send(self(), {:create_repo, org})
  9. {:noreply, assign(socket, deploy_step: "Creating GitHub org...")}
  10. end
  11. def handle_info({:create_repo, org}, socket) do
  12. {:ok, repo} = MyApp.create_repo(org)
  13. send(self(), {:push_contents, repo})
  14. {:noreply, assign(socket, deploy_step: "Creating GitHub repo...")}
  15. end
  16. def handle_info({:push_contents, repo}, socket) do
  17. :ok = MyApp.push_contents(repo)
  18. send(self(), :done)
  19. {:noreply, assign(socket, deploy_step: "Pushing to repo...")}
  20. end
  21. def handle_info(:done, socket) do
  22. {:noreply, assign(socket, deploy_step: "Done!")}
  23. end

这段代码很简单—我们并不担心部署 GitHub repo 的实现细节,但我们可以想象如何在这个代码流中添加错误处理和其他责任。

我们的 handle_event/3 函数通过发送 :create_org 消息给视图本身来启动部署过程。我们的视图通过调用执行该步骤的代码和更新套接字来响应此消息。这将导致我们的模板再次重新渲染,所以用户会看到 Status: Starting deploy... 改为 Status: Creating GitHub org...。这样,视图就会执行 GitHub 部署过程中的每一步,更新 socket,并使模板每次都重新渲染。

现在我们的实时更新已经开始工作了,让我们把 HTML 代码从 render/1 函数中重构一下,放到自己的模板文件中。

渲染一个模板文件

我们将模板定义在 lib/my_app_web/templates/page/github_deploy.html.leex:

  1. <div>
  2. <div class>
  3. <button phx-click="github_deploy">Deploy to GitHub</button>
  4. <div class="github-deploy">
  5. Status: <%= @deploy_step %>
  6. </div>
  7. </div>
  8. </div>

接下来,我们只需要简单地让 live view 的 render/1 函数告诉我们的 PageView 去渲染这个模板。

  1. defmodule MyApp.GithubDeployView do
  2. use Phoenix.LiveView
  3. def render(assigns) do
  4. MyApp.PageView.render("github_deploy.html", assigns)
  5. end
  6. ...
  7. end

现在我们的代码更有条理了。

结语

从这个有限的例子中,我们可以看到这是一个多么强大的产品。我们只用服务器端的代码就能实现这个实时功能,而且服务器端的代码还不多。我真的很喜欢玩 Phoenix LiveView,我很高兴看到其他开发人员用它构建的东西。祝你编码愉快!