当 XML 学会调 JS:krpano 全景引擎与 Vue 的双向通信实践

[复制链接]
发表于 2026-9-14 13:07:41 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
当 XML 学会调 JS:krpano 全景引擎与 Vue 的双向通信实践

本文基于一个真实运行的安全监测平台(Vue 2.6 + Ant Design Vue),讲清一件事:krpano 全景引擎(XML 驱动)是如何与宿主 HTML/Vue 页面双向通信的。上一篇我们聊了 iframe + postMessage 的跨文档通信,这次是另一种更"古老"也更故意思的模式——引擎嵌在当前文档里,XML 配置反过来调用页面 JS。文中所有代码均来自生产项目,文末附踩坑复盘与最佳实践清单。
一、背景:全景图模块为什么需要"XML 调 JS"

平台里有一块全景业务:对隐患点拍摄球形全景图,在全景中标注场景热点(点击切换到另一个场景)和设备热点(点击检察监测设备数据),管理端还要支持在全景里拖拽热点、拾取视角坐标回填表单。
渲染引擎选了 krpano——全景行业的老牌引擎。它有一个鲜明的架构特点:统统行为皆 XML。场景、图片、热点是 XML 元素,连"函数"都是 XML 里  标签定义的命令序列。项目里与之相关的三个关键文件:
文件脚色public/index.html → <script src="js/krpano.js">引擎本体,向 window 袒露 embedpano / removepanopublic/template_action.xml自定义 action 库——本文主角,XML 与 JS 的桥梁就定义在这里public/vtourskin.xmlkrpano 官方皮肤(导航按钮、缩略图等)业务需求很快把通信问题摆上桌面:

  • 用户在全景里点击热点 → 需要唤起 Vue 的编辑弹窗(引擎 → 页面)
  • 用户在工具箱选中图标 → 需要在全景当前视角创建热点(页面 → 引擎)
  • 用户拖动全景选取初始视角 → 视角值要回填 Vue 表单(双向闭环)
也就是说,XML 世界和 JS 世界必须相互调用。krpano 给出了两条通道。
二、通信全景图:两条方向相反的通道

先给全景架构,后文逐条睁开:
  1. ┌──────────────── Vue 组件(宿主 HTML)────────────────┐
  2. │                                                    │
  3. │  下行通道:krpano.call() / krpano.set()             │
  4. │  ──────────────────────────────────────▶           │
  5. │                                                    │
  6. │  上行通道:XML action 里 js() 调用全局函数           │
  7. │  ◀──────────────────────────────────────           │
  8. │                                                    │
  9. └────────────────────────┬───────────────────────────┘
  10.                          │
  11.               document.getElementById('krpanoSWFObject')
  12.                          │
  13. ┌────────────────────────▼───────────────────────────┐
  14. │              krpano 引擎(XML 驱动)                │
  15. │    ... js(全局函数(参数)) ...    │
  16. └────────────────────────────────────────────────────┘
复制代码

  • 下行(HTML → XML):krpano.call('action名(参数)') 实行 XML 里定义的 action,krpano.set('hotspot[x].属性', 值) 直接写引擎状态;
  • 上行(XML → HTML):action 内部用 js(函数名(参数)) 调用宿主页面的 window 全局函数。
没有 postMessage、没有事件总线,就是最朴素的两条原生通道。妙就妙在:XML 这个"配置文件"获得了调用 JS 的能力,JS 也获得了实行 XML"函数"的能力。
三、初始化:embedpano 与 loadxml

通信的前提是拿到引擎实例。krpano 的初始化分两步(src/views/bim/component/XmlPreview.vue):
  1. mounted() {
  2.   this.$nextTick(() => {
  3.     window.embedpano({
  4.       xml: 0,                    // 关键:0 表示不加载任何默认 XML
  5.       target: 'pano',            // 挂载目标 div 的 id
  6.       html5: 'only',             // 强制 HTML5 渲染(不用 Flash)
  7.       mobilescale: 1.0,
  8.       passQueryParameters: 'startscene,startlookat',
  9.       initvars: { KRPANOPATH: process.env.NODE_ENV === 'development' ? '/' : '' },
  10.     })
  11.     this.initKrpanoReady(this.info.panoSceneXml)
  12.   })
  13. }
复制代码
两个细节值得睁开:
1. xml: 0 是整个计划的支点。 正常用 krpano 会传一个 XML 文件路径。这里传 0,即是先启动一个"空引擎",XML 稍后再注入——由于全景的 XML 是后端数据库里存的字符串(每个场景一份,随热点增删动态变化),而不是静态文件。
2. 引擎实例靠 id 约定获取。 embedpano 会在目标容器里创建一个接口对象,默认 id 为 krpanoSWFObject(汗青遗留命名,Flash 时代的产物):
  1. initKrpanoReady(tempXml) {
  2.   this.krpano = document.getElementById('krpanoSWFObject')
  3.   // ...
  4.   this.krpano.call(`loadxml(${tempXml})`)
  5. }
