部架構(gòu)揭秘:Builder與Formatter流水線(xiàn)如何一步步生成JSON:API文檔)
JaSerializer 內(nèi)部架構(gòu)揭秘Builder與Formatter流水線(xiàn)如何一步步生成JSON:API文檔【免費(fèi)下載鏈接】ja_serializerJSONAPI.org Serialization in Elixir.項(xiàng)目地址: https://gitcode.com/gh_mirrors/ja/ja_serializerJaSerializer 是一款Elixir JSON:API 序列化庫(kù)能把 Ecto 結(jié)構(gòu)體、Plug.Conn 等 Elixir 數(shù)據(jù)按照 JSON:API 規(guī)范 的格式轉(zhuǎn)換為標(biāo)準(zhǔn)的 JSON 文檔。今天我們來(lái)拆開(kāi)它的引擎艙看看一條數(shù)據(jù)從輸入到最終 JSON 字符串是如何在Builder 構(gòu)建器與Formatter 格式化器這條兩級(jí)流水線(xiàn)上一步步被加工出來(lái)的 一、整體架構(gòu)一條兩級(jí)流水線(xiàn)JaSerializer 的入口只有一個(gè)函數(shù)JaSerializer.format/4定義在 lib/ja_serializer.ex 中。它的邏輯極其簡(jiǎn)潔數(shù)據(jù) 序列化器 Conn 選項(xiàng) ↓ ① Builder構(gòu)建階段→ 把業(yè)務(wù)數(shù)據(jù)組裝成資源對(duì)象樹(shù) ↓ ② Formatter格式化階段→ 把資源對(duì)象樹(shù)渲染成最終 JSON 文檔這種先建模、后渲染的設(shè)計(jì)是理解整個(gè)項(xiàng)目的關(guān)鍵階段負(fù)責(zé)模塊輸入輸出構(gòu)建JaSerializer.BuilderEcto 結(jié)構(gòu)體、conn、序列化器嵌套的 Builder 結(jié)構(gòu)體TopLevel / ResourceObject…格式化JaSerializer.FormatterElixir 協(xié)議Builder 結(jié)構(gòu)體可直接編碼為 JSON 的 MapBuilder 只關(guān)心有哪些數(shù)據(jù)Formatter 只關(guān)心數(shù)據(jù)長(zhǎng)什么樣。職責(zé)分離讓兩者都可以獨(dú)立測(cè)試和擴(kuò)展。二、第一階段Builder 如何搭建資源對(duì)象樹(shù)JaSerializer.Builder.build/1只是一個(gè)轉(zhuǎn)發(fā)入口lib/ja_serializer/builder.ex真正的重活由Builder.TopLevel完成。2.1 TopLevel頂層組裝與數(shù)據(jù)預(yù)加載Builder.TopLevel.build/1lib/ja_serializer/builder/top_level.ex是整個(gè)構(gòu)建階段的總指揮它依次做四件事預(yù)加載數(shù)據(jù)調(diào)用序列化器的preload/3回調(diào)把即將序列化的記錄及其關(guān)聯(lián)數(shù)據(jù)一次性從數(shù)據(jù)庫(kù)取出來(lái)避免 N1 查詢(xún)構(gòu)建資源對(duì)象把每條記錄交給Builder.ResourceObject構(gòu)建構(gòu)建側(cè)載資源根據(jù)include選項(xiàng)調(diào)用Builder.Included收集需要側(cè)載的關(guān)聯(lián)資源補(bǔ)充分頁(yè)鏈接與元數(shù)據(jù)若傳入page選項(xiàng)或 Scrivener 分頁(yè)對(duì)象生成first/next/prev/last分頁(yè)鏈接并掛上meta元數(shù)據(jù)。最終產(chǎn)出一個(gè)TopLevel結(jié)構(gòu)體它對(duì)應(yīng) JSON:API 文檔的最外層data、included、links、meta、jsonapi。2.2 ResourceObject單個(gè)資源的五要素Builder.ResourceObject.build/1lib/ja_serializer/builder/resource_object.ex負(fù)責(zé)把一條記錄變成一個(gè)資源對(duì)象結(jié)構(gòu)體字段一一對(duì)應(yīng) JSON:API 規(guī)范中的資源對(duì)象字段id來(lái)自序列化器的id/2回調(diào)默認(rèn)取:id字段type默認(rèn)從模塊名推導(dǎo)如MyApp.ArticleSerializer→articlesattributes交給Builder.Attribute構(gòu)建relationships交給Builder.Relationship構(gòu)建links/meta來(lái)自序列化器的links/2與meta/2回調(diào)如果傳入的是列表它會(huì)遞歸地為每條記錄各構(gòu)建一個(gè)資源對(duì)象——這就是列表接口返回data: [...]數(shù)組的實(shí)現(xiàn)原理。2.3 Attribute 與 Relationship稀疏字段集與資源標(biāo)識(shí)符屬性構(gòu)建lib/ja_serializer/builder/attribute.ex很簡(jiǎn)單調(diào)用serializer.attributes/2拿到屬性 Map再根據(jù)fields選項(xiàng)做**稀疏字段集Sparse Fieldsets**過(guò)濾——這正是 JSON:API 中fields[articles]title,body參數(shù)的實(shí)現(xiàn)位置。關(guān)系構(gòu)建lib/ja_serializer/builder/relationship.ex更講究每條關(guān)系會(huì)構(gòu)建links如related、self鏈接路徑中的:id占位符會(huì)被Builder.Link替換為真實(shí)值只有當(dāng)關(guān)系值得引用時(shí)才生成ResourceIdentifier即{type, id}資源標(biāo)識(shí)符。它通過(guò)identifiers: :always或:when_included策略控制——例如:when_included表示只有該關(guān)系確實(shí)被側(cè)載時(shí)才在 relationships 里放標(biāo)識(shí)符從而保持響應(yīng)精簡(jiǎn)。2.4 Included側(cè)載資源與去重魔法Builder.Includedlib/ja_serializer/builder/included.ex是構(gòu)建階段最有含金量的模塊它遞歸地收集所有被include的關(guān)聯(lián)資源做兩件事遞歸下鉆對(duì)每條被 include 的關(guān)系用對(duì)應(yīng)的子序列化器繼續(xù)構(gòu)建 ResourceObject并沿include的點(diǎn)分路徑如comments.author繼續(xù)下鉆按{id, type}去重用 MapSet 記錄已出現(xiàn)的資源主鍵同一作者只會(huì)在included數(shù)組中出現(xiàn)一次其余地方僅保留資源標(biāo)識(shí)符——這正是 JSON:API 復(fù)合文檔Compound Document避免冗余的核心機(jī)制 ?三、第二階段Formatter 如何渲染最終 JSON構(gòu)建階段產(chǎn)出的是一棵結(jié)構(gòu)體樹(shù)還不能直接發(fā)給客戶(hù)端。JaSerializer.Formatter是一個(gè)Elixir 協(xié)議Protocol按類(lèi)型分派把每種 Builder 結(jié)構(gòu)體翻譯成 JSON:API 鍵名字符串鍵的 MapBuilder 類(lèi)型Formatter 實(shí)現(xiàn)產(chǎn)出TopLevel拼裝data、links、included、meta并注入jsonapi: {version: 1.0}完整文檔頂層ResourceObjectid、type、attributes、relationships、links、meta單個(gè)資源對(duì)象Attribute鍵名做格式轉(zhuǎn)換默認(rèn) dasherize值遞歸格式化{title: ...}Relationship組合data與links{author: {data: {...}}}幾個(gè)值得注意的細(xì)節(jié)鍵名格式可配置Formatter.Utils.format_key/1支持:dasherized默認(rèn)符合 JSON:API 1.0、:camel_cased、:underscored甚至自定義函數(shù)空值自動(dòng)剔除put_if_present/3工具函數(shù)保證為空的attributes、meta等鍵不會(huì)出現(xiàn)在輸出里讓 JSON 更干凈可擴(kuò)展的兜底實(shí)現(xiàn)協(xié)議對(duì)Any類(lèi)型有兜底實(shí)現(xiàn)直接透?jìng)鲗?duì)Decimal、Ecto.DateTime等 Ecto 類(lèi)型有專(zhuān)屬實(shí)現(xiàn)遇到自定義類(lèi)型如自定義金額結(jié)構(gòu)時(shí)只需為它defimpl JaSerializer.Formatter即可無(wú)縫接入無(wú)需改動(dòng)核心代碼 四、源碼地圖從哪些文件讀起如果你想動(dòng)手翻源碼建議按這條主線(xiàn)閱讀入口lib/ja_serializer.ex ——format/4兩級(jí)流水線(xiàn)總覽構(gòu)建層lib/ja_serializer/builder/top_level.ex → resource_object.ex → included.ex格式化層lib/ja_serializer/formatter.ex協(xié)議定義與分派周邊能力lib/ja_serializer/phoenix_view.exPhoenix 集成、lib/ja_serializer/deserializer.ex請(qǐng)求參數(shù)反序列化、lib/ja_serializer/params.ex寫(xiě)入?yún)?shù)解析行為與 DSLlib/ja_serializer/serializer.ex回調(diào)契約、lib/ja_serializer/dsl.exattributes、has_many等宏分頁(yè)構(gòu)建器lib/ja_serializer/builder/pagination_links.ex 與 scrivener_links.ex配套的測(cè)試也值得一讀test/ja_serializer/json_api_spec/ 下的測(cè)試直接以 JSON:API 規(guī)范為基準(zhǔn)是理解每個(gè)模塊行為的最佳示例庫(kù)。五、總結(jié)這條流水線(xiàn)教會(huì)我們的設(shè)計(jì)JaSerializer 的架構(gòu)可以濃縮為三句話(huà)Builder 建模用一組結(jié)構(gòu)體把數(shù)據(jù)長(zhǎng)什么樣抽象成 JSON:API 的資源對(duì)象樹(shù)屏蔽數(shù)據(jù)庫(kù)細(xì)節(jié)Formatter 渲染用 Elixir 協(xié)議做類(lèi)型分派把結(jié)構(gòu)體樹(shù)機(jī)械地翻譯成規(guī)范 JSON并保留擴(kuò)展點(diǎn)include 驅(qū)動(dòng)側(cè)載由請(qǐng)求參數(shù)決定加載深度配合{id, type}去重生成標(biāo)準(zhǔn)的復(fù)合文檔。理解了 Builder 與 Formatter 這條流水線(xiàn)你不僅能看懂 JaSerializer 的每一行代碼也能把先建模、后渲染 協(xié)議擴(kuò)展的思路遷移到自己的 Elixir 項(xiàng)目中 【免費(fèi)下載鏈接】ja_serializerJSONAPI.org Serialization in Elixir.項(xiàng)目地址: https://gitcode.com/gh_mirrors/ja/ja_serializer創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考