實戰指南:基于 `@refinedev/multitenancy` 構建 SaaS 級管理后臺)
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),僅供參考