檢視原始碼 JSON 和 API
要求:本指南預設您已完成 控制器指南。
您也可以使用 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 為每個新應用程式產生的 ErrorHTML
跟 ErrorJSON
檢視來適當地處理這些錯誤路徑。
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
所產生的 FallbackController
(lib/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)的後端。