檢視原始碼 JSON 和 API

要求:本指南預設您已完成 簡介指南 且已啟動並執行 Phoenix 應用程式。

要求:本指南預設您已完成 控制器指南

您也可以使用 Phoenix 框架建立 Web API。Phoenix 預設支援 JSON,但您可以導入您想要的任何其他呈現格式。

JSON API

在這個指南中,我們將建立一個簡單的 JSON API 來儲存我們最喜愛的連結,它將可以預設支援所有 CRUD(建立、讀取、更新、刪除)操作。

在這個指南中,我們將使用 Phoenix 產生器為我們的 API 基礎架構產生腳本。

mix phx.gen.json Urls Url urls link:string title:string
* creating lib/hello_web/controllers/url_controller.ex
* creating lib/hello_web/controllers/url_json.ex
* creating lib/hello_web/controllers/changeset_json.ex
* creating test/hello_web/controllers/url_controller_test.exs
* creating lib/hello_web/controllers/fallback_controller.ex
* creating lib/hello/urls/url.ex
* creating priv/repo/migrations/20221129120234_create_urls.exs
* creating lib/hello/urls.ex
* injecting lib/hello/urls.ex
* creating test/hello/urls_test.exs
* injecting test/hello/urls_test.exs
* creating test/support/fixtures/urls_fixtures.ex
* injecting test/support/fixtures/urls_fixtures.ex

我們將把這些檔案分成四種類別

  • 負責有效呈現 JSON 的 lib/hello_web 中的檔案
  • 負責定義我們的脈絡和將連結儲存在資料庫的邏輯的 lib/hello 中的檔案
  • 負責更新我們資料庫的 priv/repo/migrations 中的檔案
  • 用來測試我們的控制器和脈絡的 test 中的檔案

在這個指南中,我們將僅探討第一類型的檔案。如需進一步了解 Phoenix 如何儲存和管理資料,請參閱 Ecto 指南脈絡指南 以取得更多資訊。我們還有一個完整的章節專門針對測試。

最後,產生器會要求我們在 lib/hello_web/router.ex 中,將 /url 資源新增到我們的 :api 範圍。

scope "/api", HelloWeb do
  pipe_through :api
  resources "/urls", UrlController, except: [:new, :edit]
end

API 範圍使用 :api 管道,它將執行特定步驟,例如確保用戶端可以處理 JSON 回應。

接著,我們需要透過執行遷移來更新我們的存放庫。

mix ecto.migrate

嘗試 JSON API

在我們繼續變更這些檔案前,讓我們來看一下我們的 API 在命令列中的行為。

首先,我們需要啟動伺服器

mix phx.server

接下來,讓我們透過以下指令來進行煙霧測試,以檢查我們的 API 是否運作正常:

curl -i https://127.0.0.1:4000/api/urls

如果一切順利,我們應該會得到一個 200 回應

HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 11
content-type: application/json; charset=utf-8
date: Fri, 06 May 2022 21:22:42 GMT
server: Cowboy
x-request-id: Fuyg-wMl4S-hAfsAAAUk

{"data":[]}

我們沒有取得任何資料,因為我們尚未將任何資料填入資料庫。因此,讓我們來新增一些連結。

curl -iX POST https://127.0.0.1:4000/api/urls \
   -H 'Content-Type: application/json' \
   -d '{"url": {"link":"https://phoenix.dev.org.tw", "title":"Phoenix Framework"}}'

curl -iX POST https://127.0.0.1:4000/api/urls \
   -H 'Content-Type: application/json' \
   -d '{"url": {"link":"https://elixir.dev.org.tw", "title":"Elixir"}}'

現在,我們可以擷取所有連結。

curl -i https://127.0.0.1:4000/api/urls

或者,我們可以透過其 id 擷取連結。

curl -i https://127.0.0.1:4000/api/urls/1

接下來,我們可以透過以下指令來更新連結:

curl -iX PUT https://127.0.0.1:4000/api/urls/2 \
   -H 'Content-Type: application/json' \
   -d '{"url": {"title":"Elixir Programming Language"}}'

回應應為 200 พร้อมลิงก์ใหม่ในเนื้อหา

สุดท้ายนี้ เราต้องลองลบลิงก์ออกดู

curl -iX DELETE https://127.0.0.1:4000/api/urls/2 \
   -H 'Content-Type: application/json'

ระบบจะส่งคืน 204 เพื่อแสดงการลบลิงก์เสร็จสมบูรณ์

