Refine v5 多租户(Multitenancy)实战指南:基于 `@refinedev/multitenancy` 构建 SaaS 级管理后台
发布时间:2026/9/11 13:35:32来源:尧图网络
Refine v5 多租户Multitenancy实战指南基于refinedev/multitenancy构建 SaaS 级管理后台【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine本文以 Refine 企业版内置的多租户能力为核心系统讲解如何用一套代码库服务多个租户从安装refinedev/enterprise与refinedev/multitenancy、通过multitenancyProviderWithTenant /搭建租户上下文到使用路由/本地存储两种 Adapter、TenantSelect选择器与useMultitenancyHook最后深入数据提供者Data Provider层说明如何通过meta.tenantId实现租户数据隔离。读完本文你将能够为 React 管理后台快速接入单代码库、多租户、按路由隔离数据的完整方案。多租户是什么为什么 SaaS 后台需要它多租户Multitenancy指一个软件系统同时服务多个客户租户的能力所有租户共享同一套基础设施与代码库但各自的数据相互隔离、互不可见。在云办公、CRM、ERP、电商平台、LMS 等 SaaS 场景中这是最普遍的系统架构要求。其核心收益包括资源共享复用共享基础设施降低整体成本成本节约维护成本由众多租户分摊按需定制每个租户可独立调整自身配置与设置统一升级一次发布全部租户同时受益。在 Refine 中多租户支持是 Enterprise Edition企业版的内置能力通过refinedev/enterprise与refinedev/multitenancy两个包提供。你可以借助预构建的组件与 Hooks用极少的配置在同一个代码库中服务多个租户。围绕该主题仓库中还维护了一份完整的概念指南 guides-concepts/multitenancy与本文互为补充概念篇侧重思路本文侧重企业版 API 的逐项用法。安装与注册表配置refinedev/multitenancy属于 Refine 企业版不发布在公共 npm 源上需要通过配置.npmrc指向私有注册表来完成安装# 需要为 refinedev scope 配置带认证 token 的注册表 refinedev:registryhttps://registry.refine.dev/ //registry.refine.dev/:_authToken$NPM_TOKEN配置完成后使用包管理器安装两个包两者缺一不可refinedev/enterprise提供RefineEnterprise /根组件refinedev/multitenancy提供 Adapter、WithTenant /、TenantSelect /与useMultitenancypnpm add refinedev/enterprise refinedev/multitenancy快速上手三步接入多租户多租户的接入可以拆解为三个步骤替换根组件、提供multitenancyProvider、用WithTenant /包裹应用代码。第一步从Refine /切换到RefineEnterprise /RefineEnterprise /完全兼容Refine /的所有 props并额外提供多租户等企业级能力- import { Refine } from refinedev/core; import { RefineEnterprise } from refinedev/enterprise; export const App () { return ( - Refine RefineEnterprise {/* Your app code */} /RefineEnterprise ); };第二步提供multitenancyProvider给RefineEnterprise /传入multitenancyProviderprop它接受一个包含adapter与fetchTenants两个属性的对象。下面是一个完整示例——租户列表通过 data provider 从tenants资源获取并把第一条记录作为默认租户import { RefineEnterprise } from refinedev/enterprise; import { useRouterAdapter, WithTenant } from refinedev/multitenancy; type Tenant { id: string; name: string; }; // ... other imports const App () { return ( RefineEnterprise // ... other props multitenancyProvider{{ adapter: useRouterAdapter(), fetchTenants: async () { const response await dataProvider(API_URL).getListTenant({ resource: tenants, pagination: { mode: off, }, }); const tenants response.data; const defaultTenant tenants[0]; return { tenants, defaultTenant, }; }, }} WithTenant fallback{divTenant not found/div} loadingComponent{divLoading.../div} {/* Your app code */} /WithTenant /RefineEnterprise ); };建议将 provider 单独抽取成模块并显式标注类型。仓库中的示例代码如 react-router.tsx展示了这种写法export const multitenancyProvider: MultiTenancyProvider { adapter: useRouterAdapter(), fetchTenants: ... }类型可直接从refinedev/core导入。第三步用WithTenant /包裹应用代码WithTenant /负责拉取租户列表、统一处理加载与异常状态是应用代码的必选包裹层详见下文组件一节。当RefineEnterprise /与WithTenant /挂载完成、multitenancyProvider配置就绪后Refine 会自动从当前路由提取tenantId并通过meta对象透传给 data provider——这一机制是后续数据隔离的基础。multitenancyProvider 详解multitenancyProvider接受两个属性职责边界清晰属性类型职责adapter函数/对象定义租户信息的存取位置URL 或 localStorage并负责在租户切换时同步更新fetchTenants异步函数从 API 或数据源拉取租户列表并确定默认租户fetchTenants租户数据的唯一入口fetchTenants是multitenancyProvider中负责数据的关键部分它从 API 或数据源获取租户列表并决定应用的默认租户。函数必须返回包含两个属性的对象tenants完整的租户数组defaultTenant默认选中的租户对象。fetchTenants: async () { const response await dataProvider(API_URL).getListTenant({ resource: tenants, pagination: { mode: off, // 关闭分页一次性取回全部租户 }, }); const tenants response.data; const defaultTenant tenants[0]; return { tenants, defaultTenant, }; };注意这里关闭了分页pagination: { mode: off }因为租户列表通常规模有限需要完整取回。Adapter租户状态存哪里Adapter 决定租户信息保存在哪里。Refine 内置两个 Adapter也可以根据MultiTenancyProvider类型自定义useRouterAdapter租户放在 URL 中从 URL 中提取tenantId并在租户切换时更新路由。适合希望租户可被链接直接定位/分享的场景如/acme/products直达某个租户。import { useRouterAdapter } from refinedev/multitenancy; const multitenancyProvider { adapter: useRouterAdapter({ // URL 中使用的参数名。例如 localhost:3000/:tenantId/products parameterName: tenantId, // 路由参数对应的租户字段。例如 localhost:3000/:tenantId/products parameterKey: id, // 是否改用 query string 获取租户而非路由参数。例如 localhost:3000/products?tenantId1 useQueryString: false, }), fetchTenants: async () { // Fetch tenants from the API }, };useLocalStorageAdapter租户放在 localStorage从 localStorage 读取tenantId并在租户切换时写入更新。适合不希望在 URL 暴露租户信息、或希望记住用户上次选择的租户的场景。import { useLocalStorageAdapter } from refinedev/multitenancy; const multitenancyProvider { adapter: useLocalStorageAdapter({ // localStorage 中使用的键名。例如 localStorage.getItem(key) storageKey: tenantId, }), fetchTenants: async () { // Fetch tenants from the API }, };两种 Adapter 的 API 表面很接近useRouterAdapter关心参数名 参数键 是否走 query stringuseLocalStorageAdapter只关心存储键名。选择哪种取决于你对 URL 可分享性、隐私性和持久化的权衡。路由级多租户让tenantId成为 URL 的一部分使用useRouterAdapter时需要让路由感知租户。仓库的概念指南中给出了 React Router、Next.js、Remix 三种框架的完整路由示例见 examples 目录核心思路一致在资源路由前加上/:tenantId前缀让 tenantId 成为路由参数。React Router Domimport { BrowserRouter, Outlet, Routes, Route } from react-router; BrowserRouter RefineEnterprise multitenancyProvider{multitenancyProvider} dataProvider{dataProvider(API_URL)} routerProvider{routerProvider} resources{[ { name: products, // 为路由添加 :tenantId 前缀使其感知租户 list: /:tenantId/products, show: /:tenantId/products/:id, edit: /:tenantId/products/:id/edit, create: /:tenantId/products/create, }, ]} Routes {/* 将 tenantId 定义为路由参数 */} Route path/:tenantId element{ WithTenant fallback{divTenant not found/div} loadingComponent{divLoading.../div} Outlet / /WithTenant } Route pathproducts element{ProductsList /} / Route pathproducts/create element{ProductsCreate /} / Route pathproducts/:id element{ProductsShow /} / Route pathproducts/:id/edit element{ProductsEdit /} / /Route /Routes /RefineEnterprise /BrowserRouterNext.jsPages Router在 Next.js 中tenantId体现在目录结构上pages/[tenantId]/products/index.tsx、pages/[tenantId]/products/create.tsx、pages/[tenantId]/products/[id]/index.tsx、pages/[tenantId]/products/[id]/edit.tsx。_app.tsx中同样以RefineEnterpriseWithTenant包裹Component {...pageProps} /资源路由定义为/:tenantId/products等完整示例见 nextjs.tsx。页面内无需特殊处理useList、useShow、useForm等 Hooks 照常使用。RemixRemix 中对应文件约定为app/routes/$tenantId.products._index.tsx、$tenantId.products.create.tsx、$tenantId.products.$id._index.tsx、$tenantId.products.$id.edit.tsxapp/root.tsx中包裹WithTenant并渲染Outlet /完整示例见 remix.tsx。注意上述示例只展示路由定义样式与布局需按所选 UI 库另行实现无论选哪种 UI 库路由接入方式都与示例一致。组件与 HooksWithTenant应用代码的必需包裹层WithTenant /负责拉取租户、处理加载与异常状态必须包裹你的应用代码import { RefineEnterprise } from refinedev/enterprise; import { WithTenant } from refinedev/multitenancy; WithTenant // 租户不可用时渲染的组件 fallback{divTenant not found/div} // 租户加载期间渲染的组件 loadingComponent{divLoading.../div} {/* Your app code */} /WithTenant;fallback当解析不到有效租户例如 URL 中的tenantId不在租户列表内时展示通常用于租户不存在提示页loadingComponent租户数据拉取过程中展示避免白屏。TenantSelect一键切换租户TenantSelect /让用户从租户列表中切换当前租户选中后自动更新当前租户并同步到 Adapter 对应的 URL 或 localStorage。它按 UI 库分目录导出Ant Design 使用refinedev/multitenancy/antdMaterial UI 使用refinedev/multitenancy/mui。两者的 props 完全一致import { TenantSelect } from refinedev/multitenancy/antd; // 或 /mui TenantSelect // 指定租户对象中用于展示的字段 optionLabeltitle // 指定租户对象中作为 select value 的字段 optionValueid // 选中租户时的回调参数为被选中的租户对象 onChange{(tenant) console.log(tenant)} // 对租户列表进行排序 sortTenants{(a, b) a.name.localeCompare(b.name)} /;四个可选 props 的语义分别为展示字段optionLabel、值字段optionValue、变更回调onChange、排序函数sortTenants。把它放进布局的顶部栏即可实现全局租户切换。useMultitenancy编程式访问租户上下文useMultitenancyHook 用于在任意组件内读写多租户上下文import { useMultitenancy } from refinedev/multitenancy; const { // 当前租户对象 tenant, // 可用租户列表 tenants, // 租户列表的加载状态 isLoading, // 触发 authProvider.fetchTenants 重新拉取租户 fetchTenants, // 设置当前租户接受一个租户对象 setTenant, // 删除当前租户 deleteTenant, } useMultitenancy();返回值中的每个成员都有明确分工tenant/tenants用于读取setTenant用于切换fetchTenants用于刷新列表适合租户列表可能动态变化的场景deleteTenant用于登出或重置当前租户。需要定制租户切换逻辑而非使用TenantSelect /时这个 Hook 是首选入口。在 Data Provider 中实现租户数据隔离多租户落地的最后一环是数据隔离Refine 会自动把tenantId放进meta对象传给 data provider你可以在 data provider 中读取它并据此请求租户专属数据。这一点在核心包的实现中也有迹可循——packages/core/src/hooks/useMeta/index.ts中处理了tenantId相关的 meta 合并逻辑即租户信息会随每次数据请求的meta一起下发。定制 data provider 有两种方式逐个覆盖方法在 data provider 实例上重写需要定制的方法推荐改动最小swizzle命令使用 Refine CLI 的swizzle命令见 packages/cli 文档把 data provider 源码弹出到项目中完全掌控实现。下面是一个自定义getList的示例它在请求前把tenantId注入过滤器再调用基础 data providerimport dataProvider from refinedev/simple-rest; const API_URL API_URL; const baseDataProvider dataProvider(API_URL); const customDataProvider { ...baseDataProvider, getList: async ({ resource, filters [], meta, ...props }) { const { tenantId } meta; // 将 tenantId 添加到过滤器中 // 你的 API 可能有不同的处理方式 if (meta?.tenantId) { filters.push({ field: organization, operator: eq, value: meta.tenantId, }); } // 以更新后的过滤器调用基础 data provider 的 getList return baseDataProvider.getList({ resource, filters, meta, ...props, }); }, };要点说明meta由 Refine 自动注入tenantId无需手动传递示例采用字段过滤方式field: organization, operator: eq实现共享表shared-schema模式的数据隔离若你的后端采用独立数据库/独立 Schema的隔离模式可以改用在 URL 路径或请求头中携带tenantId核心不变始终从meta读取租户标识若要彻底定制可用swizzle把 data provider 弹出为项目源码后自由修改概念指南中也明确建议了这条路径。两种隔离模式的选型建议结合仓库文档多租户实现通常落在两种模式上你在设计数据层时应先明确选型共享表 行级过滤Shared所有租户共用同一张表通过tenantId列区分数据归属。数据 provider 侧只需像上文那样追加eq过滤器即可基础设施成本最低完全隔离Isolated每个租户拥有独立的数据库/Schema/表数据天然物理隔离。需要 data provider 根据tenantId切换数据源或 Schema 前缀安全边界最强。官方示例分别对应这两种取向Multitenancy App with Strapi 展示共享数据源下的接入方式Isolated Multitenancy App with Rest API 展示完全隔离的实现。选型时需权衡成本、合规要求与运维复杂度。示例应用仓库中围绕该主题提供了两个可参考的多租户应用示例Multitenancy App with Strapi基于 Strapi 数据源的多租户应用Isolated Multitenancy App with Rest API基于 REST API 的完全隔离型多租户应用。结合这些示例与 guides-concepts/multitenancy 概念指南中的 react-router.tsx、nextjs.tsx、remix.tsx 三份完整路由示例你可以快速对照出自己的落地路径。小结Refine 企业版的多租户方案把租户识别、租户切换、租户数据下发三条链路全部封装好你只需要做好三件事配置安装企业版包、配置.npmrc、用RefineEnterprise替换Refine声明实现multitenancyProvider选择useRouterAdapter或useLocalStorageAdapter编写fetchTenants并用WithTenant /包裹应用隔离在 data provider 中读取meta.tenantId按你的隔离模式过滤或路由请求需要 UI 切换时直接使用TenantSelect /或useMultitenancy。通过这套机制你可以在不改动业务组件的前提下让一个 React 代码库同时服务多个租户从 URL 到数据请求全链路感知当前是谁。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网