NocoBase 嵌入外部系统指南:@nocobase/plugin-embed 插件原理与实战
2026/9/17 10:19:12 网站建设 项目流程

NocoBase 嵌入外部系统指南:@nocobase/plugin-embed 插件原理与实战

【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase

本文面向希望把 NocoBase 搭建的业务系统无缝嵌入到自有门户、管理后台或第三方页面中的开发者。围绕内置插件@nocobase/plugin-embed,本文将系统讲解嵌入链接的生成方式、/embed路由的渲染机制、会话与 Token 的隔离管理、访问控制与鉴权流程,并结合仓库源码给出可验证的实现依据,帮助读者理解"嵌入"的完整链路并正确配置使用。

插件概览:定位与元数据

@nocobase/plugin-embed是 NocoBase 内置(builtIn: true)的免费(isFree: true)插件,其官方定位是"将 NocoBase 嵌入外部系统或页面中,使其成为该系统或页面的一部分"(见 插件文档)。

从插件清单的元数据(frontmatter)与 package.json 中可以确认以下关键事实:

字段说明
packageName@nocobase/plugin-embed插件包名
displayName嵌入 NocoBase / Embed NocoBase插件显示名称
supportedVersions1.x2.x支持 NocoBase 1.x 与 2.x
isFreetrue免费插件
builtIntrue随发行版内置
defaultEnabledfalse默认不启用,需在插件管理器中手动启用
editionLevel0基础版即可使用
licenseApache-2.0开源协议

插件在运行时区分为服务端与客户端两部分:服务端 server/index.ts 目前仅是一个空的PluginEmbedServer继承类,说明嵌入能力全部集中在前端实现;真正的工作由src/client(v1 客户端)与src/client-v2(v2 客户端)承担。

核心机制一:/embed路由与页面渲染

嵌入功能的核心是提供一套独立的/embed路由前缀,使嵌入页面可以脱离 NocoBase 自身的导航框架、顶栏与侧边栏,只渲染业务页面本身。

v2 客户端的路由注册

在 client-v2/plugin.tsx 中,插件通过布局管理器注册了一个名为embed的布局:

const EMBED_ROUTE_PREFIX = '/embed'; this.app.layoutManager.registerLayout({ routeName: 'embed', routePath: EMBED_ROUTE_PREFIX, uid: EMBED_LAYOUT_MODEL_UID, // 'embed-layout-model' layoutModelClass: EMBED_LAYOUT_MODEL_CLASS, // 'EmbedLayoutModelV2' authCheck: false, });

同时注册了对应的布局模型加载器,将EmbedLayoutModelV2类与embed-layout-modelUID 关联(见 EmbedLayoutModel.tsx)。EmbedLayoutModelV2继承自BaseLayoutModel,其render()方法返回 EmbedLayoutComponent。

嵌入路径的判定

client-v2/route.ts 中的isEmbedRoutePathname()用于判定当前路径是否属于嵌入路由:

const embedPathname = normalizeRootPath(relativePathname).replace(/\/+$/, ''); if (embedPathname === '/embed') return true; return embedPathname.startsWith('/embed/');

这段代码会先剥离应用自身的basename(通过router.getBasename())或publicPath,再判断相对路径是否为/embed或以/embed/开头。也就是说,无论 NocoBase 部署在根路径还是子路径(如/nocobase)下,只要访问${basePath}/embed/...就会进入嵌入模式。

v1 客户端的路由注册

v1 客户端(client/index.tsx)采用router.add方式注册了一系列嵌入路由,均设置skipAuthCheck: true(鉴权由嵌入层自行处理):

this.router.add('embed', { path: '/embed', Component: EmbedLayout, skipAuthCheck: true }); this.router.add('embed.page', { path: '/embed/:name', Component: EmbedPage, skipAuthCheck: true }); this.router.add('embed.page.tab', { path: '/embed/:name/tabs/:tabUid', Component: PageTabs, skipAuthCheck: true }); this.router.add('embed.page.flowTab', { path: '/embed/:name/tab/:tabUid', Component: EmbedPage, skipAuthCheck: true }); this.router.add('embed.page.view', { path: '/embed/:name/view/*', Component: EmbedPage, skipAuthCheck: true }); this.router.add('embed.page.flowTabView', { path: '/embed/:name/tab/:tabUid/view/*', Component: EmbedPage, skipAuthCheck: true });

从路由表可以推断,嵌入模式下不仅支持普通页面(:name),还支持标签页(tabs/:tabUid)、流程页面(tab/:tabUid)以及详情视图(view/*)等多种页面形态。

页面渲染:普通页与流程页

client/EmbedLayout.tsx 中的EmbedPage组件根据页面类型分两条渲染路径:

  • 普通页面:通过RemoteSchemaComponent远程加载页面 schema,并用KeepAlive保持页面状态、以CurrentPageUidContext提供当前页面 UID 上下文;
  • 流程页面NocoBaseDesktopRouteType.flowPage):走EmbedFlowPage,通过getEmbedLayoutModel()获取EmbedLayoutModelV2模型,再用FlowModelRenderer配合FlowRoute渲染流程页面。

嵌入布局在视觉上完全隐藏了 NocoBase 自身的头部导航——EmbedAdminLayout使用'--nb-header-height': '0px'将头部高度置零(见 EmbedLayout.tsx),v2 的 EmbedLayoutComponent 同样设置了'--nb-header-height': '0px',并针对移动端断点(Grid.useBreakpoint()screens.md === false或窗口宽度小于 768px)自动切换为移动端布局。

核心机制二:一键复制嵌入链接

插件为页面配置提供了"复制嵌入链接"的能力,使用者无需手写 URL,可直接从页面菜单一键获取可嵌入的链接。

v2:注册为页面菜单项

client-v2/copyEmbedLinkFlow.tsx 通过RootPageModel.registerExtraMenuItems将"复制嵌入链接"注册到页面公共操作菜单中:

RootPageModel.registerExtraMenuItems({ keyPrefix: COPY_EMBED_LINK_KEY, // 'embed.copyEmbeddedLink' group: 'common-actions', sort: -100, matcher: (model) => !!getRoutePageUid(model), items: (model, t) => [ /* 菜单项:点击后调用 copy(buildEmbedLink(model)) */ ], });

链接的生成逻辑buildEmbedLink()

const pageUid = getRoutePageUid(model); // 取 schemaUid / uid / id 等页面标识 const pathname = getEmbedRoutePath(app, `/embed/${pageUid}`); return new URL(pathname, window.location.origin).toString();

它从当前路由模型(currentRoute.schemaUidparentIduid等)解析出页面 UID,拼接出{origin}{basePath}/embed/{pageUid}形式的完整链接。

v1:页面设置菜单项

v1 客户端在PageSettings中注册了同名菜单项(client/index.tsx),其点击逻辑(EmbedLayout.tsx)通过字符串替换生成链接:

const url = window.location.href .replace('/admin', '/embed') // 将管理端路径替换为嵌入路径 .replace(pageUid, fieldSchema['x-uid']) // 用当前块的 uid 替换页面 uid .replace(window.location.search || '', ''); // 去掉查询参数 copy(url);

这段逻辑从源码结构看属于 v1 的简化实现:把当前访问地址中的/admin前缀替换为/embed,并将页面 UID 替换为当前 schema 节点的x-uid,从而得到可嵌入到任意 iframe 或新窗口中的链接。

核心机制三:嵌入会话隔离与 Token 管理

嵌入场景中最棘手的问题是:外部宿主页面本身可能就是一个已登录的 NocoBase 页面,或者同一浏览器中同时存在多个嵌入实例。插件通过embedSession实现了会话级的隔离,核心代码在 client-v2/embedSession.tsx。

存储前缀的哈希隔离

getEmbedStoragePrefix()以"应用作用域 + 窗口作用域"的组合生成隔离的存储前缀:

const appScope = app.router?.getBasename?.() || app.getPublicPath?.() || window.location.origin; const frameScope = getEmbedWindowName(); return `${EMBED_STORAGE_PREFIX}_${hashStorageSegment(appScope)}_${hashStorageSegment(frameScope)}_`;