复制代码
拿到实例后,loadxml() 把后端返回的 XML 字符串动态注入引擎——这就是下行通道的第一次使用。
3. 路径重写:XML 里的相对路径要按情况改写。 后端存的 XML 里引用 template_action.xml、vtourskin.xml、images/ 都是相对路径,而前端开辟情况与生产情况的静态资源根路径不同,于是有了三连 replaceAll:
  1. tempXml = tempXml.replaceAll(`url="template_action.xml"`, `url="${window._CONFIG.krpanoPath}template_action.xml"`)
  2. tempXml = tempXml.replaceAll(`url="vtourskin.xml"`, `url="${window._CONFIG.krpanoPath}vtourskin.xml"`)
  3. tempXml = tempXml.replaceAll(`url="images/`, `url="${window._CONFIG.krpanoPath}images/`)
复制代码
土办法,但有用。本质是在做"XML 模板渲染"——后端存模板,前端按情况补全上下文。
四、上行通道:XML 里的 js() 全局函数桥

4.1 XML 侧:action 里的 js() 调用

看 public/template_action.xml 的核心片段:
  1.   js(handleClickHotSpotsEdit(get(hotspot_title),get(hotspot_type),get(print_ath),get(print_atv),get(url)));
  2. </action>
  3.   js(handleClickHotSpotsScene(get(scene_id),get(hotspot_code)));
  4.   hotspot_get_ath_atv();
  5. </action>
  6.   js(handleClickHotSpotsDevice(get(collector_id),get(host_id),get(obj_type),get(hotspot_title),get(hotspot_code)));
  7.   hotspot_get_ath_atv();
  8. </action>
  9.   copy(print_fov, view.fov);
  10.   copy(print_hlookat, view.hlookat);
  11.   copy(print_vlookat, view.vlookat);
  12.   roundval(print_fov, 1);
  13.   roundval(print_hlookat, 3);
  14.   roundval(print_vlookat, 3);
  15.   js(handleGetViewTohTov(get(print_hlookat),get(print_vlookat),get(print_fov)))
  16. </action>
复制代码
js() 是 krpano 提供的宿主桥:把括号里的表达式求值后,直接调用宿主页面 window 上的同名函数。get(xxx) 取当前作用域/热点的变量值作为参数。注意 view_get_toh_tov 里先 copy + roundval 做了数值规整——数据在过桥之前先在 XML 侧清洗好,JS 侧拿到的就是可直接入库的值,这是一个值得坚持的好习惯。
4.2 Vue 侧:把 methods 挂到 window 上

XML 只认 window 全局函数,而 Vue 的 methods 天然不在 window 上。组件的解法是在 beforeMount 里逐个挂载(XmlPreview.vue):
  1. beforeMount() {
  2.   window['handleClickHotSpotsDevice'] = this.handleClickHotSpotsDevice
  3.   window['handleClickHotSpotsScene'] = this.handleClickHotSpotsScene
  4.   window['handleClickHotSpotsEdit'] = this.handleClickHotSpotsEdit
  5.   window['handleGetViewTohTov'] = this.handleGetViewTohTov
  6.   window['handleClickHotSpotsGetAthAtv'] = this.handleClickHotSpotsGetAthAtv
  7. }
复制代码
于是 XML 的 js(handleClickHotSpotsEdit(...)) 就能直达组件方法。五个桥函数的分工:
全局函数触发源(XML action)Vue 侧行为handleClickHotSpotsEdithotspot_click_edit打开热点编辑弹窗handleClickHotSpotsScenehotspot_click_scene编辑模式开弹窗 / 浏览模式 loadscene 切场景handleClickHotSpotsDevicehotspot_click_device编辑模式开弹窗 / 浏览模式查设备详情handleClickHotSpotsGetAthAtvhotspot_get_ath_atv记录当前热点球面坐标 ath/atvhandleGetViewTohTovview_get_toh_tov接收当前视角 hlookat/vlookat/fov注意 handleClickHotSpotsScene 的双模式计划——同一个上行回调,按 info.toolkit(是否编辑模式)分流:编辑模式下打开弹窗,浏览模式下反过来走下行通道 krpano.call('loadscene(...)') 切换场景。上行和下行在同一个方法里交汇。
五、下行通道:call() 与 set()

5.1 call():实行 XML action 或内置命令
  1. // 执行自定义 action:获取当前视角(结果经 js() 回调返回)
  2. this.krpano.call(`view_get_toh_tov()`)
  3. // 内置命令:切换场景,带 2 秒缩放混合过渡
  4. this.krpano.call(`loadscene(${sceneId}, null, MERGE, ZOOMBLEND(2.0, 2.0, easeInOutSine))`)
  5. // 内置命令:镜头飞向某个热点
  6. this.krpano.call(`looktohotspot(${hotspotCode},120)`)
