wp-calypso 数据组件深度解析:用 QuerySiteDomains 声明式拉取站点域名列表
【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso
<QuerySiteDomains />是 wp-calypso(WordPress.com 的 JavaScript 前端)中负责"站点域名列表"这一数据请求的 React 组件:只要把它渲染到页面上并传入siteId,它就会自动发起对sites/%s/domains接口的请求,并把结果写入 Redux 状态树。本文以 client/components/data/query-site-domains/README.md 为骨架,结合其组件源码、Redux action / selector / reducer 以及真实业务调用场景,完整还原这一"数据查询组件"从渲染到状态落地的整条链路,读完即可在自己的业务页面中正确使用它,并理解其去重与防抖设计。
一、组件定位:wp-calypso 的"数据查询组件"模式
在 wp-calypso 中,client/components/data/目录下聚集了一类特殊的组件:它们不渲染任何 DOM,唯一的职责是替页面发起数据请求。QuerySiteDomains正是其中的一员,README 的第一句就点明了它的使命:
<QuerySiteDomains />is a React component used in managing network requests for sites/%s/domains.
也就是说,它管理的是对 WordPress.com REST APIsites/{siteId}/domains这一端点的请求。与之配套的还有query-site、query-site-purchases等兄弟组件,共同构成 Calypso"谁需要数据谁就挂一个 Query 组件"的声明式数据获取范式:业务组件不再自己写fetch,而是把数据需求声明出来,由 Query 组件集中处理请求生命周期。
二、基本用法:渲染组件、传入 siteId
README 给出的使用方式非常简洁:渲染组件并传入siteId,组件不接受任何 children,也不会向页面渲染任何元素。文档中的示例(Class 组件风格)如下:
import QuerySiteDomains from 'calypso/components/data/query-site-domains'; class MyComponent extends React.Component { render() { const { site, domains } = this.props; return ( <div> <QuerySiteDomains siteId={ site.ID } /> <ul> { domains.map( ( domain ) => { return <li>{ domain.domain }</li>; } ) } </ul> </div> ); } }这里有一个容易被忽略的关键点:<QuerySiteDomains />并不负责把数据"吐"给子组件。它只是把请求触发出去,数据最终存放在 Redux store 中;页面通过connect或 selector(如getDomainsBySiteId)从 store 里读取domains再自行渲染。所以示例中domains来自this.props,而不是来自组件的 children 或渲染输出。
基于同一模式,在现代 Hooks 风格下可以这样组合使用:
import { useDispatch, useSelector } from 'react-redux'; import QuerySiteDomains from 'calypso/components/data/query-site-domains'; import { getDomainsBySiteId } from 'calypso/state/sites/domains/selectors'; function MyDomainsList( { siteId } ) { const domains = useSelector( ( state ) => getDomainsBySiteId( state, siteId ) ); return ( <div> <QuerySiteDomains siteId={ siteId } /> <ul> { domains.map( ( domain ) => ( <li key={ domain.domain }>{ domain.domain }</li> ) ) } </ul> </div> ); }三、组件源码拆解:useEffect 驱动的请求触发
只看 README 无法了解组件的内部行为,真正实现位于同目录下的 client/components/data/query-site-domains/index.jsx。完整源码只有 24 行,却包含了几个值得注意的设计:
import PropTypes from 'prop-types'; import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { fetchSiteDomains } from 'calypso/state/sites/domains/actions'; import { isRequestingSiteDomains } from 'calypso/state/sites/domains/selectors'; const request = ( siteId ) => ( dispatch, getState ) => { if ( siteId && ! isRequestingSiteDomains( getState(), siteId ) ) { dispatch( fetchSiteDomains( siteId ) ); } }; export default function QuerySiteDomains( { siteId } ) { const dispatch = useDispatch(); useEffect( () => { dispatch( request( siteId ) ); }, [ dispatch, siteId ] ); return null; } QuerySiteDomains.propTypes = { siteId: PropTypes.number.isRequired };可以提炼出四个要点:
- 函数式组件 + Hooks:组件用
useDispatch拿到 Redux 的dispatch,在useEffect中触发请求,依赖数组为[ dispatch, siteId ]——当siteId变化时,旧站点的请求会被清理、新站点会被重新请求。 - 返回
null:组件不渲染任何 DOM,与 README 中"does not render any elements to the page"的描述完全一致。 - 请求去重:
request是一个 thunk,它在dispatch前先通过isRequestingSiteDomains( getState(), siteId )检查该站点是否已经在请求中;如果已有一个进行中的请求,就跳过,避免重复打接口。这是"数据查询组件"模式的防重设计。 - 空值保护:
siteId为假值(undefined/null/0)时直接跳过请求,因此可以放心地在站点尚未加载完成的场景下无条件渲染该组件。propTypes声明siteId为必填的 number 类型。
四、底层数据流:从 thunk 到 REST API 再到 Redux
组件 dispatch 的fetchSiteDomains定义在 client/state/sites/domains/actions.js,它是整条链路的核心。简化后的实现如下:
export function fetchSiteDomains( siteId ) { return ( dispatch ) => { dispatch( domainsRequestAction( siteId ) ); return wpcom.req .get( `/sites/${ siteId }/domains`, { apiVersion: '1.2' } ) .then( ( data ) => { const { domains = [], error, message } = data; if ( error ) { throw new Error( message ); } dispatch( domainsRequestSuccessAction( siteId ) ); dispatch( domainsReceiveAction( siteId, domains ) ); } ) .catch( ( error ) => { const message = error instanceof Error ? error.message : translate( 'There was a problem fetching site domains. Please try again later or contact support.' ); dispatch( domainsRequestFailureAction( siteId, message ) ); } ); }; }一次完整请求的生命周期由四类 action 标记(类型定义见 client/state/action-types.js 中的SITE_DOMAINS_*常量):
| Action | 触发时机 | 含义 |
|---|---|---|
SITE_DOMAINS_REQUEST | 请求发出前 | 将该站点标记为"请求中" |
SITE_DOMAINS_REQUEST_SUCCESS | 接口成功返回 | 清除"请求中"标记 |
SITE_DOMAINS_RECEIVE | 成功返回且无 error 字段 | 把组装好的域名数组写入 state |
SITE_DOMAINS_REQUEST_FAILURE | 请求失败或响应带 error | 记录错误信息 |
几个实现细节值得注意:
- API 版本:请求使用
apiVersion: '1.2',这是 wpcom.js(calypso/lib/wp)对sites/{siteId}/domains端点约定的版本参数。 - 业务错误处理:即使 HTTP 请求成功,响应体中若带有
error字段(如{ error: ..., message: ... }),同样会被视为失败并抛出new Error( message )。 - 错误信息本地化:兜底错误文案通过
i18n-calypso的translate()提供,符合 Calypso 全局的国际化要求。 - 响应数据组装:
domainsReceiveAction内部对每个原始域名调用createSiteDomainObject(见下文)完成字段归一化后再入库。
从代码结构看,这一套"REQUEST → SUCCESS/FAILURE/RECEIVE"的动作组,是整个state/sites/domains模块的基础:同文件中的setPrimaryDomain在切换主域名后也会调用fetchSiteDomains( siteId )刷新列表(见 actions.js 中的setPrimaryDomain),说明该 thunk 既是 Query 组件的数据入口,也是业务操作后刷新数据的公共手段。
五、状态落地:reducer 与 selector 的配合
5.1 Redux 状态结构
请求结果被 client/state/sites/domains/reducer.js 中的combineReducers组织为五个切片:
export default combineReducers( { errors, items, requesting, updatingPrivacy, updatingPrimaryDomain, } );与本文主题直接相关的是items与requesting两个切片:
items:以siteId为 key、域名数组为 value 的映射。SITE_DOMAINS_RECEIVE时通过Object.assign( {}, state, { [ siteId ]: action.domains } )不可变地写入;它还用withSchemaValidation包了一层,配合 client/state/sites/domains/schema.js 做持久化时的数据校验。此外DOMAIN_PRIVACY_ENABLE_SUCCESS、DOMAIN_DNSSEC_ENABLE_SUCCESS、DOMAIN_DETAILS_RECEIVE等业务 action 也会通过modifySiteDomainObjectImmutable局部更新某个域名对象,而无需重新请求。requesting:以siteId为 key 的布尔标记。SITE_DOMAINS_REQUEST置为true,SITE_DOMAINS_REQUEST_SUCCESS/SITE_DOMAINS_REQUEST_FAILURE置为false。这正是组件去重逻辑依赖的数据源。errors:以siteId为 key 的错误信息,SITE_DOMAINS_REQUEST_FAILURE时写入,下一次请求开始时清空。
5.2 常用 selector
client/state/sites/domains/selectors.js 提供了读取这些状态的官方入口:
getDomainsBySiteId( state, siteId ):返回某站点的域名数组;siteId为空或该站点尚未加载时返回EMPTY_SITE_DOMAINS(一个Object.freeze( [] )的共享空数组,保证多次调用的返回值引用相等,避免引起不必要的重渲染)。getDomainsBySite( state, site ):接受 site 对象(取site.ID)的便捷封装。getWpComDomainBySiteId( state, siteId ):从列表中挑出 WordPress.com 自带域名(isWPCOMDomain或isWpcomStagingDomain)。hasLoadedSiteDomains( state, siteId ):判断该站点的域名列表是否已加载完成(items[siteId]是否存在)。isRequestingSiteDomains( state, siteId ):判断是否正在请求,Query 组件内部即使用它做请求去重。
六、数据归一化:createSiteDomainObject 组装 ResponseDomain
接口返回的域名对象是 snake_case 的原始结构,而业务代码消费的是 camelCase 的"领域对象"。这一转换由 client/state/sites/domains/assembler.js 的createSiteDomainObject完成,domainsReceiveAction会把它逐一应用到响应数组上。
该函数将原始字段映射为统一形态的ResponseDomain对象(类型定义参见 client/lib/domains/types.ts),覆盖了域名管理所需的几乎所有维度,例如:
- 基础标识:
domain/name(均为域名本身)、blogId、type(通过getDomainType判定,如'mapping'、'wpcom'、'transfer'等)、isSubdomain、isPrimary(来自primary_domain); - 生命周期:
registrationDate、expiry、expirySoon、autoRenewing、renewableUntil、redeemableUntil; - 所有权与权限:
currentUserIsOwner、currentUserCanManage、canSetAsPrimary、canManageDnsRecords、canUpdateContactInfo及对应的cannot*Reason字段; - 隐私与合规:
privateDomain、privacyAvailable、contactInfoDisclosed、gdprConsentStatus(经getGdprConsentStatus计算); - 技术状态:
isDnssecEnabled/dnssecRecords(经assembleDnssecRecords拆出dnskey与dsData)、hasWpcomNameservers、pointsToWpcom、sslStatus; - 转移相关:
transferStatus(经getTransferStatus计算)、transferStartDate、transferEndDate(由transfer_start_date加 7 天推算)、pendingTransfer、isEligibleForInboundTransfer; - 邮件与订阅:
googleAppsSubscription/titanMailSubscription(键统一转 camelCase)、emailForwardsCount、productSlug、subscriptionId。
值得注意的是,组装过程做了大量类型收窄:多数布尔字段用Boolean(...)强制归一,日期字段统一转为String或null,避免 API 返回undefined或数字导致下游类型混乱。这正是"数据查询组件"模式的价值延伸——把不稳定的外部响应在入口处清洗成内部可信的领域模型。
七、真实业务场景:组件在 Calypso 中的落地
QuerySiteDomains并非孤立示例,它被多个真实业务页面复用。以域名管理模块为例:
- 在 client/my-sites/domains/domain-management/edit-contact-info-page/bulk-edit-contact-info-page.tsx 中,批量编辑联系信息页面会根据首个选中域名所属站点
blog_id渲染<QuerySiteDomains siteId={ firstSelectedDomain.blog_id } />,以确保后续对域名列表(如contactInfoDisclosed等字段)的读取有最新的 store 数据支撑。 - 在 client/my-sites/domains/domain-search/index.tsx 中,域名搜索页在
selectedSite?.ID存在时渲染<QuerySiteDomains siteId={ selectedSite.ID } />,让搜索页能够同步展示当前站点已绑定的域名状态。
这两个场景体现了一个共同的使用契约:当页面需要在"站点尚未完全就绪"时就声明数据需求时,把 Query 组件无条件挂载、用siteId的可用性做守卫即可——组件内部自己处理空值与去重,业务组件无需关心请求时机。
八、使用要点与最佳实践小结
综合 README、组件源码与底层实现,在实际业务中使用QuerySiteDomains时建议遵循以下约定:
- 声明式挂载:在需要域名数据的组件中直接渲染
<QuerySiteDomains siteId={ site.ID } />,不要手动触发fetchSiteDomains(除非是setPrimaryDomain这类业务操作后的主动刷新)。 - 数据从 store 读取:用
getDomainsBySiteId/getDomainsBySite读取域名数组,用hasLoadedSiteDomains判断是否加载完成、isRequestingSiteDomains判断是否请求中,从而决定是否展示 loading 占位。 - 渲染与请求解耦:组件返回
null、不接受 children,页面布局完全由业务组件自己控制。 - 安全守卫:
siteId为必填 number;当站点对象尚未加载时,用site?.ID或selectedSite?.ID之类的可选链传参,组件内部对空值做了保护。 - 复用而非重复请求:同一
siteId的多个页面同时挂载该组件时,isRequestingSiteDomains会拦截重复请求;请求完成后数据已入 store,后续挂载无需重新拉取。
相关源码导航
- 组件实现:client/components/data/query-site-domains/index.jsx
- 组件文档:client/components/data/query-site-domains/README.md
- Action 定义:client/state/sites/domains/actions.js
- Selector 定义:client/state/sites/domains/selectors.js
- Reducer 定义:client/state/sites/domains/reducer.js
- 数据组装:client/state/sites/domains/assembler.js
- 模块状态说明:client/state/sites/domains/README.md
- 真实使用案例:bulk-edit-contact-info-page.tsx、domain-search/index.tsx
【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考