恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Frappe UI Island 的挂载契约:为什么 `mountVueIsland` 归属于 frappe-ui 侧(而不是 desk)

  • 首页
  • 资讯中心
  • /
  • Frappe UI Island 的挂载契约:为什么 `mountVueIsland` 归属于 frappe-ui 侧(而不是 desk)

相关资讯

水体渲染:从光学建模到跨平台落地的完整方案 2026/9/15 19:46:27
C++实现小猫钓鱼纸牌游戏:队列与STL容器实战解析 2026/9/15 19:46:27
FastapiAdmin定时任务实战:从cron配置到APScheduler调度原理 2026/9/15 19:41:26

最新资讯

电脑突然蓝屏怎么办?2026主流远程工具横测:哪款适合救急修电脑?
从 0 到成片:OpenCreator(原 KrillinAI)视频翻译工作台 30 分钟部署上手
基于 Azure Translator 服务实现树莓派多语言智能定时器:IoT-For-Beginners 语音翻译实战指南
@scalar/workspace-store 深度指南:用分块加载与响应式工作区驾驭大型 OpenAPI 文档
es-toolkit compat 版 partialRight 完全指南:从右侧预填参数,实现函数柯里化的反向技巧
Pocket TTS 17个社区应用项目大全:从macOS App到Deno服务器的创意清单

今日推荐

GDPR下大数据架构重构与隐私保护实践
多组学数据平台架构设计与优化实践
企业主数据管理系统架构设计与实施全解析

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Frappe UI Island 的挂载契约:为什么 `mountVueIsland` 归属于 frappe-ui 侧(而不是 desk)

