檢視原始碼 自訂錯誤頁面

新的 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 找不到 訊息是什麼樣子。Phoenix 預設會在開發環境中偵錯錯誤,並向我們顯示資訊豐富的偵錯頁面。然而,我們希望看到應用程式在正式環境中會提供什麼樣的頁面。為此,我們需要在 config/dev.exs 中設定 debug_errors: false

import Config

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

在修改設定檔之後,我們需要重新啟動伺服器才能讓變更生效。重新啟動伺服器後,讓我們開啟一個正在執行的本機應用程式 https://127.0.0.1:4000/such/a/wrong/path,看看會出現什麼結果。

嗯,這沒什麼新鮮的。我們只會看到一個顯示在裸字串中的「找不到」,而沒有任何標記或樣式。

第一個問題是,此錯誤字串從何而來?答案就在 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.dev.org.tw/phoenix/overview.html">Get Started</a></li>
          </ul>
        </nav>
        <a href="https://phoenix.dev.org.tw/" 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

現在,當我們回到 https://127.0.0.1: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 協定在某些情況下會很方便,例如提供可行的錯誤。

可行的錯誤

例外處理動作是在錯誤頁面觸發的函數,它們基本上是定義 標籤處理常式 的清單,以便執行。例如,如果您有待處理的遷移,則 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