查看源代码 错误和异常处理

与任何其他 Elixir 代码一样,在 LiveView 生命周期的各个阶段都可能发生异常。此页面描述了 LiveView 在不同阶段如何处理错误。

预期场景

在本节中,我们将讨论您希望在应用程序中发生的错误情况。例如,用户填写带有无效数据的表单是预期的。在 LiveView 中,我们通常通过将表单状态存储在 LiveView 赋值中并将任何相关的错误消息渲染回客户端来处理这些情况。

我们也可以为此使用 flash 消息。例如,想象一下您有一个页面来管理组织中的所有“团队成员”。但是,如果组织中只剩下一个成员,则不允许他们离开。您可能希望通过使用闪存消息来处理这个问题

if MyApp.Org.leave(socket.assigns.current_org, member) do
  {:noreply, socket}
else
  {:noreply, put_flash(socket, :error, "last member cannot leave organization")}
end

然而,有人可能会争辩说,如果组织的最后一名成员不能离开,那么当组织只有一个成员时,最好甚至不要在 UI 中显示“离开”按钮。

鉴于该按钮没有出现在 UI 中,当组织只有一个成员时触发“离开”操作是一个意外的场景。这意味着我们可以将上面的代码重写为

true = MyApp.Org.leave(socket.assigns.current_org, member)
{:noreply, socket}

如果 leave 没有返回 true,Elixir 将引发一个 MatchError 异常。或者您可以提供一个 leave! 函数来引发特定异常

MyApp.Org.leave!(socket.assigns.current_org, member)
{:noreply, socket}

但是,如果发生异常,LiveView 会发生什么?让我们谈谈意外场景。

意外场景

Elixir 开发人员倾向于编写断言代码。这意味着,如果我们希望 leave 始终返回 true,我们可以像上面那样显式地匹配其结果

true = MyApp.Org.leave(socket.assigns.current_org, member)
{:noreply, socket}

如果 leave 失败并返回 false,则会引发异常。对于 Elixir 开发人员来说,在他们的 Phoenix 应用程序中使用异常来处理意外场景很常见。

例如,如果您正在构建一个应用程序,其中用户可能属于一个或多个组织,那么在访问组织页面时,您可能希望检查用户是否有权访问它,如下所示

organizations_query = Ecto.assoc(socket.assigns.current_user, :organizations)
Repo.get!(organizations_query, params["org_id"])

上面的代码构建了一个查询,该查询返回属于当前用户的所有组织,然后验证给定的 org_id 是否属于用户。如果没有这样的 org_id 或用户无权访问它,Repo.get! 将引发一个 Ecto.NoResultsError 异常。

在常规的控制器请求期间,此异常将转换为 404 异常,并作为自定义错误页面呈现,如 此处详细说明。LiveView 将以三种不同的方式对异常做出反应,具体取决于它在生命周期中的哪个阶段。

HTTP 装载期间的异常

当您第一次访问 LiveView 时,一个常规的 HTTP 请求被发送到服务器并由 LiveView 处理。 mount 回调被调用,然后渲染一个页面。此处的任何异常都会被捕获、记录并由 Phoenix 错误视图转换为异常页面 - 正如它在控制器中如何工作一样。

连接装载期间的异常

如果初始 HTTP 请求成功,LiveView 将使用有状态连接(通常是 WebSocket)连接到服务器。这会在服务器上生成一个长时间运行的轻量级 Elixir 进程,该进程调用 mount 回调并渲染页面的更新版本。

此阶段的异常将导致 LiveView 进程崩溃,这将被记录。一旦客户端注意到崩溃,它将完全重新加载页面。这将导致 mount 在常规 HTTP 请求(上一小节的完全场景)期间再次被调用。

换句话说,LiveView 会在发生错误时重新加载页面,使其失败,就好像 LiveView 最初没有参与渲染一样。

连接装载后的异常

一旦您的 LiveView 被装载并连接,任何错误都会导致 LiveView 进程崩溃并被记录。一旦客户端注意到错误,它将通过有状态连接重新装载 LiveView,而不会重新加载页面(上一小节的完全场景)。如果重新装载成功,LiveView 将恢复到工作状态,更新页面并向用户显示最新信息。

例如,假设两个用户同时尝试离开组织。在这种情况下,他们两个人都会看到“离开”按钮,但我们的 leave 函数调用只会对其中一个人成功

true = MyApp.Org.leave(socket.assigns.current_org, member)
{:noreply, socket}

当异常引发时,客户端将重新装载 LiveView。一旦您重新装载,您的代码现在会注意到组织中只有一个用户,因此不再显示“离开”按钮。换句话说,通过重新装载,我们通常会更新页面的状态,从而允许自动处理异常。

请注意,在 leave 函数的结果上使用 if 条件检查,还是简单地断言它返回 true,完全取决于您。如果每个人同时离开组织的可能性很低,那么您也可以将其视为意外情况。尽管其他开发人员会更愿意通过显式处理这些情况来解决问题。在这两种情况下,LiveView 都能满足您的需求。