发布时间:2026/9/15 19:46:27
Frappe UI Island 的挂载契约:为什么 `mountVueIsland` 归属于 frappe-ui 侧(而不是 desk) Frappe UI Island 的挂载契约为什么mountVueIsland归属于 frappe-ui 侧而不是 desk【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappemountVueIsland是 Frappe UI island 架构中最核心的挂载契约它接收一个空元素和一份 host 上下文打开 shadow root、adopt 样式表、镜像 host 的主题、为 frappe-ui 的浮层提供 portal 目标并返回update/unmount句柄。本文以 0006 号决策文档 为主线结合 ui/island 下的源码与决策链说明这条契约为什么必须住在 frappe-ui 侧framework/ui/island、desk 侧保留什么、以及两条被否决的替代方案各自输掉了什么。读完你会掌握 island 边界划分的原则、挂载实现的每个细节以及如何在自己的 app 中写出正确的 island 入口。1. 背景契约双方与接线图在 Frappe 的 island 架构里边界两侧是app 侧app 编写并构建 island一个 ES module自带 Vue、frappe-ui 与所有依赖见 0001-an-app-bundles-its-own-island.mdhost 侧desk通过frappe.ui.mount_island或一个 frappe-ui app通过Island组件解析 island 的名字、import 模块并调用其mount(el, context)。ui/README.md 把整套接线归纳为一张职责表契约 / 构件归属mountVueIsland挂载契约framework/ui/islandbuildIslands构建预设framework/ui/vite/islandmountIslandhost 循环framework/ui/island/hostfrappe.ui.mount_islanddesk 加载器frameworkdesk 侧IslandVue host 组件framework/ui/island/Island.vue其中 index.js 就是这条契约的对外出口它只导出两样东西——mountVueIsland来自 mount.js和hostKey/useHost来自 context.js。文件的头部注释写得很清楚构建侧是framework/ui/vite/islanddesk 侧是frappe.ui.mount_island而 app 的 island 入口 import 的正是这个模块。2. 决策契约住在 frappe-ui 侧紧邻它托管的组件0006 决策的第一句话就是结论契约住在 frappe-ui 一侧的边界上紧邻它托管的组件。理由在文档里逐条列出并且都能在 mount.js 的源码中得到印证它调用createAppmount.js#L153 用createApp({ name: FrappeIsland, render: ... })创建 Vue 应用它提供portalTargetKeymount.js#L166 执行app.provide(portalTargetKey, portal)把浮层目标注入给 frappe-ui 的 Dialog、Popover、Select 等组件它安装一个内存路由器因为 frappe-ui 的 Button、MultiSelect 等组件会无条件调用useRouter()mount.js#L164 用createRouter({ history: createMemoryHistory(), routes: routes || [] })保证这个 inject 始终可解析。文档的总结是“所有推理 frappe-ui 的代码都应该放在把 frappe-ui 作为依赖的地方。”换言之这些代码一旦读不到 frappe-ui 的类型与实现就无法编译、无法推理、无法被 app 的 bundler 静态分析——它必须生活在能 importfrappe-ui的模块里。desk 侧的接缝则保持在 desk。文档说“desk loader 解析名字并组装 desk 上下文host 循环调用 island 的mount。两者都不碰 Vue所以 desk 的 bundle 保持不变。”这与 0001 决策 一脉相承——desk 不需要 Vue、不需要 vue-router、不需要 frappe-ui 作为自己的依赖自然也不会因为 island 的存在而膨胀。3. 边界上的另一半hostKey与useHost文档特别指出framework/ui/island还导出hostKey和useHost。island 通过提供它的同一个模块读取 host 上下文因此没有任何 app 需要再次声明这个 symbol。context.js 的实现印证了这一点hostKey使用Symbol.for(frappe:island-host)context.js#L23。Symbol.for是全局注册表所以即使 island 打包了自己的一份该模块副本这正是 0001 决策允许的重复它仍然能读到 host 通过mountVueIsland提供的那份上下文useHost()就是inject(hostKey, {})context.js#L34-L36。空上下文也是可用上下文——每个字段都可选所以在单测里、或在早于某个字段出现的旧 host 下组件依然能渲染。IslandHost的类型定义context.js#L8-L17给出了上下文的标准形状locale、timezone、user、base_url、navigate(route)以及由mountVueIsland补充的theme。注意 mount.js#L127-L135 的一个精妙细节theme 是在挂载这一侧加入 host 上下文的因为它只有在这一侧才能变成一次可追踪tracked的读取——get theme()返回一个ref的值会话中途切换主题会触发 island 重渲染。而且这里构造的是新对象因为 host 传进来的那份是 host 自己的不能改动。theme 的实时监听来自 theme.js用一个MutationObserver观察html上的data-theme属性一个 observer 服务页面上所有 island。4. 挂载契约的完整实现拆解ui/island/mount.js 把文档描述的行为逐行落实。入口签名是export async function mountVueIsland(el, options) { const { component, host {}, props {}, styles [], configure, routes } options || {}; // ... }4.1 前置校验在动手之前做三件事mount.js#L55-L64没有component直接抛错el不是元素没有appendChild抛错el必须已连接进文档el.isConnected因为后面的showPopover()需要一个 connected 元素。4.2 打开 shadow root 并搭建内部结构const shadowHost document.createElement(div); shadowHost.className frappe-island; shadowHost.style.height 100%; el.appendChild(shadowHost); const shadowRoot shadowHost.attachShadow({ mode: open });shadow root 从两个方向隔离 CSSmount.js#L14-L16 的注释island 带着自己的 preflight什么都不漏进 desk 的 Bootstrap DOM。height: 100%的链式传递保证百分比高度可以逐级下传而对 auto 高度的目标则解析为 auto内容尺寸的 island 不受影响。root 内部再建两个子节点mount.js#L76-L112rootfrappe-island-root真正的 Vue 挂载点portalfrappe-island-portal浮层目标。这里有两个值得注意的点主题属性必须放进 shadow root 内部frappe-ui 的暗色选择器[data-themedark] .dark\:x是后代选择器没有任何后代组合符能跨越:host边界所以data-theme必须落在 root 和 portal 上applyTheme函数mount.js#L115-L119portal 用popovermanual属性顶层top layer能逃逸任何祖先的 stacking context——例如 workspace 位于 Editor.js 的.codex-editorz-index: 1下会低于 desk 侧栏而z-index无法解决。留在 shadow root 内可以保住 reka 补丁、adopted sheet 和主题属性而如果另起一个body上的第二 hostreka 的层级会被劈成两个 root。它需要 Safari 17 / Chrome 114target: esnext已经默认了这一前提。Object.assign(portal.style, ...)则是撤销 UA 给 popover 的默认盒子因为内部浮层是position: fixedportal 收缩到零尺寸、也不需要 pointer-events。4.3 主题镜像theme.js 提供currentTheme()读html上的data-theme缺省light和onThemeChange(callback)MutationObserver返回退订函数。mount 时立即应用一次主题并订阅后续变化mount.js#L115-L125。4.4 构建 Vue 应用并返回句柄build()mount.js#L147-L197依次完成按顺序 adopt 样式表shadowRoot.adoptedStyleSheets await Promise.all(styles.map(sharedStyleSheet))——顺序有讲究靠后的 sheet 在同名规则上获胜创建 appcreateApp({ name: FrappeIsland, render: () h(component, currentProps.value) })注入 desk 全局window.SetVueGlobals?.(app)让调用__()或读取frappe的组件能工作装内存路由器保证无条件调用useRouter()的组件可解析island 想自己导航就传routes两个 provideportalTargetKey→ portalhostKey→ 合并了 theme 的 contextconfigure?.(app)给调用方一个注册插件和全局组件的钩子app.mount(root)并返回句柄。句柄的形状mount.js#L174-L194与文档一致——update/unmount两个方法外加app和shadow_rootupdate(next)把新 props 合并进currentPropsshallowRef触发原地重渲染unmount()先置destroyed防重入stopTheme()退订主题监听app.unmount()失败只console.error不阻塞清理最后shadowHost.remove()连带 shadow root 一起移除。sharedStyleSheetmount.js#L200-L222维护一个按 URL 去重的全局Map同一 URL 的 sheet 只 fetch/解析一次被 adopt 进每个 shadow root某个 URL 失败会从 Map 里删除不让一次失败毒化后续所有挂载。最后是失败清理mount.js#L137-L145shadow host 已经进页面了所以样式表 fetch 失败或组件抛错时必须stopTheme()并shadowHost.remove()否则会留下一个空 island。5. 被否决的方案一desk 提供挂载契约文档描述的替代方案是desk 的 bundle 暴露frappe.ui.mount_vue_islandisland 从window上调用它。否决的理由有三层desk 会被迫引入一堆依赖要在自己 bundle 里编译这条契约desk 需要 Vue、vue-router 和 frappe-ui 作为自己的依赖共享运行时问题页面上的每个 island 都会运行在 desk 的副本之上——这其实就是 0001 决策 明确否决的“共享运行时”换了一条路又回来了对 app 的静态分析不透明契约藏在一个全局变量后面app 的类型检查器和 bundler 都看不到它无法做静态验证。对照 0001 决策 里的原话可以看得更清楚“一个向页面要 Vue 的 island是一个页面可以弄坏的 island一个自带 Vue 的 island只会在它自己的构建上出错。”6. 被否决的方案二每个 app 各自实现另一个替代方案是shadow root 加一个createApp也就二十行每个 app 自己开一个就是了。否决的理由是细节不止二十行。文档明确点名了四处“每个都只被找到过一次、错了也不报错”的细节portal 目标浮层必须渲染进 styled tree而不是body覆盖 desk 页面控件的堆叠层级top layer 逃逸 stacking contextroot 内部的主题属性后代选择器跨不过:host编译器的注释处理。源码里还能再补上几处el.isConnected的前置校验、sharedStyleSheet的失败恢复、unmount的防重入与失败容错。这些是反复踩坑换来的隐性知识。文档的结论是“只有一份实现第二个 app 才能免费得到它们。”这是一次性实现的本质价值——单一实现是知识沉淀的载体。7. 契约在实践中的形态7.1 app 侧写一个 island 入口ui/README.md#L107-L118 展示了 app 侧的入口写法构建测试的 fixture 也有完全一致的模式badge.js、panel.js// apps/insights/frontend/src/islands/dashboard.js import { mountVueIsland } from framework/ui/island; import App from ./App.vue; export const mount (el, context) mountVueIsland(el, { ...context, component: App });注意几个要点入口只导出一个mount(el, context)名字由 host 解析context里是 host 注入的locale/user/navigate等字段以及mountVueIsland补充的themeoptions支持props、styleshost 注入的样式表 URL 列表、configure注册插件/全局组件和routesisland 自己的导航入口的mount会把整个context展开后交给mountVueIsland所以组件能通过useHost()读到它。7.2 host 侧两种 host 包装同一个循环文档指出 desk 侧的接缝desk loader 解析名字、组装 desk 上下文host 循环调用 island 的mount两者都不碰 Vue。这正好对应 0008 决策 与 ui/island/host.js 的分工——一个不 import Vue/frappe-ui/frappe 的循环由frappe.ui.mount_islanddesk和IslandVue app两个薄包装共享。desk 保留它自己的boot 注册表、desk 上下文、frappe.ui.mount_islandAPI 与热更新注册Vue 侧保留 Vue 的生命周期与 props watchIsland.vue。7.3 类型与 JSDoc 即文档mountVueIsland的参数、返回句柄都有完整的 JSDocmount.js#L30-L51MountVueIslandOptions里的每个字段都标注了来源host-injected 还是 app 自定与作用。对 app 作者来说这份类型注释就是消费这条契约最直接的说明。8. 决策的启示依赖决定归属契约里每一行代码createApp、portalTargetKey、内存路由都依赖 frappe-ui 的能力它就属于把 frappe-ui 当依赖的那一侧接缝要小、要可陈述island 架构的接缝只是一个名字、一个 URL 和一个mount(el, context)导出0001 决策其余一切都被 shadow root 隔离共享实现而不是共享运行时把“一份实现”放在依赖图正确的位置让所有 app 免费获得踩过坑的细节同时绝不通过window共享运行时避免 desk 被 Vue 全家桶绑架、也避免 app 的类型检查失效隐性知识的成本portal 目标、堆叠层级、主题属性、注释处理这些细节“每个都只被找到过一次错了也不报错”只有集中维护才能让第二次使用不再付费。围绕这个决策的完整讨论链条可以从 ui/island/decisions/ 的 README 进入配套的落地代码在 ui/island契约、host 循环、Vue host与 ui/vite/island构建侧与测试 fixtureui/README.md#L91-L266 则给出了从写入口到构建、再到 desk 挂载与 Vue app 托管的端到端操作步骤。【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号