
wp-calypso 通知客户端数据模型解析API 耦合、轮询竞态与 Redux 状态增强设计【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso本文以 wp-calypso 仓库中apps/notifications/src/doc/data-model.md为骨架深入剖析通知面板Notifications Panel的数据模型它如何直接耦合 WordPress.com 通知 API 的返回结构如何通过数据增强Augmentation机制化解本地操作与后台轮询之间的竞态条件以及hiddenNoteIds、noteLikes、noteApprovals等本地状态在 Redux 中的落地实现。读完本文你将掌握通知客户端服务端数据 本地覆盖这一双流状态模型的完整设计思路并能直接定位到对应的 reducer、action、selector 与 thunk 源码。一、数据模型总览直接耦合 API 响应通知客户端目前没有在应用层建立一套与 API 解耦的独立领域模型而是直接将 WordPress.com 通知 API 返回的数据与内部数据结构耦合在一起data-model.md 开篇即明确这一点the app is currently built by directly coupling the data from the WordPress.com API for notifications with the internal data structures。在此基础上应用额外维护了一套数据增强系统data augmentation system专门用来处理本地状态同步与远程轮询之间天然存在的竞态问题。换句话说数据本体来自 API 的原始 note 对象直接被塞进 Redux store数据增强由应用自己维护的、叠加在原始数据之上的局部状态隐藏、点赞、审核等用于在异步网络请求往返期间稳定 UI。来自 API 的 note 结构API 返回的单条通知note对象包含以下字段const note { id: [number] // note id as a number , type: [string] // type of notification, such as comment or like_milestone_achievement , read: [1 or 0] // boolean value representing whether or not the note has already been read , noticion: [unicode character] // mapping to icon in the noticon font corresponding to note category type , timestamp: [ISO8601 string] // time note was created , title: [string] // brief title to display above note , icon: [string URL] // URL for image to show as main notification icon , url: [string URL] // naturally related link to note: a post, a comment, a sign-up page, etc… , subject // list of blocks for header , body // list of blocks for body , meta // related note references: posts, sites, comments, etc… }各字段要点说明字段类型语义idnumber通知的唯一数字 ID也是 Redux 状态中所有以 ID 为键的映射如hiddenNoteIds、noteLikes的核心索引typestring通知类别例如comment、like_milestone_achievement决定通知如何渲染、对应哪些操作按钮read1 或 0该通知是否已被阅读noticionunicode 字符映射到 noticon 字体中对应通知类别图标的字符timestampISO8601 字符串通知创建时间titlestring显示在通知上方的简短标题icon字符串 URL作为主通知图标展示的图片 URLurl字符串 URL与通知自然相关的链接一篇文章、一条评论、一个注册页等subjectblock 数组渲染在头部的 block 列表bodyblock 数组渲染在正文的 block 列表meta对象关联的通知引用posts、sites、comments 等其中body/subject的 block 数组以及meta的具体渲染细节可参见同目录下的 note-rendering.mdblock 的ranges索引如何驱动富文本格式化与 getting-notes.md数据如何从/me/notifications接口与 Pinghub 通道进入应用。二、数据增强Augmentation为什么需要它轮询与本地修改的竞态只要通知面板处于可见状态后台轮询就始终在运行Polling is always active in the background while the app is visible。这带来一个必然结果当用户在应用内做出修改比如删除一条通知、点赞一条评论时这个本地修改与在我们修改之前就已经开始在网络上传送的旧更新之间会形成竞态条件。data-model.md 用一张时序图展示了这一过程例如删除一条通知时较早发出的轮询请求可能携带该通知仍然存在的数据返回导致 UI 上出现闪烁——通知消失、重新出现、然后又突然消失。当前策略由于尚未实现理想的网络锁network-lock即在既有请求成功或失败之前阻止某些更新写入本地数据应用退而求其次采用一套有状态的、彼此独立的本地数据存储来分别管辖各自类型的数据点赞、删除隐藏等。每个 store 只管自己那一类状态从而在轮询数据迟到时本地覆盖仍然生效避免 UI 抖动。未来演进方向data-model.md 明确写道随着持续重构期望能建立网络锁机制来从根本上防止竞态——在此之前当前的状态化、独立 store方案是过渡性的务实设计。这也是阅读本模块代码时理解许多 reducer 为何自成一体、互不耦合的关键背景。三、隐藏通知Hidden Notes标记与撤销当通知被标记为spam垃圾或trash回收站时它应当从应用中消失但同时如果用户想要撤销这个破坏性操作它又应该立即重新出现。因此应用维护了一个hidden notes列表保存那些不应被渲染的 note id当撤销 trash 或 spam 操作时只需把对应 id 从该列表中移除即可。state.notes.hiddenNoteIds [ id1, id2, id3 /*...*/ ]; getIsNoteHidden( store.getState(), noteId );reducer 实现data-model.md 中提到该列表由state/notes/reducers.js#hiddenNoteIdsreducer 维护。在当前仓库中对应实现位于 apps/notifications/src/panel/state/notes/reducer.jsexport const hiddenNoteIds ( state {}, { type, noteId } ) { if ( types.TRASH_NOTE type || types.SPAM_NOTE type ) { return { ...state, [ noteId ]: true }; } if ( types.UNDO_ACTION type ) { const nextState { ...state }; delete nextState[ noteId ]; return nextState; } return state; };可以看出它是一个以 noteId 为键、布尔值为值的普通对象收到TRASH_NOTE或SPAM_NOTE时把 id 置为true收到UNDO_ACTION时删除对应键。对应的 action 创建函数trashNote、spamNote定义在 apps/notifications/src/panel/state/notes/actions.js而真正触发隐藏的 thunk 是 trash-note.js 与 spam-note.js。值得注意的细节trashNote/spamNotethunk 支持立即执行与延迟执行两种模式immediately参数。延迟模式下会调用restClient.global.updateUndoBar( trash, note )弹出撤销条——这正是隐藏 → 可撤销 → 立即重现交互的支撑立即模式下则直接调用wpcom()REST 接口删除评论。无论哪种模式最终都会 dispatch 对应的本地 action把 noteId 写入hiddenNoteIds。selector 与消费方selectorget-is-note-hidden.js 直接判断notesState.hiddenNoteIds[ noteId ] trueget-hidden-note-ids.js 返回整个隐藏 id 集合。消费方任何使用可见通知列表的地方都必须过滤掉该列表中的 id。data-model.md 列举了三处关键场景实际渲染在列表中的通知决定高亮落在哪里键盘在通知列表中的导航查找下一条通知。源码中的落实例如 apps/notifications/src/app/note-list/index.tsx 通过tab.notes.filter( ( note ) hiddenNoteIds[ note.id ] ! true )过滤可见列表apps/notifications/src/app/note-list/hooks.ts 在键盘导航、查找下一条通知时同样逐一校验hiddenNoteIds[ id ] ! true。四、点赞通知Liked Notes本地点赞覆盖当通知内部的评论或文章被点赞like或取消点赞unlike时这个本地变更应当在点赞网络请求返回之前持续覆盖外部轮询带来的更新避免点赞状态被迟到的轮询数据闪回。这些本地点赞被维护在 Redux 状态中store.dispatch( actions.notes.likeNote( noteId, isLiked ) ); getIsNoteLiked( store.getState(), note );与隐藏通知的差异data-model.md 特别强调了两点差异没有对应的撤销机制no corresponding undo as with the hidden notes——点赞不存在 spam/trash 那样的可撤销交互如果一条评论/文章在点赞之后又要取消点赞只需再次调用同一函数并传入新状态即可但该函数并不能消除快速连续点赞/取消点赞时产生的竞态This function does not eliminate the race conditions when quickly liking and unliking in sequence。reducer 与 thunk 实现点赞状态由 reducer.js 中的noteLikesreducer 维护export const noteLikes ( state {}, { type, noteId, isLiked } ) { if ( types.LIKE_NOTE type ) { return { ...state, [ noteId ]: isLiked }; } if ( types.RESET_LOCAL_LIKE type ) { const nextState { ...state }; delete nextState[ noteId ]; return nextState; } return state; };LIKE_NOTE写入本地覆盖RESET_LOCAL_LIKE在轮询数据可以安全接管时清除本地覆盖对应 actions.js 中的likeNote与resetLocalLike后者注释明确说明本地点赞覆盖的目的是防止轮询操作带来的过期数据把点赞状态错误地闪回。实际的点赞请求走 set-like-status.jsconst setLikeStatus ( noteId, siteId, postId, commentId, isLiked, restClient ) async ( dispatch ) { const type commentId ? comment : post; dispatch( likeNote( noteId, isLiked ) ); // ...bumpStat / recordTracksEvent... const entityPath type comment ? comments/${ commentId } : posts/${ postId }; if ( isLiked ) { await wpcom().req.post( /sites/${ siteId }/${ entityPath }/likes/new ); } else { await wpcom().req.del( /sites/${ siteId }/${ entityPath }/likes/mine/delete ); } // getNote() updates the redux store with a fresh object from the API restClient.getNote( noteId ); };关键顺序dispatch( likeNote(...) )在发起网络请求之前执行——这正是本地立即生效、覆盖轮询的时序保证请求完成后通过restClient.getNote( noteId )拉取最新对象并配合resetLocalLike让本地覆盖让位于权威数据。selector 实现get-is-note-liked.js 展示了本地覆盖优先、API 数据兜底的完整读取逻辑export const getIsNoteLiked ( notesState, note ) { const noteLikes notesState.noteLikes; if ( noteLikes.hasOwnProperty( note.id ) ) { return noteLikes[ note.id ]; } const actionMeta getActions( note ); const likeProperty note.meta.ids.comment ? like-comment : like-post; return actionMeta[ likeProperty ] ?? false; };即本地有覆盖就读本地否则回落到从 note 的meta/action 信息中推导出的服务端状态。五、审核通知Approved Notes与点赞同构的机制审核批准/不批准评论的机制与点赞完全同构只是函数不同store.dispatch( actions.notes.approveNote( noteId, isApproved ) ); getIsNoteApproved( store.getState(), note );对应实现reducerreducer.js 中的noteApprovals处理APPROVE_NOTE写入[ noteId ]: isApproved与RESET_LOCAL_APPROVAL删除键actionactions.js 中的approveNote与带注释的resetLocalApprovalthunkset-approve-status.js 通过wpcom().site( siteId ).comment( commentId ).update( { status: isApproved ? approved : unapproved } )调用 REST API请求前 dispatchapproveNote( noteId, isApproved )请求回调中调用restClient.getNote( noteId )刷新权威数据selectorget-is-note-approved.js。需要注意的是data-model.md 中写的是state/notes/reducers.js而仓库实际文件名为单数形式的 apps/notifications/src/panel/state/notes/reducer.js阅读源码时请以实际路径为准。六、从文档到源码状态机的全景映射将>export default combineReducers( { allNotes, // API 返回的全部 note以 id 为键 filteredNoteIds, // 各过滤标签页下的有序 id 列表 hiddenNoteIds, // 隐藏通知 id 集合 noteApprovals, // 本地审核覆盖 noteLikes, // 本地点赞覆盖 noteReads, // 已读标记 filteredNoteReads, // 当前过滤视图下的已读 id } );allNotes与hiddenNoteIds、noteLikes、noteApprovals的分离正是服务端数据直接耦合 本地增强覆盖这一文档主旨的代码落地API 数据进allNotes本地竞态处理全在三个独立的覆盖型 reducer 里。七、延伸阅读getting-notes.md通知数据如何通过/me/notificationsAPI、idnote_hash双流网络优化以及 Pinghub WebSocket 通道进入应用是理解轮询为什么始终活跃的上游文档public-api.md通知面板对外暴露的 props 与消息协议其中isVisible/isShowing的轮询节流策略直接决定了竞态窗口的大小note-rendering.mdnote 的body/subjectblock 数组如何渲染meta中的 posts/sites/comments 引用如何被消费模块总览见 apps/notifications/README.md构建、iframe 通信togglePanel、iFrameReady、renderAllSeen、widescreen、render等完整说明均在其中。综上wp-calypso 通知客户端的数据模型本质上是一套权威数据来自 API、本地增强兜底竞态的双层结构allNotes直接耦合接口返回hiddenNoteIds、noteLikes、noteApprovals三个独立 reducer 在请求往返期间维持 UI 稳定。理解这个模型是后续为通知面板新增任何乐观更新类交互回复、关注、编辑等时避免 UI 闪烁问题的前提。【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考