查看源代码 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
我们将把这些文件分成四类
- 位于
lib/hello_web
中的文件负责有效地渲染 JSON - 位于
lib/hello
中的文件负责定义我们的上下文和逻辑,以将链接持久化到数据库 - 位于
priv/repo/migrations
中的文件负责更新我们的数据库 - 位于
test
中的文件用于测试我们的控制器和上下文
在本指南中,我们将只探索第一类文件。要了解有关 Phoenix 如何存储和管理数据的更多信息,请查看 Ecto 指南 和 上下文指南 以了解更多信息。我们还专门有一节内容介绍测试。
最后,生成器要求我们将 /url
资源添加到 lib/hello_web/router.ex
中的 :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 http://localhost: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 http://localhost:4000/api/urls \
-H 'Content-Type: application/json' \
-d '{"url": {"link":"https://phoenix.erlang.ac.cn", "title":"Phoenix Framework"}}'
curl -iX POST http://localhost:4000/api/urls \
-H 'Content-Type: application/json' \
-d '{"url": {"link":"https://elixir.erlang.ac.cn", "title":"Elixir"}}'
现在我们可以检索所有链接
curl -i http://localhost:4000/api/urls
或者我们也可以只通过其 id
检索一个链接。
curl -i http://localhost:4000/api/urls/1
接下来,我们可以使用以下命令更新链接
curl -iX PUT http://localhost:4000/api/urls/2 \
-H 'Content-Type: application/json' \
-d '{"url": {"title":"Elixir Programming Language"}}'
响应应该是 200
,并在正文中包含更新的链接。
最后,我们需要尝试删除链接。
curl -iX DELETE http://localhost:4000/api/urls/2 \
-H 'Content-Type: application/json'
应该返回一个 204
响应,表明已成功删除链接。
渲染 JSON
要了解如何渲染 JSON,让我们从 lib/hello_web/controllers/url_controller.ex
中定义的 UrlController
中的 index
操作开始。
def index(conn, _params) do
urls = Urls.list_urls()
render(conn, :index, urls: urls)
end
正如我们所见,这与 Phoenix 如何渲染 HTML 模板没有区别。我们调用 render/3
,传递连接、我们希望视图渲染的模板(:index
)以及我们希望提供给视图的数据。
Phoenix 通常对每种渲染格式使用一个视图。当渲染 HTML 时,我们会使用 UrlHTML
。现在我们正在渲染 JSON,我们会在 lib/hello_web/controllers/url_json.ex
中与模板位于同一位置找到一个 UrlJSON
视图。让我们打开它。
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,并将它们转换为地图列表。这些地图放置在根目录下的 data 键内,就像我们从 cURL
与应用程序交互时看到的那样。换句话说,我们的 JSON 视图将我们复杂的数据转换为简单 Elixir 数据结构。一旦我们的视图层返回,Phoenix 将使用 Jason
库来编码 JSON 并将响应发送到客户端。
如果您探索其余的控制器,您将了解到 show
操作类似于 index
操作。对于 create
、update
和 delete
操作,Phoenix 使用了另一个重要的功能,称为“操作回退”。
操作回退
操作回退允许我们集中在插头的错误处理代码中,这些插头在控制器操作无法返回 %Plug.Conn{}
结构时调用。这些插头会接收最初传递给控制器操作的 conn
,以及操作的返回值。
假设我们有一个 show
操作,该操作使用 with
来获取博客文章,然后授权当前用户查看该博客文章。在这个例子中,我们可能希望 fetch_post/1
在文章未找到时返回 {:error, :not_found}
,而 authorize_user/3
在用户未授权时可能会返回 {:error, :unauthorized}
。我们可以使用我们的 ErrorHTML
和 ErrorJSON
视图(它们是由 Phoenix 为每个新应用程序生成的)来相应地处理这些错误路径。
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 和 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。更改集是一个负责转换和验证数据的结构。在我们的例子中,它在 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
正如您所见,更改集要求提供链接和标题。这意味着我们可以尝试发布没有链接和标题的 URL,并查看我们的 API 如何响应。
curl -iX POST http://localhost: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
--no-html
是我们创建任何用于 API 的 Phoenix 应用程序时要使用的显而易见的标志,以便省略所有不必要的 HTML 脚手架。您也可以传递 --no-assets
,如果您不希望使用任何资产管理功能,--no-gettext
,如果您不支持国际化,等等。
还要记住,没有什么能够阻止您拥有同时支持 REST API 和 Web 应用程序(HTML、资产、国际化和套接字)的后端。