การแสดงผล JSON

เพื่อทำความเข้าใจเกี่ยวกับวิธีการแสดงผล JSON ให้เริ่มต้นด้วย index แอคชันจาก UrlController ตามที่กำหนดไว้ที่ lib/hello_web/controllers/url_controller.ex

  def index(conn, _params) do
    urls = Urls.list_urls()
    render(conn, :index, urls: urls)
  end

อย่างที่เห็น นี่ไม่ต่างจากวิธีที่ Phoenix แสดงผลเทมเพลต HTML เรียกใช้ render/3 โดยส่งการเชื่อมต่อ เทมเพลตที่ต้องการให้มุมมองแสดงผล (:index และข้อมูลที่ต้องการให้มุมมองเข้าถึงได้

โดยทั่วไป Phoenix จะใช้มุมมองใดมุมมองหนึ่งในการแสดงผลรูปแบบหนึ่งๆ เมื่อแสดงผล HTML เราจะใช้ UrlHTML ตอนนี้เราแสดงผล JSON เราจึงพบว่ามุมมอง UrlJSON วางเคียงไว้กับเทมเพลตที่ lib/hello_web/controllers/url_json.ex ลองเปิดดู

defmodule HelloWeb.UrlJSON do
  alias Hello.Urls.Url

  @doc """
  Renders a list of urls.
  """
  def index(%{urls: urls}) do
    %{data: for(url <- urls, do: data(url))}
  end

  @doc """
  Renders a single url.
  """
  def show(%{url: url}) do
    %{data: data(url)}
  end

  defp data(%Url{} = url) do
    %{
      id: url.id,
      link: url.link,
      title: url.title
    }
  end
end

มุมมองนี้ง่ายมาก ฟังก์ชัน index รับ URL ทั้งหมดและแปลงเป็นรายการของแผนที่ แผนที่เหล่านั้นจะถูกวางไว้ในคีย์ข้อมูลที่ราก เหมือนอย่างที่เราเห็นเมื่อเชื่อมต่อกับแอปพลิเคชันของเราจาก cURL กล่าวอีกนัยหนึ่งคือ มุมมอง JSON ของเราแปลงข้อมูลที่ซับซ้อนเป็นโครงสร้างข้อมูล Elixir ที่ง่ายๆ หลังจากที่เลเยอร์มุมมองของเรากลับมา Phoenix จะใช้ไลบรารี Jason เพื่อเข้ารหัส JSON และส่งไปยังไคลเอ็นต์

หากคุณสำรวจส่วนที่เหลือของคอนโทรลเลอร์ คุณจะพบว่าแอคชัน show คล้ายกับแอคชัน index สำหรับแอคชัน create update และ delete Phoenix ใช้ฟังก์ชันที่สำคัญอีกอย่างหนึ่งที่เรียกว่า "Action fallback"

Action fallback

Action fallback ช่วยให้เราสามารถรวมศูนย์รหัสการจัดการข้อผิดพลาดในปลั๊กอิน ซึ่งจะเรียกใช้เมื่อแอคชันของคอนโทรลเลอร์ไม่สามารถส่งคืนโครงสร้าง %Plug.Conn{} ปลั๊กอินเหล่านี้รับทั้ง conn ที่ส่งไปยังแอคชันของคอนโทรลเลอร์เดิมพร้อมกับค่าที่แอคชันส่งคืน

假設我們有一個 show 動作,使用 with 來擷取部落格文章,然後授權目前使用者檢視該篇部落格文章。在此範例中,我們可能會預期 fetch_post/1 回傳 {:error, :not_found} 如果文章不存在,還有 authorize_user/3 可能會回傳 {:error, :unauthorized} 如果使用者未經授權。我們可以使用 Phoenix 為每個新應用程式產生的 ErrorHTMLErrorJSON 檢視來適當地處理這些錯誤路徑。

defmodule HelloWeb.MyController do
  use Phoenix.Controller

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- fetch_post(id),
         :ok <- authorize_user(current_user, :view, post) do
      render(conn, :show, post: post)
    else
      {:error, :not_found} ->
        conn
        |> put_status(:not_found)
        |> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
        |> render(:"404")

      {:error, :unauthorized} ->
        conn
        |> put_status(403)
        |> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
        |> render(:"403")
    end
  end
end

現在想像一下,你可能需要為你的 API 處理的每個控制器和動作實作類似的邏輯。這將會導致很多重複。

相反地,我們可以定義一個模組外掛程式,它知道如何特別處理這些錯誤案例。由於控制器是模組外掛程式,讓我們把我們的外掛程式定義為控制器。

defmodule HelloWeb.MyFallbackController do
  use Phoenix.Controller

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(json: HelloWeb.ErrorJSON)
    |> render(:"404")
  end

  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(403)
    |> put_view(json: HelloWeb.ErrorJSON)
    |> render(:"403")
  end
end

然後,我們可以將我們的新控制器作為 action_fallback 參考,並從我們的 with 移除 else 區塊。

defmodule HelloWeb.MyController do
  use Phoenix.Controller

  action_fallback HelloWeb.MyFallbackController

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- fetch_post(id),
         :ok <- authorize_user(current_user, :view, post) do
      render(conn, :show, post: post)
    end
  end
end

只要 with 條件不相符,HelloWeb.MyFallbackController 就會收到原始 conn 以及動作的結果,並適當地回應。

FallbackController and ChangesetJSON

有了這個知識,我們可以探討 mix phx.gen.json 所產生的 FallbackControllerlib/hello_web/controllers/fallback_controller.ex)。特別是,它會處理一個子句(另一個子句是產生為範例)。

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(json: HelloWeb.ChangesetJSON)
    |> render(:error, changeset: changeset)
  end

