
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交给 IslandIsland 通过mount挂载、渲染并通过普通事件向上报告Shadow Root 双向隔离 CSSmountVueIsland负责打开 shadow root、adopt 样式表、镜像宿主的主题并为 frappe-ui 的浮层提供 portal 目标详见 ADR 0006mount 契约随 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是数据 proponNavigate、onTitle是监听器命名规则是on 事件名首字母大写。mountVueIsland原样交给h在 ui/island/mount.js 中mountVueIsland把整个对象原封不动地交给hconst currentProps shallowRef({ ...props }); const app createApp({ name: FrappeIsland, render: () h(component, currentProps.value), });注意这行render: () h(component, currentProps.value)——props 对象没有被重命名、拆分或重新组织直接进入h的第二个参数。组件作者声明什么 props宿主就能传什么 propsonNavigate会以 Vue 标准的事件监听机制解析到组件的emit(navigate)上。update合并即重渲染update的语义同样简单合并merge。在 mount.js 中update(next) { if (destroyed) return; currentProps.value { ...currentProps.value, ...next }; },currentProps是shallowRef替换其.value触发重渲染新值 旧值展开 新 props 展开。这就是一次 re-render 所做的全部事情——不重建组件、不重挂载 shadow root只更新 props。Island组件同一对象当作属性Vue 侧宿主Island组件ui/island/Island.vue采用同一套语义只是换成了模板写法Island nameinsights.dashboard :dashboarddashboard :context{ user, locale, navigate } titletitle $event actionsactions $event navigaterouter.push($event) /组件是透明的除name与context之外所有属性都是 Island 的 props 对象逐字传递给它的组件。title、navigate与普通组件上的事件绑定行为一致。实现上islandProps计算属性把 attrs 里除class、style之外的键全部收集起来Island.vueconst islandProps computed(() Object.fromEntries(Object.entries(attrs).filter(([key]) key ! class key ! style)) );而class/style停留在宿主元素上inheritAttrs: false防止它们意外落入 Island。为什么要没有翻译层这个形状的核心理念是形状本身没有翻译层所以没有需要学习的规则。一个懂 Vue 的调用者天然懂这个 APIh(Component, props)的入参就是它一个组件作者阅读组件声明的 props 就能知道能传什么。任何额外规则都会成为拼写正确但传不到位的出错点。被否决的方案一结构化对象{ props, on, model }这是最早尝试的形状。问题在于它需要三处翻译mount.js要把on.navigate转成onNavigateIsland.vue要把navigate转回on.navigateDesk 侧的调用者又写了第三种写法。同一个想法存在三种翻译而每一次翻译都是一个名字写对了但传不到的出错点。此外{ props, on, model }是一种我们自己的语法——任何新来者都无法从 Vue 知识中推断它。一个 Idea 出现三种拼写本身就是设计失败的信号。被否决的方案二camelCase 监听器名onUpdateTitle这个方案读起来更顺眼但它永远不会触发。原因在 Vue 的底层事件解析规则Vue 会把事件名中的连字符hyphen驼峰化但不会处理冒号colon。update:title会原样解析为字面量键onUpdate:title仅此而已。结果是一个 Vue 无法解析的名字而无法解析的监听器失败时不会有任何报错——这是监听器出错最糟糕的方式静默失效调用者无从排查。冒号彻底退出 Island API作为配套措施ADR 0010页面 Island 上报标题与动作 移除了页面 Island API 中仅有的两个冒号事件update:title、update:actions被否决改为普通事件title、actions。但事件名不得含冒号这条规则依然约束任何宿主挂载的组件——这正是 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.jsDesk 与 frappe-ui 应用各自只是它的薄封装。host.js 的文档注释里明确重申了这条契约host.jsprops是 Vue 的 props 对象数据键与on*监听器键在同一个扁平对象中正如h(Component, props)所接收的那样。事件名中的连字符或冒号必须保持引号形式——写onUpdate:modelValue而不是onUpdateModelValue后者 Vue 永远不会解析。Desk 侧frappe.ui.mount_islandDesk 的封装在 frappe/public/js/frappe/ui/island/loader.jsfunction mount_island(name, el, props {}) { return mountIsland(name, el, { resolve: resolve_island, host: build_desk_context(), props, }); }调用者传的就是 props 对象Desk 额外注入host与styles。build_desk_context()自动组装locale、timezone、user、base_url与navigate路由到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),仅供参考