关于 Electron webview 通信问题

618 阅读3分钟

简单写了一个 Electron 使用 webview 嵌入外部页面的代码

// App.tsx

import React, { useEffect, useRef } from 'react'

const style = { width: '100%', height: '100vh' }

const App: React.FC = () => {
  const webview = useRef<Electron.WebviewTag>(null)

  const handleDomReady = (): void => {
    const element = webview.current as Electron.WebviewTag
    element.openDevTools()
  }

  useEffect(() => {
    const element = webview.current
    element?.addEventListener('dom-ready', handleDomReady)
    return (): void => {
      element?.removeEventListener('dom-ready', handleDomReady)
    }
  }, [])

  return (
    <webview
      ref={webview}
      preload="file://C:/Users/MissGwen/Desktop/electron-app/preload.cjs"
      src="https://www.github.com"
      style={style}
    />
  )
}

export default App
  • preload: 预加载脚本的本地路径,必须为 file 协议 + 本地绝对路径
  • Electron 提供了 webview DOM 的类型为 Electron.WebviewTag (可能有一部分小伙伴不知道,随口一提)

进程通信

Electron 渲染进程 👉 Webview 预加载脚本

// App.tsx

const App: React.FC = () => {
  ...
  const webview = useRef<Electron.WebviewTag>(null)

  const handleDomReady = (): void => {
    const element = webview.current as Electron.WebviewTag
    element.openDevTools()

    // ✨✨✨
    element.send('to-webview:preload', 'ping') 
  }

  ...

    return (
    <webview
      ref={webview}
      preload="file://C:/Users/MissGwen/Desktop/electron-app/preload.cjs"
    />
  )
}

export default App

// preload.cjs

const { ipcRenderer } = require('electron')

ipcRenderer.on('to-webview:preload', (_, data) => {
  console.log(data) // ping
})

Webview 预加载脚本 👉 Electron 渲染进程

// preload.cjs

const { ipcRenderer } = require('electron')

ipcRenderer.sendToHost('to-electron:render', 'ping')

// App.tsx

const App: React.FC = () => {
  const webview = useRef<Electron.WebviewTag>(null)
  
  // ✨✨✨
  const handleIpcMessage = (event: Electron.IpcMessageEvent): void => {
    console.log(event.args)  // ['ping']
  }

  useEffect(() => {
    const element = webview.current
    
    // ✨✨✨
    element?.addEventListener('ipc-message', handleIpcMessage)
    
    return (): void => {
      element?.removeEventListener('ipc-message', handleIpcMessage)
    }
  }, [])

  return (
    <webview
      ref={webview}
      preload="file://C:/Users/MissGwen/Desktop/electron-app/preload.cjs"
    />
  )
}

export default App

接收的 event.args 的值,实际为一个数组,这样就可以传递和接收多个值

Webview 渲染进程 👈👉 Webview 预加载脚本

  • Electronwebview 提供了两种注入 JavaScript 代码的方式
    1. 就是上面提到的 使用 preload 脚本
    1. 就是使用 executeJavaScript 注入
const webview = useRef<Electron.WebviewTag>(null)
webview.current?.executeJavaScript(`console.log(window)`)

这样注入的 js 就可以在嵌入页面的内部执行
executeJavaScript只能在 'dom-ready' 之后进行调用,注意先后顺序

但是也随之遇到了一个问题,我们使用 executeJavaScript 去访问 window 对象,和在 preload 文件中去访问 window 对象,得到的结果其实并不相同。这说明,虽然都是 window 对象,都可以去操作 DOM ,都可以在嵌入页面运行,但其实两者的环境是相互独立的!

  • 通过 executeJavaScript 注入的 js 代码,可以去访问嵌入页面本身在 window 上挂载的方法,而无法访问 node api
  • 使用 preload 文件注入的 js 代码,可以访问 node api 而无法访问嵌入页面本身在 window 上挂在的方法
  • 这个概念相当于 electron 文档介绍的 上下文隔离

如何通信

  • preload 使用 contextBridgewindow 中暴露注册处理函数,为了方便双向通信,我们将第二个参数设置为回调
// preload.cjs

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('preload', {
  // 定义自己的处理函数
  doAThing(data, cb) {
    console.log(data, 'preload') // ping
    cb('pong')
  }
})
  • executeJavaScript 环境中,就有了访问 preload 的能力,我们使用 element.executeJavaScript('window.preload.doAThing("ping")') 调用定义的处理函数
// App.tsx

import React, { useEffect, useRef } from 'react'

const style = { width: '100%', height: '100vh' }

const App: React.FC = () => {
  const webview = useRef<Electron.WebviewTag>(null)

  const handleDomReady = (): void => {
    const element = webview.current as Electron.WebviewTag
    element.openDevTools()
    
    // ✨✨✨
    element.executeJavaScript(`
      window.preload.doAThing('ping', (data) => {
        console.log(data, 'executeJavaScript') // pong
      })
    `)
  }

  useEffect(() => {
    const element = webview.current
    element?.addEventListener('dom-ready', handleDomReady)

    return (): void => {
      element?.removeEventListener('dom-ready', handleDomReady)
    }
  }, [])

  return (
    <webview
      ref={webview}
      preload="file://C:/Users/MissGwen/Desktop/electron-app/preload.cjs"
      src="https://www.github.com"
      style={style}
    />
  )
}

export default App

我们使用 webview.openDevTools() 打开嵌入页面的控制台,就可以成功看到输出了,虽然在一个控制台,但是是属于两个环境打印出来的日志

image.png