复制代码
call() 的参数就是一段 krpano action 代码字符串,自定义 action 和引擎内置命令一视同仁。
5.2 set():逐属性创建热点

工具箱创建热点是下行通道最麋集的用法——addhotspot 后连打十几个 set():
  1. handleCreateHotspot(val) {
  2.   this.krpano.call(`view_get_toh_tov()`)          // ① 先取当前视角(上行回调写入 this.hlookat)
  3.   this.$nextTick(() => {
  4.     this.krpano.call(`addhotspot(hotspotname${val.toolId})`)   // ② 创建热点
  5.     this.krpano.set(`hotspot[hotspotname${val.toolId}].url`, `${window._CONFIG.krpanoPath}${val.hotspotUrl}`)
  6.     this.krpano.set(`hotspot[hotspotname${val.toolId}].ath`, this.hlookat)   // ③ 用刚取的视角当坐标
  7.     this.krpano.set(`hotspot[hotspotname${val.toolId}].atv`, this.vlookat)
  8.     this.krpano.set(`hotspot[hotspotname${val.toolId}].hotspot_title`, val.hotspotName)
  9.     this.krpano.set(`hotspot[hotspotname${val.toolId}].scale`, '0.6')
  10.     this.krpano.set(`hotspot[hotspotname${val.toolId}].zoom`, 'true')
  11.     // ④ 行为绑定:动画、标题、拖动、点击——全部指向 XML action
  12.     this.krpano.set(`hotspot[hotspotname${val.toolId}].onloaded`, 'hotspot_do_animation();hotspot_show_title();')
  13.     this.krpano.set(`hotspot[hotspotname${val.toolId}].ondown`, 'hotspot_drag();')
  14.     this.krpano.set(`hotspot[hotspotname${val.toolId}].onclick`, 'hotspot_click_edit();')
  15.   })
  16. }
复制代码
第 ④ 步是点睛之笔:JS 侧只负责"造数据",热点的行为全部委托回 XML action。动画(hotspot_do_animation 精灵图逐帧播放)、标题(hotspot_show_title)、拖动(hotspot_drag)、点击(hotspot_click_edit)都是 template_action.xml 里的能力。职责划分非常清晰:Vue 管业务数据与弹窗,XML 管三维交互。
六、三个完备闭环

闭环 A:点击热点 → 编辑弹窗(纯上行)
  1. 用户点击热点
  2.   → 引擎触发 hotspot.onclick
  3.   → 执行 XML action: hotspot_click_edit()
  4.   → js(handleClickHotSpotsEdit(标题, 类型, ath, atv, url))
  5.   → Vue 方法打开 HotspotEdit 弹窗
  6.   → 表单提交 → 接口保存 → 重新 loadxml 刷新全景
复制代码
闭环 B:选取初始视角(最典范的双向闭环)

场景编辑表单里有三个字段:水平视角 hlookat、垂直视角 vlookat、缩放 fov。让用户手填球面坐标是不大概的,于是做了"所见即所得"的拾取(SceneEdit.vue):
  1. // 1. 用户 focus 视角输入框 → 打开全景弹窗,动态拼 XML 注入引擎
  2. handleFocusAthAtv() {
  3.   this.lookatInfo.visible = true
  4.   this.$nextTick(() => {
  5.     window.embedpano({ xml: 0, target: 'pano', html5: 'only', /* ... */ })
  6.     const krpano = document.getElementById('krpanoSWFObject')
  7.     // 模板字符串拼出完整场景 XML,把表单当前值设为初始视角
  8.     var tempXml = `<krpano version="1.20.7" onstart="loadscene(scene1);">
  9.       <include url="template_action.xml" />
  10.       <scene name="scene1" thumburl="${sceneUrl}" title="场景1">
  11.         <view hlookat="${hlookat}" vlookat="${vlookat}" fovmin="${fovmin}" fovmax="${fovmax}" fov="${fov}" ... />
  12.         <preview url="${previewUrl}"/>
  13.         <image><cube url="${cubeUrl}" /></image>
  14.       </scene>
  15.     </krpano>`
  16.     krpano.call(`loadxml(${tempXml})`)
  17.   })
  18. }
  19. // 2. 用户拖到满意视角后点保存 → 下行:call XML action
  20. handleSaveXMLLookat() {
  21.   const krpano = document.getElementById('krpanoSWFObject')
  22.   krpano.call(`view_get_toh_tov()`)      // 触发 XML 侧取值并 js() 回调
  23.   this.lookatInfo.visible = false
  24.   window.removepano('pano')
  25. }
  26. // 3. 上行:XML action 把视角值送回来,写回表单
  27. handleGetViewTohTov(hlookat, vlookat, fov) {
  28.   this.formData.hlookat = hlookat
  29.   this.formData.vlookat = vlookat
  30.   this.formData.fov = fov
  31. }
