news 2026/9/15 11:26:46

Frappe Island 的 Props 契约:为什么一个 Island 直接接收 Vue 的 props 对象

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Frappe Island 的 Props 契约:为什么一个 Island 直接接收 Vue 的 props 对象

Frappe Island 的 Props 契约:为什么一个 Island 直接接收 Vue 的 props 对象

【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe

导读

本文基于 frappe 仓库ui/island/decisions/下的架构决策记录(ADR),深入解析 Island 系统的核心数据契约:宿主(Host)向 Island 传递数据与监听器时,采用的就是 Vue 的 props 对象本身——数据和on*监听器平铺在同一个扁平对象里,与h(Component, props)的入参完全一致。读完本文,你将理解frappe.ui.mount_island<Island>组件为何能"零翻译"地共享同一套传参语义、update如何实现 Island 的增量刷新,以及为什么事件名中不能出现冒号。

Island 是什么:Shadow Root 后面的 Vue 组件

在 frappe 的 Island 架构中,一个 Island 是运行在 Shadow Root 内部的 Vue 组件。宿主页面(host)与 Island 之间存在一条清晰边界:

  • 宿主(host)把数据与监听器(listeners)交给 Island;
  • Island 通过mount挂载、渲染,并通过普通事件向上报告;
  • Shadow Root 双向隔离 CSS,mountVueIsland负责打开 shadow root、adopt 样式表、镜像宿主的主题,并为 frappe-ui 的浮层提供 portal 目标(详见 ADR 0006:mount 契约随 frappe-ui 发布)。

既然数据与监听器要"一起旅行",那么它们以什么形状打包?这就是本 ADR 要回答的问题。

决策:一个扁平的 props 对象

决策结论非常直接:数据键与on*监听器键放在同一个扁平对象中,这正是h(Component, props)的入参形状。

frappe.ui.mount_island("insights.dashboard", el, { dashboard: "sales", onNavigate: (route) => frappe.set_route(route), onTitle: (title) => frappe.utils.set_title(title), });

以这个调用为例:

  • dashboard是数据 prop;
  • onNavigateonTitle是监听器,命名规则是on+ 事件名(首字母大写)。

mountVueIsland:原样交给h

在 ui/island/mount.js 中,mountVueIsland把整个对象原封不动地交给h

const currentProps = shallowRef({ ...props }); const app = createApp({ name: "FrappeIsland", render: () => h(component, currentProps.value), });

注意这行render: () => h(component, currentProps.value)——props 对象没有被重命名、拆分或重新组织,直接进入h的第二个参数。组件作者声明什么 props,宿主就能传什么 props;onNavigate会以 Vue 标准的事件监听机制解析到组件的emit("navigate")上。

update:合并即重渲染

update的语义同样简单:合并(merge)。在 mount.js 中:

update(next) { if (destroyed) return; currentProps.value = { ...currentProps.value, ...next }; },

currentPropsshallowRef,替换其.value触发重渲染;新值 = 旧值展开 + 新 props 展开。这就是"一次 re-render 所做的全部事情"——不重建组件、不重挂载 shadow root,只更新 props。

<Island>组件:同一对象当作属性

Vue 侧宿主<Island>组件(ui/island/Island.vue)采用同一套语义,只是换成了模板写法:

<Island name="insights.dashboard" :dashboard="dashboard" :context="{ user, locale, navigate }" @title="title = $event" @actions="actions = $event" @navigate="router.push($event)" />

组件是"透明"的:namecontext之外,所有属性都是 Island 的 props 对象,逐字传递给它的组件@title@navigate与普通组件上的事件绑定行为一致。实现上,islandProps计算属性把 attrs 里除classstyle之外的键全部收集起来(Island.vue):

const islandProps = computed(() => Object.fromEntries(Object.entries(attrs).filter(([key]) => key !== "class" && key !== "style")) );

class/style停留在宿主元素上,inheritAttrs: false防止它们意外落入 Island。

为什么要"没有翻译层"

