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 | 插件显示名称 |
| supportedVersions | 1.x、2.x | 支持 NocoBase 1.x 与 2.x |
| isFree | true | 免费插件 |
| builtIn | true | 随发行版内置 |
| defaultEnabled | false | 默认不启用,需在插件管理器中手动启用 |
| editionLevel | 0 | 基础版即可使用 |
| license | Apache-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.schemaUid、parentId、uid等)解析出页面 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()在进入嵌入路由时被调用,它完成三件事:
- 保存当前应用原始的
storage与storagePrefix(快照,用于退出嵌入后恢复); - 将
apiClient.storage切换为sessionStorage并应用隔离前缀; - 从 URL 查询参数中读取
token与authenticator,若存在则注入到认证上下文中:
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 的访问守卫组件,其鉴权流程如下:
- 校验当前用户:请求
auth:check接口(skipAuth: true不携带旧凭证),若未登录则重置 ACL 并渲染 403 页面; - 加载嵌入运行时:确保数据源(
dataSourceManager)与路由仓库的"可访问路由"(routeRepository.ensureAccessibleLoaded)已加载; - 页面可达性判断:
canAccessEmbedPage()通过routeRepository.getRouteBySchemaUid(pageUid)判断目标页面是否存在于当前用户可访问的路由集合中,不存在则直接渲染 403; - 加载 ACL:请求
roles:check接口,将返回的角色、权限片段(snippets)写入应用级 ACL 存储,并同步pluginSettingsManager.setAclSnippets()与apiClient.auth.setRole();若用户没有ui.*权限片段,还会调用flowEngine.flowSettings.disable()禁用流程设置能力; - 渲染上下文:将
CurrentUserContext与ACLContext注入子树,供嵌入页面内的区块、按钮按权限渲染。
v1:auth check 拦截器
v1 客户端的 client/embedAuth.ts 采用 axios 响应拦截器方式处理嵌入鉴权。当任意请求返回 401 时:
- 若当前不在
/embed路径下则原样抛出; - 在嵌入路径下先调用
restoreEmbedSessionToken()尝试用会话 Token 恢复; - 若失败的请求恰好是
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.tsx | v1 客户端:路由注册、PageSettings 菜单项 |
src/client/EmbedLayout.tsx | v1 嵌入布局、页面渲染、复制链接 |
src/client/embedAuth.ts | v1 嵌入鉴权拦截器 |
src/client-v2/plugin.tsx | v2 客户端:布局注册、Provider、菜单 |
src/client-v2/embedSession.tsx | 会话激活、Token 同步与恢复 |
src/client-v2/EmbedAccessGuard.tsx | v2 访问控制守卫 |
src/client-v2/EmbedLayoutComponent.tsx | v2 嵌入布局 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/:包含
EmbedAccessGuard、EmbedLayoutComponent、copyEmbedLinkFlow、route与插件本身的单元测试,覆盖鉴权守卫、布局组件、复制链接流程与路径判定逻辑。
安装与启用
由于插件defaultEnabled: false,需要手动启用。启用方式与 NocoBase 通用插件管理流程一致:
- 以管理员身份登录 NocoBase;
- 进入「插件管理」(插件市场)页面;
- 搜索
@nocobase/plugin-embed(显示名"嵌入 NocoBase"); - 点击安装/启用。
启用后,即可在任意页面的菜单(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),仅供参考