此子句的目標是處理 HelloWeb.Urls 語境的 {:error, changeset} 回傳類型,並透過 ChangesetJSON 檢視將它們呈現為呈現錯誤。讓我們打開 lib/hello_web/controllers/changeset_json.ex 來了解更多。

defmodule HelloWeb.ChangesetJSON do
  @doc """
  Renders changeset errors.
  """
  def error(%{changeset: changeset}) do
    # When encoded, the changeset returns its errors
    # as a JSON object. So we just pass it forward.
    %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
  end
end

正如我們所見,它會將錯誤轉換為資料結構,並會呈現為 JSON。Changeset 是一個負責強制資料並驗證資料的資料結構。在我們的範例中,它定義於 Hello.Urls.Url.changeset/1。讓我們打開 lib/hello/urls/url.ex 並看看它的定義。

  @doc false
  def changeset(url, attrs) do
    url
    |> cast(attrs, [:link, :title])
    |> validate_required([:link, :title])
  end

正如你所見,changeset 需要提供 link 和 title。這表示我們可以試著張貼一個沒有 link 和 title 的 URL,並看看 API 如何回應。

curl -iX POST https://127.0.0.1:4000/api/urls \
   -H 'Content-Type: application/json' \
   -d '{"url": {}}'

{"errors": {"link": ["can't be blank"], "title": ["can't be blank"]}}

請隨時修改 changeset 函式,並觀察你的 API 如何運作。

僅 API 的應用程式

如果您想產生一個專門用於 API 的 Phoenix 應用程式,您可以呼叫 mix phx.new 時傳遞數個選項。讓我們看看在用於 REST API 的 Phoenix 應用程式中,不需要產生哪些 --no-* 旗標才能不產生不必要的架構。

從您的終端機中執行

mix help phx.new

輸出應該包含以下內容

  • --no-assets - equivalent to --no-esbuild and --no-tailwind
  • --no-dashboard - do not include Phoenix.LiveDashboard
  • --no-ecto - do not generate Ecto files
  • --no-esbuild - do not include esbuild dependencies and
    assets. We do not recommend setting this option, unless for API
    only applications, as doing so requires you to manually add and
    track JavaScript dependencies
  • --no-gettext - do not generate gettext files
  • --no-html - do not generate HTML views
  • --no-live - comment out LiveView socket setup in your Endpoint
    and assets/js/app.js. Automatically disabled if --no-html is given
  • --no-mailer - do not generate Swoosh mailer files
  • --no-tailwind - do not include tailwind dependencies and
    assets. The generated markup will still include Tailwind CSS
    classes, those are left-in as reference for the subsequent
    styling of your layout and components

對於任何用於 API 的 Phoenix 應用程式,--no-html 都是我們想要使用的一個明確選項,以便略過所有不必要的 HTML 架構。您也可以傳遞 --no-assets(如果您不需要任何資產管理位元)、--no-gettext(如果您不支援國際化)等等。

另外,請記住,沒有任何因素會阻止您同時擁有支援 REST API 和網路應用程式(HTML、資產、國際化和 socket)的後端。