这个形状的核心理念是:形状本身没有翻译层,所以没有需要学习的规则。一个懂 Vue 的调用者天然懂这个 API(h(Component, props)的入参就是它);一个组件作者阅读组件声明的 props 就能知道能传什么。任何"额外规则"都会成为拼写正确但传不到位的出错点。

被否决的方案一:结构化对象{ props, on, model }

这是最早尝试的形状。问题在于它需要三处翻译

  1. mount.js要把on.navigate转成onNavigate
  2. Island.vue要把@navigate转回on.navigate
  3. Desk 侧的调用者又写了第三种写法。

同一个想法存在三种翻译,而每一次翻译都是一个"名字写对了但传不到"的出错点。此外,{ props, on, model }是一种"我们自己的语法"——任何新来者都无法从 Vue 知识中推断它。一个 Idea 出现三种拼写,本身就是设计失败的信号。

被否决的方案二:camelCase 监听器名onUpdateTitle

这个方案"读起来更顺眼",但它永远不会触发。原因在 Vue 的底层事件解析规则:

Vue 会把事件名中的连字符(hyphen)驼峰化,但不会处理冒号(colon)

update:title会原样解析为字面量键"onUpdate:title",仅此而已。结果是一个 Vue 无法解析的名字,而无法解析的监听器失败时不会有任何报错——这是监听器出错最糟糕的方式:静默失效,调用者无从排查。

冒号彻底退出 Island API

作为配套措施,ADR 0010:页面 Island 上报标题与动作 移除了页面 Island API 中仅有的两个冒号事件(update:titleupdate:actions被否决,改为普通事件titleactions)。但"事件名不得含冒号"这条规则依然约束任何宿主挂载的组件——这正是 props 对象必须原样交给h的原因:任何自作主张的重写都可能制造出 Vue 无法解析的事件名。

普通事件 vsv-model:为什么 Island 上报用@title而非update:title

顺带说明 0010 中的相关决策:v-model:title在 Vue 中的展开是:title@update:title,而 Island 根本不声明title这个 prop,于是绑定会向一个忽略它的组件传值,并暗示"宿主与 Island 共享状态"。实际上没有任何东西被共享——Island 上报,宿主存储。普通事件@title干净地表达了这一方向性。而且放弃update:形状还省掉了<Island>的一个过滤逻辑:update:形态的绑定会把宿主绑定的值当作 stray attribute 回传下来,导致每次上报都通过update回显一遍。

这个契约落在哪里:host loop 与两个宿主

props 契约之所以能"一处定义、两处复用",是因为它定义在共享的宿主循环(host loop)中。ADR 0008:一个宿主循环,两个宿主 规定:解析名字 → import 模块 → 校验导出mount→ 卸载占位 → 调用mount→ 持有可跨重挂载的 handle,这段逻辑只存在于 ui/island/host.js,Desk 与 frappe-ui 应用各自只是它的薄封装。

host.js 的文档注释里明确重申了这条契约(host.js):

props是 Vue 的 props 对象:数据键与on*监听器键在同一个扁平对象中,正如h(Component, props)所接收的那样。事件名中的连字符或冒号必须保持引号形式——写"onUpdate:modelValue",而不是onUpdateModelValue,后者 Vue 永远不会解析。

Desk 侧:frappe.ui.mount_island

Desk 的封装在 frappe/public/js/frappe/ui/island/loader.js:

function mount_island(name, el, props = {}) { return mountIsland(name, el, { resolve: resolve_island, host: build_desk_context(), props, }); }

调用者传的就是 props 对象,Desk 额外注入hoststylesbuild_desk_context()自动组装localetimezoneuserbase_urlnavigate(路由到frappe.set_route)等环境上下文(loader.js),所有字段都是纯数据或 desk 调用,不含任何 Vue 值——因为 desk 的 bundle 与 Island 跑在各自独立的 Vue 副本上。

Vue 侧:<Island>组件的 props 监听

