wps加载项不同窗口间通信

0 阅读5分钟

postMessage 失效原因分析

问题现象

在 WPS 插件项目中,ShowDialog.vue 弹窗通过 window.opener.postMessage() 向 TaskPane.vue 发送消息时,消息无法被正确接收。

根本原因

1. 弹窗打开方式的问题

查看 TaskPane.vueopenOptimizeDialog 方法(第 268-288 行):

const openOptimizeDialog = (requestData) => {
  const url = Util.GetUrlPath() + Util.GetRouterHash() + '/show-dialog?data=' + ...

  if (wps && wps.ShowDialog) {
    wps.ShowDialog(url, '提示词优化结果', width, height, false, false)
  } else {
    window.open(url, 'OptimizeDialog', `width=${width},height=${height},left=${left},top=${top}`)
  }
}

问题分析:

情况 A:使用 WPS 的 wps.ShowDialog()

当 WPS API 可用时,使用 wps.ShowDialog() 打开弹窗。这是问题的主要原因

┌─────────────────────────────────────────┐
         WPS 应用程序环境                 
                                         
  ┌──────────────┐    ┌───────────────┐ 
    TaskPane          ShowDialog    
    (主窗口)           (弹窗)         
                                    
   window: A         window: B      
   opener: null      opener: ???      关键问题
  └──────────────┘    └───────────────┘ 
                                       
             WPS 内部通信               
         └────────────────────┘          
                                         
└─────────────────────────────────────────┘

WPS 环境的特殊性:

  • wps.ShowDialog() 可能创建的是一个独立的 WPS 窗口,而不是标准的浏览器弹窗
  • 弹窗运行在不同的 JavaScript 上下文进程
  • window.opener 引用可能为 null 或指向了一个不可访问的对象
  • 即使 window.opener 不为 null,也可能无法正确访问父窗口的方法和属性
情况 B:使用 window.open()

虽然标准浏览器环境下 window.open() 会设置正确的 window.opener 引用,但在 WPS 插件环境中仍可能存在问题:

┌─────────────────────────────────────────┐
      浏览器环境 (WPS 插件上下文)          
                                         
  ┌──────────────┐    ┌───────────────┐ 
    TaskPane    │────>│  ShowDialog    
    (主窗口)           (弹窗)         
                                    
   window: A         window: B      
   opener: null │<───│ opener: A       理论上正确
  └──────────────┘    └───────────────┘ 
                                       
                                       
           postMessage 可能失败的原因:  
           1. 跨域限制                    
           2. 沙箱环境隔离                
           3. 窗口引用丢失                
         └────────────────────────────┘  
                                         
└─────────────────────────────────────────┘

可能的问题:

  1. 跨域安全限制:如果弹窗 URL 的域与主窗口不同,postMessagetargetOrigin 参数为 '*' 时可能被浏览器拦截
  2. WPS 插件沙箱:WPS 可能对插件窗口实施了沙箱隔离,限制了窗口间的直接访问
  3. 窗口引用丢失:在某些情况下,window.opener 可能会被浏览器或 WPS 清空

2. ShowDialog.vue 中 postMessage 的尝试方式分析

const applyOptimization = () => {
  try {
    // 方式1:发送到 opener
    if (window.opener) {
      window.opener.postMessage({...}, '*')
    }

    // 方式2:发送到 parent
    if (window.parent !== window) {
      window.parent.postMessage({...}, '*')
    }

    // 方式3:发送到自己
    window.postMessage({...}, '*')

    // 方式4:WPS 消息机制
    if (window.wps && window.wps.SendMessage) {
      window.wps.SendMessage('optimization_applied', streamContent.value)
    }
  } catch (error) {
    console.error('Error sending message----->', error)
  }
}

各方式失败原因:

方式失败原因
window.opener.postMessage()opener 为 null 或不可访问(WPS 环境下)
window.parent.postMessage()parent 指向自己(弹窗不是 iframe),或 parent 不可访问
window.postMessage()发送给当前窗口,无法到达 TaskPane
wps.SendMessage()WPS API 可能不支持此方法,或需要特殊权限

3. 为什么 window.opener 会失效?

技术原因详解:

3.1 WPS 窗口管理机制
传统浏览器环境:
┌─────────────────────────────────────┐
│ 浏览器进程                           │
│  ┌─────────────┐  ┌──────────────┐ │
│  │ 主窗口       │  │ 弹窗         │ │
│  │ window.opener ─>│ 引用指向主窗口│ │ ← 正常工作
│  └─────────────┘  └──────────────┘ │
└─────────────────────────────────────┘

WPS 插件环境:
┌─────────────────────────────────────┐
│ WPS 主进程                           │
│  ┌─────────────┐  ┌──────────────┐ │
│  │ TaskPane    │  │ ShowDialog   │ │
│  │ (进程 A)     │  │ (进程 B)      │ │ ← 进程隔离
│  │             │  │              │ │
│  │ 无直接引用   │  │ opener = null│ │ ← 引用丢失
│  └─────────────┘  └──────────────┘ │
│         ↑                ↑          │
│         └───── IPC ─────┘          │
└─────────────────────────────────────┘

WPS 可能使用了**进程间通信(IPC)**而非传统的浏览器窗口引用,导致:

  • 弹窗在独立的进程或线程中运行
  • JavaScript 的 window.opener 引用无法跨越进程边界
  • 需要使用 WPS 提供的特定 API 进行通信
