查看源代码 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

我们将把这些文件分成四类

  • 位于 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 操作。对于 createupdatedelete 操作,Phoenix 使用了另一个重要的功能,称为“操作回退”。

操作回退

操作回退允许我们集中在插头的错误处理代码中,这些插头在控制器操作无法返回 %Plug.Conn{} 结构时调用。这些插头会接收最初传递给控制器操作的 conn,以及操作的返回值。

假设我们有一个 show 操作,该操作使用 with 来获取博客文章,然后授权当前用户查看该博客文章。在这个例子中,我们可能希望 fetch_post/1 在文章未找到时返回 {:error, :not_found},而 authorize_user/3 在用户未授权时可能会返回 {:error, :unauthorized}。我们可以使用我们的 ErrorHTMLErrorJSON 视图(它们是由 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 生成的 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。更改集是一个负责转换和验证数据的结构。在我们的例子中,它在 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、资产、国际化和套接字)的后端。