<Island>islandProps变化由watch捕获,通过handle?.update(next)推过 shadow 边界(Island.vue)。由于useAttrs()返回的 proxy 在父组件每次渲染时都会刷新,它用sameProps做逐键比较,避免每次父渲染都向 shadow root 推送一次冗余更新。

错误处理:静默失败是监听器最糟糕的失败方式

贯穿两个被否决方案的一条主线是失败的可诊断性

  • 结构化对象需要翻译,翻译处可能"名字对但传不到";
  • camelCase 监听器名可能"从不触发且无任何报告"。

这个契约的设计哲学因此非常明确:props 对象原样交给h,让 Vue 自己的解析规则成为唯一的真理来源。调用者写出的事件名是否有效,取决于 Vue 是否认识它;认识则触发,不认识则……虽然仍然可能静默,但至少失败点收敛到了"Vue 的事件解析规则"这一个可学习、可文档化的位置,而不是散布在各宿主自造的翻译逻辑里。

小结

  • Island 的宿主契约 =Vue props 对象:数据键与on*监听器键平铺一个扁平对象,与h(Component, props)的入参一致;
  • mountVueIsland把它原样交给hupdate通过合并触发重渲染,<Island>把除name/context外的属性原样传递;
  • 该契约定义在共享的 ui/island/host.js 宿主循环中,Desk 的frappe.ui.mount_island与 Vue 的<Island>只是两种宿主语法;
  • 事件名中的冒号是禁区:Vue 只驼峰化连字符、不处理冒号,update:title只会解析成永远不触发的"onUpdate:title"
  • 两个被否决方案(结构化{ props, on, model }、camelCase 监听器名)的共同教训:任何一层翻译都是一个名字拼对了却传不到的出错点,而"没有规则可学"本身就是这个 API 最大的优点。

想继续深入,可以按决策编号阅读 ui/island/decisions/ 系列:0001(应用自打包 Island)、0006(契约随 frappe-ui 发布)、0008(单一宿主循环)、0010(页面 Island 的 title/actions 上报),以及宿主循环的实现 ui/island/host.js 与 Desk 装载器 frappe/public/js/frappe/ui/island/loader.js。

【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 11:26:15

AB Download Manager 上手指南:多线程下载、断点续传与定时调度

AB Download Manager 上手指南&#xff1a;多线程下载、断点续传与定时调度 【免费下载链接】ab-download-manager A Download Manager that speeds up your downloads 项目地址: https://gitcode.com/GitHub_Trending/ab/ab-download-manager 从浏览器下载一个 5GB 安装…

作者头像 李华
网站建设 2026/9/15 11:25:22

建设一个网站选择的服务器一文搞懂备案与避坑指南

建设一个网站选择的服务器一文搞懂备案与避坑指南 刚接手第一个网站项目,最让人头大的往往不是代码怎么写,而是域名解析和服务器备案。很多新手盯着后台那一堆“审核中”的状态发呆,心里直打鼓:这服务器到底选对了没?备案流程一头雾水,怕填错一个字段就要重来一周。别慌,这篇指南就是为了解决你此刻的焦虑。咱们不整…

作者头像 李华
网站建设 2026/9/15 11:24:36

Mermaid 时序图进阶教程:3 个场景画会条件分支与并行流程

Mermaid 时序图进阶教程&#xff1a;3 个场景画会条件分支与并行流程 【免费下载链接】mermaid Generation of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown 项目地址: https://gitcode.com/GitHub_Trending/me/mermaid 你…

作者头像 李华
网站建设 2026/9/15 11:24:34

GLM-5开源大模型技术解析与工程实践

1. GLM-5开源模型的技术突破与工程价值作为2023年最受瞩目的开源大模型之一&#xff0c;GLM-5的完整开源标志着Agentic Engineering时代的真正到来。这个由智谱AI团队研发的模型不仅在benchmark测试中表现出色&#xff0c;更重要的是它首次实现了从预训练模型到完整工程解决方案…

作者头像 李华