3.2 安全策略限制

现代浏览器和 WPS 都实施了严格的安全策略:

// 可能的限制场景
1. Sandbox 属性: 如果弹窗被设置了 sandbox 属性
   <iframe sandbox="allow-scripts allow-same-origin">
   // 会阻止 window.opener 访问

2. opener 为 null 的情况:
   // 某些环境下,为了安全,opener 会被设为 null
   window.open(url, '_blank', 'noopener')  // 显式设置 noopener

3. 跨域限制:
   // 即使 postMessage 允许跨域,某些环境仍会拦截
   targetWindow.postMessage(message, '*')  // '*' 可能被拦截
3.3 WPS 插件的特殊性
// WPS 插件可能使用的技术栈
1. ElectronChromium 嵌入式浏览器
   - 多进程架构
   - 窗口隔离
   - 需要使用 IPC 通信

2. WPS 自定义窗口管理器
   - 不遵循标准浏览器窗口行为
   - 自定义的窗口打开/关闭逻辑
   - window.opener 可能未正确设置

3. 安全沙箱
   - 限制窗口间的直接访问
   - 需要通过 WPS API 中转

为什么 BroadcastChannel 能解决问题?

对比分析

postMessage 方式:
┌─────────────┐                    ┌──────────────┐
│  TaskPane   │                    │  ShowDialog  │
│             │                    │              │
│  需要窗口引用 ◄────────────────────── opener     │ ← 失败点
│             │                    │              │
└─────────────┘                    └──────────────┘
       ↑                                   │
       └───────── 直接依赖窗口关系 ──────────┘
                   (脆弱,易断裂)

BroadcastChannel 方式:
┌─────────────┐                    ┌──────────────┐
│  TaskPane   │                    │  ShowDialog  │
│             │                    │              │
│  订阅频道   │                    │  发布消息    │
│     ↓       │                    │     ↓        │
│  Channel   ─┼──────────────────┼─>  Channel    │
│             │                    │              │
└─────────────┘                    └──────────────┘
       ↑                                   │
       └───────── 通过频道解耦 ────────────┘
                   (稳定,可靠)

BroadcastChannel 的优势

  1. 不依赖窗口引用

    // 不需要知道目标窗口的引用
    // 不依赖 window.opener
    const channel = new BroadcastChannel('channel_name')
    channel.postMessage(message)  // 直接发送
    
  2. 同源策略下的安全通信

    // 浏览器保证同源窗口可以通信
    // 无需担心跨域问题
    // 无需传递窗口引用
    
  3. 多窗口广播

    // 一个消息可以到达所有订阅者
    // 不需要知道接收方的数量和状态
    
  4. 生命周期独立

    // 窗口可以随时订阅/取消订阅
    // 不受窗口打开/关闭顺序的影响
    

调试验证

如何验证 window.opener 是否失效

在 ShowDialog.vue 中添加调试代码:

onMounted(() => {
  // 调试:检查 window.opener
  console.log('window.opener:', window.opener)
  console.log('window.opener === null:', window.opener === null)
  console.log('window.opener === undefined:', window.opener === undefined)
  console.log('window.parent:', window.parent)
  console.log('window.parent === window:', window.parent === window)

  // 尝试访问父窗口
  try {
    if (window.opener) {
      console.log('opener.location:', window.opener.location)
    }
  } catch (e) {
    console.error('Cannot access opener:', e)
    // 这里很可能会抛出安全错误
  }
})

预期结果:

  • 在 WPS 环境下:window.opener 很可能为 null 或访问时报错
  • 在标准浏览器:window.opener 应该指向父窗口

如何验证 BroadcastChannel 是否工作

// 在 ShowDialog.vue 发送消息后
const testBroadcastChannel = () => {
  const channel = new BroadcastChannel('test_channel')

  // 监听自己的消息(BroadcastChannel 会广播给所有订阅者,包括自己)
  channel.onmessage = (event) => {
    console.log('Received my own message:', event.data)
  }

  channel.postMessage({ test: 'hello' })

  // 如果能在控制台看到 "Received my own message",说明 BroadcastChannel 工作正常
}

总结

postMessage 失效的核心原因

  1. WPS 环境特殊性wps.ShowDialog() 创建的弹窗不在标准浏览器窗口体系中
  2. 窗口引用丢失window.opener 为 null 或不可访问
  3. 进程隔离:弹窗可能运行在独立进程,无法直接访问父窗口
  4. 安全策略:WPS 可能主动限制窗口间的直接访问

为什么 BroadcastChannel 有效

  1. 解耦窗口关系:不需要窗口引用,通过频道名称通信
  2. 浏览器原生支持:不依赖 WPS API,是浏览器标准 API
  3. 同源安全保障:只要同源就能通信,不受窗口层级影响
  4. 简单可靠:API 简洁,不存在复杂的窗口引用关系

最佳实践

在 WPS 插件或类似环境中,跨窗口通信应优先使用:

  1. BroadcastChannel(首选)- 同源窗口通信
  2. localStorage + storage event(降级方案)- 兼容性更好
  3. SharedWorker(高级场景)- 多窗口共享状态

避免使用:

  • window.opener.postMessage() - 依赖窗口引用,易失效
  • window.parent.postMessage() - 仅适用于 iframe
  • WPS 特定 API(除非有官方文档支持)

参考资料