如何在视图处理后运行Plug管道?统一处理JSON API数据转换
当然可以在视图处理完成后统一应用转换逻辑,不用在每个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_body是iodata(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