复制代码
一个往返:JS call XML → XML 取值 → XML call JS → JS 写表单。两条通道首尾相接,这就是"XML 与 HTML 通信交互"的完备形态。
闭环 C:拖动热点 → 坐标回传(XML 内部盘算 + 上行回传)

拖动热点最难的部分——球面坐标与屏幕坐标的实时换算——完全在 XML action 里解决:
  1.   spheretoscreen(ath, atv, hotspotcenterx, hotspotcentery, calc(mouse.stagex LT stagewidth/2 ? 'l' : 'r'));
  2.   sub(drag_adjustx, mouse.stagex, hotspotcenterx);
  3.   sub(drag_adjusty, mouse.stagey, hotspotcentery);
  4.   asyncloop(pressed,
  5.       sub(dx, mouse.stagex, drag_adjustx);
  6.       sub(dy, mouse.stagey, drag_adjusty);
  7.       screentosphere(dx, dy, ath, atv);      
  8.       copy(print_ath, ath);
  9.       copy(print_atv, atv);
  10.       roundval(print_ath, 3);
  11.       roundval(print_atv, 3);
  12.   );
  13. </action>
  14.   js(handleClickHotSpotsGetAthAtv(get(print_ath),get(print_atv)));
  15. </action>
复制代码
spheretoscreen / screentosphere 是引擎内置的坐标变换,asyncloop(pressed, ...) 在按住期间每帧实行。拖完后由 hotspot_get_ath_atv 把规整到 3 位小数的坐标 js() 回 Vue,存库时就是这个值。坐标换算这种引擎强相关的逻辑留在 XML,Vue 只接收最终结果——再次印证两边的职责边界。
七、踩坑复盘:生产情况教我们的事

7.1 全局函数只挂不摘:悬挂引用与单播覆盖

组件 beforeMount 挂了 5 个 window 函数,但 destroyed 里只清算了全景(window.removepano('pano')),没有摘除全局函数。后果:

  • 组件烧毁后 window.handleClickHotSpotsEdit 仍持有已烧毁组件的引用(内存走漏 + 若被调用会操作僵尸组件);
  • 两个组件(XmlPreview 与 XmlPreviewModel)挂载同名函数,后挂的覆盖先挂的——和 iframe 场景下 window.onmessage 单播覆盖是同一类问题。
正确姿势:
  1. beforeMount() {
  2.   const bridge = {
  3.     handleClickHotSpotsEdit: this.handleClickHotSpotsEdit,
  4.     // ...
  5.   }
  6.   this._bridge = bridge
  7.   Object.entries(bridge).forEach(([k, v]) => (window[k] = v))
  8. },
  9. destroyed() {
  10.   Object.keys(this._bridge).forEach((k) => {
  11.     if (window[k] === this._bridge[k]) delete window[k]   // 只删自己的,防止误删后挂者
  12.   })
  13. }
复制代码
7.2 call() 是同步的,$nextTick 只是荣幸

handleCreateHotspot 里先 call('view_get_toh_tov()'),再在 $nextTick 里使用 this.hlookat。这能工作,纯属 krpano 的一个特性帮了忙:call() 同步实行 action,action 里的 js() 也是同步调用——所以 call() 返回时 this.hlookat 已经被回调写好了,$nextTick 等不等都一样。
但这个隐含契约非常脆弱:一旦 action 里有异步步骤(好比 setinterval、asyncloop、delayedcall),"call 返回即结果就绪"的假设立即崩塌。更稳的写法是回调驱动——view_get_toh_tov() 触发的 js() 回调里直接完成后续创建逻辑,而不是依赖时序巧合。
7.3 桥函数署名漂移

view_get_toh_tov 这个 action 传 3 个参数(hlookat、vlookat、fov),但 XmlPreview.vue 里的 handleGetViewTohTov 只声明了 2 个形参(靠 this.hlookat/this.vlookat 存值,fov 被抛弃);SceneEdit.vue 里则是 3 个。XML 与多个组件之间的"协议"没有单一事实来源,端赖口头约定。改进方案和上一篇 iframe 博客的结论一致:把桥函数的署名(参数名、顺序、寄义)收敛到一个共享的常量/文档里,最好由一个统一的 bridge 模块会集挂载。
7.4 XML 字符串拼接的注入风险


SceneEdit.vue 用模板字符串拼 XML,thumburl="${sceneUrl}" 这些值来自后端。一旦 URL 里出现 " 或
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表