查看源代码 自定义错误页面

新的 Phoenix 项目有两个错误视图,称为 ErrorHTMLErrorJSON,它们位于 lib/hello_web/controllers/ 中。这些视图的目的是以通用的方式处理每个格式的错误,从一个集中位置进行处理。

错误视图

对于新应用程序,ErrorHTMLErrorJSON 视图如下所示

defmodule HelloWeb.ErrorHTML do
  use HelloWeb, :html

  # If you want to customize your error pages,
  # uncomment the embed_templates/1 call below
  # and add pages to the error directory:
  #
  #   * lib/<%= @lib_web_name %>/controllers/error_html/404.html.heex
  #   * lib/<%= @lib_web_name %>/controllers/error_html/500.html.heex
  #
  # embed_templates "error_html/*"

  # The default is to render a plain text page based on
  # the template name. For example, "404.html" becomes
  # "Not Found".
  def render(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

defmodule HelloWeb.ErrorJSON do
  # If you want to customize a particular status code,
  # you may add your own clauses, such as:
  #
  # def render("500.json", _assigns) do
  #   %{errors: %{detail: "Internal Server Error"}}
  # end

  # By default, Phoenix returns the status message from
  # the template name. For example, "404.json" becomes
  # "Not Found".
  def render(template, _assigns) do
    %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
  end
end

在我们深入研究之前,让我们看看在浏览器中渲染的 404 Not Found 消息是什么样的。在开发环境中,Phoenix 默认情况下会调试错误,向我们展示一个非常有信息量的调试页面。但是,我们在这里想要看到的是应用程序在生产中将要提供什么页面。为此,我们需要在 config/dev.exs 中设置 debug_errors: false

import Config

config :hello, HelloWeb.Endpoint,
  http: [port: 4000],
  debug_errors: false,
  code_reloader: true,
  . . .

修改配置文件后,我们需要重新启动服务器才能使此更改生效。重新启动服务器后,让我们转到 http://localhost:4000/such/a/wrong/path(对于运行的本地应用程序)并查看我们得到了什么。

好的,这不太令人兴奋。我们得到了一个简单的字符串“Not Found”,它没有任何标记或样式地显示。

第一个问题是,那个错误字符串从哪里来?答案就在 ErrorHTML 中。

def render(template, _assigns) do
  Phoenix.Controller.status_message_from_template(template)
end

很好,所以我们有这个 render/2 函数,它接受一个模板和一个 assigns 映射,我们忽略了它。当您从控制器调用 render(conn, :some_template) 时,Phoenix 首先在视图模块上查找 some_template/1 函数。如果不存在函数,它会回退到使用模板和格式名称调用 render/2,例如 "some_template.html"

换句话说,要提供自定义错误页面,我们可以简单地在 HelloWeb.ErrorHTML 中定义一个合适的 render/2 函数子句。

  def render("404.html", _assigns) do
    "Page Not Found"
  end

但我们还可以做得更好。

Phoenix 为我们生成了一个 ErrorHTML,但它没有给我们一个 lib/hello_web/controllers/error_html 目录。现在让我们创建一个。在我们新的目录中,让我们添加一个名为 404.html.heex 的模板,并给它一些标记——应用程序布局的混合以及一个新的 <div>,其中包含我们对用户的消息。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Welcome to Phoenix!</title>
    <link rel="stylesheet" href="/assets/app.css"/>
    <script defer type="text/javascript" src="/assets/app.js"></script>
  </head>
  <body>
    <header>
      <section class="container">
        <nav>
          <ul>
            <li><a href="https://hexdocs.erlang.ac.cn/phoenix/overview.html">Get Started</a></li>
          </ul>
        </nav>
        <a href="https://phoenix.erlang.ac.cn/" class="phx-logo">
          <img src="/images/logo.svg" alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <main class="container">
      <section class="phx-hero">
        <p>Sorry, the page you are looking for does not exist.</p>
      </section>
    </main>
  </body>
</html>

定义模板文件后,请记住删除该模板的等效 render/2 子句,否则该函数将覆盖该模板。让我们对我们之前在 lib/hello_web/controllers/error_html.ex 中引入的 404.html 子句这样做。我们还需要告诉 Phoenix 将我们的模板嵌入到模块中

+ embed_templates "error_html/*"

- def render("404.html", _assigns) do
-  "Page Not Found"
- end

现在,当我们回到 http://localhost:4000/such/a/wrong/path 时,我们应该看到一个更好的错误页面。值得注意的是,我们没有通过应用程序布局渲染我们的 404.html.heex 模板,即使我们希望我们的错误页面具有我们网站其他部分的外观。这是为了避免循环错误。例如,如果我们的应用程序由于布局中的错误而失败怎么办?再次尝试渲染布局只会触发另一个错误。所以理想情况下,我们希望最小化错误模板中的依赖项和逻辑,只共享必要的东西。

自定义异常

Elixir 提供了一个名为 defexception/1 的宏,用于定义自定义异常。异常表示为结构体,结构体需要在模块内部定义。

为了创建一个自定义异常,我们需要定义一个新的模块。按照惯例,这将包含“Error”。在该模块内部,我们需要使用 defexception/1 定义一个新的异常,文件 lib/hello_web.ex 似乎是它的一个好地方。

defmodule HelloWeb.SomethingNotFoundError do
  defexception [:message]
end

您可以像这样引发您的新异常

raise HelloWeb.SomethingNotFoundError, "oops"

默认情况下,Plug 和 Phoenix 会将所有异常视为 500 错误。但是,Plug 提供了一个名为 Plug.Exception 的协议,我们可以在其中自定义状态并添加异常结构可以在调试错误页面上返回的操作。

如果我们想为 HelloWeb.SomethingNotFoundError 错误提供 404 状态,我们可以通过像这样在 lib/hello_web.ex 中定义 Plug.Exception 协议的实现来实现

defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
  def status(_exception), do: 404
  def actions(_exception), do: []
end

或者,您可以在异常结构体中直接定义一个 plug_status 字段

defmodule HelloWeb.SomethingNotFoundError do
  defexception [:message, plug_status: 404]
end

但是,手动实现 Plug.Exception 协议在某些情况下可能很方便,例如在提供可操作的错误时。

可操作的错误

异常操作是可以从错误页面触发的函数,它们基本上是定义了要执行的 labelhandler 的映射列表。例如,Phoenix 会在您有待处理的迁移时显示错误,并在错误页面上提供一个按钮来执行待处理的迁移。

debug_errorstrue 时,它们在错误页面中作为按钮集合渲染,并遵循以下格式

[
  %{
    label: String.t(),
    handler: {module(), function :: atom(), args :: []}
  }
]

如果我们想为 HelloWeb.SomethingNotFoundError 返回一些操作,我们将像这样实现 Plug.Exception

defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
  def status(_exception), do: 404

  def actions(_exception) do
    [
      %{
        label: "Run seeds",
        handler: {Code, :eval_file, ["priv/repo/seeds.exs"]}
      }
    ]
  end
end