其中getEmbedWindowName()会为当前窗口分配一个以__nocobase_embed_开头的唯一window.name。这样,即使同一浏览器中嵌套了多个 NocoBase 嵌入实例,各自的 Token、认证器等sessionStorage键也不会互相覆盖。

会话激活与 Token 注入

activateEmbedSession()在进入嵌入路由时被调用,它完成三件事:

  1. 保存当前应用原始的storagestoragePrefix(快照,用于退出嵌入后恢复);
  2. apiClient.storage切换为sessionStorage并应用隔离前缀;
  3. 从 URL 查询参数中读取tokenauthenticator,若存在则注入到认证上下文中:
const searchParams = getSearchParams(search); const token = searchParams.get('token'); const authenticator = searchParams.get('authenticator'); if (authenticator) { session.authenticator = authenticator; app.apiClient.auth.setAuthenticator?.(authenticator); } if (token) { session.token = token; app.apiClient.auth.setToken(token); }

这意味着宿主系统完全可以通过https://your-nocobase.com/embed/{pageUid}?token=xxx&authenticator=xxx的方式,在 URL 中携带已签发的 Token 实现免登录嵌入。

新 Token 的自动同步

registerEmbedSessionTokenSync()在 axios 响应拦截器中监听x-new-token响应头,一旦服务端在响应中下发了新 Token(例如刷新后的 Token),便会自动更新会话中的 Token,保证嵌入会话长期可用:

responseInterceptor.use((response) => { syncEmbedSessionTokenFromHeaders(app, response.headers); // 读取 x-new-token return response; });

会话退出与恢复

  • restoreEmbedSessionToken():当接口返回 401 时,把会话中保存的 Token 重新写回存储,实现一次"会话内重试";
  • restoreEmbedSession():离开/embed路径后,恢复应用最初的存储配置并触发auth:tokenChanged事件,通知上层刷新认证状态。

EmbedSessionProvider作为应用级 Provider(通过providers.unshift插入到最前)监听路由变化,自动在"激活嵌入会话"与"恢复普通会话"之间切换(embedSession.tsx)。

核心机制四:嵌入访问控制与鉴权

嵌入页面绕过了 NocoBase 常规的路由鉴权(skipAuthCheck: true/authCheck: false),因此插件必须自行完成用户校验、权限检查与页面可达性判断。

v2:EmbedAccessGuard

client-v2/EmbedAccessGuard.tsx 是 v2 的访问守卫组件,其鉴权流程如下:

  1. 校验当前用户:请求auth:check接口(skipAuth: true不携带旧凭证),若未登录则重置 ACL 并渲染 403 页面;
  2. 加载嵌入运行时:确保数据源(dataSourceManager)与路由仓库的"可访问路由"(routeRepository.ensureAccessibleLoaded)已加载;
  3. 页面可达性判断canAccessEmbedPage()通过routeRepository.getRouteBySchemaUid(pageUid)判断目标页面是否存在于当前用户可访问的路由集合中,不存在则直接渲染 403;
  4. 加载 ACL:请求roles:check接口,将返回的角色、权限片段(snippets)写入应用级 ACL 存储,并同步pluginSettingsManager.setAclSnippets()apiClient.auth.setRole();若用户没有ui.*权限片段,还会调用flowEngine.flowSettings.disable()禁用流程设置能力;
  5. 渲染上下文:将CurrentUserContextACLContext注入子树,供嵌入页面内的区块、按钮按权限渲染。

v1:auth check 拦截器

v1 客户端的 client/embedAuth.ts 采用 axios 响应拦截器方式处理嵌入鉴权。当任意请求返回 401 时:

  1. 若当前不在/embed路径下则原样抛出;
  2. 在嵌入路径下先调用restoreEmbedSessionToken()尝试用会话 Token 恢复;
  3. 若失败的请求恰好是auth:check,则构造一个status: 200的"未授权用户"响应(用户 ID 为__nocobase_embed_unauthorized__且带__nocobaseEmbedUnauthorized标记),让上层感知"已登录但无权限"而不是直接崩溃。

EmbedLayout组件(client/EmbedLayout.tsx)通过isEmbedUnauthorizedUser()识别这类特殊用户并渲染 403 页面(NotAuthorized),否则渲染AdminProvider + EmbedAdminLayout

源码结构与测试验证

插件源码位于 packages/plugins/@nocobase/plugin-embed,主要文件与职责如下:

路径职责
src/server/index.ts服务端插件入口(空实现)
src/client/index.tsxv1 客户端:路由注册、PageSettings 菜单项
src/client/EmbedLayout.tsxv1 嵌入布局、页面渲染、复制链接
src/client/embedAuth.tsv1 嵌入鉴权拦截器
src/client-v2/plugin.tsxv2 客户端:布局注册、Provider、菜单
src/client-v2/embedSession.tsx会话激活、Token 同步与恢复
src/client-v2/EmbedAccessGuard.tsxv2 访问控制守卫
src/client-v2/EmbedLayoutComponent.tsxv2 嵌入布局 UI
src/client-v2/copyEmbedLinkFlow.tsx复制嵌入链接
src/client-v2/route.ts/embed路径判定与链接拼装
src/locale/zh-CN.json中文本地化文案

仓库同时提供了较完整的测试覆盖,可作为行为契约参考:

  • client/e2e/popup.test.ts 与 client/e2e/templates.ts:端到端验证嵌入弹窗场景;
  • client/tests/EmbedPage.test.tsx 与 client/tests/plugin.test.ts:v1 页面渲染与插件加载测试;
  • client-v2/tests/:包含EmbedAccessGuardEmbedLayoutComponentcopyEmbedLinkFlowroute与插件本身的单元测试,覆盖鉴权守卫、布局组件、复制链接流程与路径判定逻辑。

安装与启用

由于插件defaultEnabled: false,需要手动启用。启用方式与 NocoBase 通用插件管理流程一致:

  1. 以管理员身份登录 NocoBase;
  2. 进入「插件管理」(插件市场)页面;
  3. 搜索@nocobase/plugin-embed(显示名"嵌入 NocoBase");
  4. 点击安装/启用。

启用后,即可在任意页面的菜单(v2)或页面设置(v1)中找到「复制嵌入链接」入口,将生成的${origin}/embed/{pageUid}链接放入宿主系统的 iframe、弹窗或新窗口中使用。若宿主侧已具备登录态,也可在链接后追加?token=xxx&authenticator=xxx实现免登录直入。

使用场景与注意事项

基于上述机制,该插件适合以下场景:

  • 门户集成:将 NocoBase 页面嵌入企业门户或第三方管理后台,页面只展示业务内容,不带 NocoBase 全局导航;
  • 流程审批嵌入:通过embed.page.flowTab/embed.page.flowTabView路由嵌入流程页面与流程详情视图,在外部系统内完成审批操作;
  • 多实例并行:借助window.name+ 哈希前缀的存储隔离,同一浏览器中可同时运行多个嵌入实例而互不串号。

实际使用时需注意:

  • 嵌入链接会暴露页面 UID,权限完全由EmbedAccessGuard的页面可达性判断与 ACL 加载逻辑兜底,请确保目标页面已配置合适的角色权限;
  • Token 以查询参数形式传递会出现在浏览器历史与日志中,生产环境建议通过宿主页预先换取短期 Token 或采用同域 Cookie 方案;
  • 服务端插件本身不提供额外接口,所有能力均位于客户端,升级客户端版本时需同步验证嵌入路由与会话逻辑的兼容性(插件声明支持 1.x 与 2.x)。

小结

@nocobase/plugin-embed以一套/embed路由前缀为核心,串联起"链接生成 → 会话隔离 → Token 注入与同步 → 权限校验 → 页面渲染"的完整嵌入链路:copyEmbedLinkFlow负责产出可复用的嵌入链接,embedSession通过存储前缀哈希与x-new-token拦截保证会话独立与长期可用,EmbedAccessGuard/embedAuth在绕过常规路由鉴权后自行完成用户与 ACL 校验,EmbedLayout(Component)最终以无头部导航的形态渲染出可无缝融入宿主系统的业务页面。理解了这条链路,即可在自有系统中安全、稳定地完成 NocoBase 的嵌入式集成。

【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询