You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在视图处理后运行Plug管道?统一处理JSON API数据转换

如何在Phoenix视图渲染后统一应用响应转换

当然可以在视图处理完成后统一应用转换逻辑,不用在每个render函数里重复写代码!你之前尝试的register_before_send思路方向是对的,但没处理好conn.resp_body的格式问题,另外还有更贴合Phoenix生态的方案,我给你详细说下:

方案一:用Phoenix视图的after_render钩子(推荐)

这是最优雅的方式,因为它直接在视图渲染的生命周期里做处理,不需要额外的JSON编解码开销。

Phoenix的视图提供了after_render回调,会在每个视图的render函数执行完成后、数据被编码成JSON之前触发。你可以在项目的基础视图(比如MyAppWeb.View)里定义这个回调,所有继承它的业务视图都会自动应用转换逻辑:

defmodule MyAppWeb.View do
  use Phoenix.View, root: "lib/my_app_web/templates"
  # 导入你需要的转换函数
  import ApiHelpers, only: [add_data_property: 1]
  import ProperCase, only: [to_camel_case: 1]

  @impl true
  def after_render(_template, rendered_data, _assigns) do
    # 在这里统一应用你的两次转换
    rendered_data
    |> add_data_property()
    |> to_camel_case()
  end
end

之后你的业务视图就可以简化成只返回原始数据,不用再重复写转换代码了:

def render("app.json", %{app: app}) do
  app # 直接返回原始的app数据即可
end

这个方案的优势在于:

  • 直接操作Elixir数据结构(map/list),不需要把JSON字符串解码再编码,性能更好
  • 逻辑集中在基础视图,维护起来更方便
  • 完全贴合Phoenix的视图生命周期设计

方案二:修复register_before_send的实现

如果你更倾向于在Plug层处理(比如需要对所有API响应统一处理,不管视图逻辑),那可以修正你之前的代码。问题出在conn.resp_bodyiodata(Elixir用于高效构建字符串的列表结构),不是普通的二进制字符串,所以需要先转换格式,再处理JSON:

defmodule MyAppWeb.Plugs.ResponseFormatter do
  import Plug.Conn

  def call(conn, _opts) do
    register_before_send(conn, fn conn ->
      # 先把iodata转换成二进制字符串
      body = IO.iodata_to_binary(conn.resp_body)
      
      # 尝试解码JSON并应用转换,非JSON响应直接跳过
      with {:ok, data} <- Jason.decode(body) do
        transformed_data =
          data
          |> ApiHelpers.add_data_property()
          |> ProperCase.to_camel_case()

        # 重新编码成JSON并更新响应体
        new_body = Jason.encode!(transformed_data)
        resp(conn, conn.status, new_body)
      else
        _error -> conn
      end
    end)
  end
end

然后把这个Plug加到你的API管道里(比如在lib/my_app_web/router.ex的api管道中):

pipeline :api do
  plug :accepts, ["json"]
  plug MyAppWeb.Plugs.ResponseFormatter # 加上这一行
end

这个方案适合需要跨视图、跨控制器的全局响应处理,但因为多了一次JSON编解码,性能会比after_render略差一点。

为什么之前的register_before_send会失败?

你提到conn.resp_body是列表,这是因为Phoenix为了性能优化,会把响应体以iodata的形式存储(比如["hello", " ", "world"]这样的列表),而不是直接拼接成字符串。所以必须先用IO.iodata_to_binary/1把它转换成普通的二进制字符串,才能进行JSON解码操作。

内容的提问来源于stack exchange,提问作者ThreeAccents

火山引擎 最新活动