electron + vue3 + vite 渲染进程与渲染进程之间的消息端口通信

渲染进程与渲染进程之间的通信有两种:

  • 通过主进程进行消息转发(通过组合主进程与渲染进程之间的单向、双向通信可以实现,可以自己动手尝试,该篇不讲解)
  • 通过消息端口进行直接通信
    该篇主要用示例讲解下单项目内多个窗口的渲染进程之间相互通信的流程(也就是通过消息端口进行通知)

初始版本项目结构可参考项目:https://github.com/ylpxzx/electron-forge-project/tree/init_project

渲染进程与渲染进程之间的消息端口通信

实现整项目示例:https://github.com/ylpxzx/electron-forge-project/tree/message_port

MessageChannelMain方法

示例:

jsx 复制代码
const { port1, port2 } = new MessageChannelMain()

MessageChannelMain方法用于创建一个消息通道,该通道包含两个端口 port1 和 port2。这两个端口可以用于在不同的上下文(如主进程和渲染进程)之间传递消息

通信逻辑

通过模拟主窗口(Main)与配置窗口(Settings)之间的通信,来了解下如何通过消息端口实现渲染进程之间的通信

新增Settings窗口预加载文件
  • 创建src/settingPreload.js文件

    该预加载文件用于暴露可用的Electron API给Settings窗口页面调用

    该文件创建完成后,需要在forge.config.js文件的plugins字段进行配置

    javascript 复制代码
    {
          name: '@electron-forge/plugin-vite',
          config: {
            build: [
              {
                entry: 'src/main.js',
                config: 'vite.main.config.mjs',
                target: 'main',
              },
              {
                entry: 'src/preload.js',
                config: 'vite.preload.config.mjs',
                target: 'preload',
              },
              { // 加入该配置
                entry: 'src/settingPreload.js',
                config: 'vite.preload.config.mjs',
                target: 'preload',
              },
            ],
            renderer: [
              {
                name: 'main_window',
                config: 'vite.renderer.config.mjs',
              },
            ],
          },
        },
主进程下发消息端口给窗口
  • src/main.js文件

    端口下发逻辑:

    • 在主进程main.js文件中,调用消息体MessageChannelMain方法,得到两个可互相通信的端口port1、port2

    • 将消息体端口port1、port2分别下发给准备就绪后的main窗口和settings窗口(即下发给两个渲染进程)

      javascript 复制代码
      // 将port1端口下发给main窗口
      mainWindow.once('ready-to-show', () => {
        mainWindow.webContents.postMessage('main-port', null, [port1])
      })
      
      // 将port2端口下发给settings窗口
      settingsWindow.once('ready-to-show', () => {
        settingsWindow.webContents.postMessage('settings-port', null, [port2])
      })

    完整代码如下:

    javascript 复制代码
    import { app, BrowserWindow, MessageChannelMain } from 'electron';
    import path from 'node:path';
    import started from 'electron-squirrel-startup';
    
    // Avoid Warning:Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security
    process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
    
    if (started) {
      app.quit();
    }
    
    // crerate message channel
    const { port1, port2 } = new MessageChannelMain()
    
    // 创建Main窗口
    const createWindow = () => {
      const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
          preload: path.join(__dirname, 'preload.js'),
        },
      });
    
      if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
        mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
      } else {
        mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
      }
    
      // webContents准备就绪后,使用postMessage向webContents发送端口main-port
      mainWindow.once('ready-to-show', () => {
        mainWindow.webContents.postMessage('main-port', null, [port1])
      })
    
      mainWindow.webContents.openDevTools();
    };
    
    // 创建Settings窗口
    const createSettingsWindow = () => {
      const settingsWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
          nodeintegration: true,
          preload: path.join(__dirname, 'settingPreload.js'),
        },
      });
    
      if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
        const settingUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/settings`;
        settingsWindow.loadURL(settingUrl);
      } else {
        mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
      }
    
    	// webContents准备就绪后,使用postMessage向webContents发送端口settings-port
      settingsWindow.once('ready-to-show', () => {
        settingsWindow.webContents.postMessage('settings-port', null, [port2])
      })
    
      settingsWindow.webContents.openDevTools();
    };
    
    app.whenReady().then(() => {
    	// app准备好后,创建两个窗口
      createWindow();
      createSettingsWindow();
    
      app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
          createWindow();
        }
      });
    });
    
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit();
      }
    });
在预加载文件暴露可调用的Electron API给两个窗口页面
  • src/preload.js

    javascript 复制代码
    const { contextBridge, ipcRenderer } = require('electron/renderer')
    
    let electronMessage = null
    
    ipcRenderer.on('main-port', e => {
      electronMessage = e.ports[0]
    })
    
    contextBridge.exposeInMainWorld('electronAPI', {
    	// 向端口发送消息
      pushMessageEvent: (message) => electronMessage.postMessage(message),
      // 监听端口消息
      onMessagePort: (callback) => {
        const listener = (e) => {
          e.ports[0].onmessage = messageEvent => {
            callback(messageEvent.data)
          }
        }
        ipcRenderer.on('main-port', listener)
        return () => ipcRenderer.removeListener('main-port', listener)
      },
    })

    暴露给页面调用的API有两个:发送消息API、监听消息API; 都是通过ipcRenderer.on实现。

    • 发送消息API:需在上下文隔离外(contextBridge)先实例化消息示例(electronMessage), 然后通过postMessage方法推送消息给端口。

      如果在contextBridge内直接调用 e.ports[0].postMessage(message), 会发送不成功。如下错误示例(没有任何报错):

      javascript 复制代码
      pushMessageEvent: (message) => {
      	ipcRenderer.on('main-port', e => {
      	  e.ports[0].postMessage(message)
      	})
      },

      具体为啥会失败,目前没找到原因。如果有人能答下疑惑,可以在评论区解答下

    • 监听消息API:通过e.ports[0].onmessage方法监听端口,接收到消息时触发对应的回调函数callback

  • src/settingPreload.js 同上

    javascript 复制代码
    const { contextBridge, ipcRenderer } = require('electron/renderer')
    let electronMessageTest = null
    
    ipcRenderer.on('settings-port', e => {
      electronMessageTest = e.ports[0]
    })
    
    contextBridge.exposeInMainWorld('electronSettingAPI', {
      pushMessageEvent: (message) => electronMessageTest.postMessage(message),
      onMessagePort: (callback) => {
        const listener = (e) => {
          e.ports[0].onmessage = messageEvent => {
            callback(messageEvent.data)
          }
        }
        ipcRenderer.on('settings-port', listener)
        return () => ipcRenderer.removeListener('settings-port', listener)
      },
    })

页面示例

通信逻辑实现后,接下来就用两个窗口页面来验证结果

  • src/vue-project/pages/home/index.vue

    javascript 复制代码
    <template>
      <h1>😁 Main Render Process</h1>
      <div style="padding-bottom: 10px;">Receive messages from the Settings window process:<span style="color: #71C25C;">{{
        messageVal }}</span>
      </div>
      <div style="display: flex; gap: 10px;">
        <input id="inputID" ref="inputRef" v-model="inputVal" />
        <button @click="onClick">Send to Settings window</button>
      </div>
    </template>
    
    <script setup>
    import { ref, onUnmounted, onMounted } from 'vue'
    const inputRef = ref(null)
    const inputVal = ref('')
    const messageVal = ref(null)
    const onClick = () => {
    	// 点击发送消息
      electronAPI.pushMessageEvent(inputVal.value)
    }
    let removeLister = null
    
    onMounted(() => {
    	// 监听接收settins窗口回传的消息
      removeLister = electronAPI.onMessagePort(async (value) => {
        messageVal.value = value
      })
    })
    
    onUnmounted(() => {
    	// 页面移除时,移除监听
      removeLister()
    })
    </script>
  • src/vue-project/pages/settings/index.vue

    javascript 复制代码
    <template>
      <h1>😊 Settings Render Process</h1>
      <div style="padding-bottom: 10px;">Received message from the main window process:<span style="color: #71C25C;">{{
        message
      }}</span>
      </div>
      <div style="display: flex; gap: 10px;">
        <input v-model="inputVal" />
        <button @click="onClick">Send to Main window</button>
      </div>
    </template>
    
    <script setup>
    import { ref, onUnmounted, onMounted } from 'vue'
    const message = ref('')
    const inputVal = ref('')
    
    const onClick = () => {
    	// 点击发送消息
      electronSettingAPI.pushMessageEvent(inputVal.value)
    }
    
    let removeLister = null
    
    onMounted(() => {
    	// 监听端口消息,当接收到消息时,再回传一段话给main窗口
      removeLister = electronSettingAPI.onMessagePort((value) => {
        message.value = value
        electronSettingAPI.pushMessageEvent(`👋 Hello main window, I have received your message, message is ${message.value}`)
      })
    })
    
    onUnmounted(() => {
    	// 页面移除时,移除监听
      removeLister()
    })
    </script>
  • src/vue-project/router/index.js

    javascript 复制代码
    import { createWebHashHistory, createRouter } from 'vue-router'
    
    import HomeView from '@/vue-project/pages/home/index.vue'
    import SettingsView from '@/vue-project/pages/settings/index.vue'
    
    const routes = [
      { path: '/', component: HomeView },
      { path: '/settings', component: SettingsView },
    ]
    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    })
    
    export default router;
  • src/vue-project/App.vue

    javascript 复制代码
    <template>
      <h1>🖥️ Hello World!</h1>
      <p>Welcome to your Electron application.</p>
      <p>🎉 Welcome to your Electron <span style="font-weight: 600;">{{ isMain ? 'Main' : 'Settings' }} Window </span>. 🎊
      </p>
      <div
        style="margin-top: 20px; border: 1px solid grey; padding: 20px; border-radius: 6px; background-color: #23272E; color: #fff">
        <router-view></router-view>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue'
    const isMain = ref(true)
    if (window.electronAPI) {
      isMain.value = true
    } else {
      isMain.value = false
    }
    </script>

示例演示:

解决潜在问题:

问题1:从其他页面切换回来时,消息端口监听失效

  • src/vue-project/pages/empty/index.vue

    新增一个空页面,用于复现从其他页面切换回来后,消息端口监听失效问题

    javascript 复制代码
    <template>
      <h1>😁Test whether the message communication is still valid after the page is switched back.</h1>
    </template>
    
    <script setup>
    </script>
  • src/vue-project/router/index.js

    注册empty页面路由

    javascript 复制代码
    import { createWebHashHistory, createRouter } from 'vue-router'
    
    import HomeView from '@/vue-project/pages/home/index.vue'
    import SettingsView from '@/vue-project/pages/settings/index.vue'
    import EmptyView from '@/vue-project/pages/empty/index.vue'
    
    const routes = [
      { path: '/', component: HomeView },
      { path: '/settings', component: SettingsView },
      { path: '/empty', component: EmptyView },
    ]
    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    })
    
    export default router;
  • src/vue-project/App.vue

    javascript 复制代码
    <template>
      <h1>🖥️ Hello World!</h1>
      <p>Welcome to your Electron application.</p>
      <p>🎉 Welcome to your Electron <span style="font-weight: 600;">{{ isMain ? 'Main' : 'Settings' }} Window </span>. 🎊
      </p>
      <nav>
        <div v-if="isMain">
          <RouterLink to="/">Go to Home</RouterLink>
        </div>
        <div v-if="isMain">
          <div>
            <RouterLink to="/empty">Go to empty page<span style="font-size: 12px; color: #999; padding-left: 10px;"></span>
            </RouterLink>
          </div>
        </div>
      </nav>
      <div
        style="margin-top: 20px; border: 1px solid grey; padding: 20px; border-radius: 6px; background-color: #23272E; color: #fff">
        <router-view></router-view>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue'
    const isMain = ref(true)
    if (window.electronAPI) {
      isMain.value = true
    } else {
      isMain.value = false
    }
    </script>
  • 问题复现

  • 解决方法

    vue页面没缓存的情况下,页面会直接被移除,导致之前的监听也丢失。需要给页面路由加上<keep-alive>

    src/vue-project/App.vue

    javascript 复制代码
    <!-- 该处得加入keep-alive,否则切换路由时会重新渲染组件,导致消息端口监听器失效 -->
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </router-view>

问题2:项目打包构建npm run make后运行,settings窗口显示报错

  • 解决方法:

    报错的原因是在生产环境下,找不到settings窗口的路由导致的,更改下生产环境的页面路由路径就可以了

    src/main.js

    javascript 复制代码
    // 其他代码...
    const createSettingsWindow = () => {
      const settingsWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
          nodeintegration: true,
          preload: path.join(__dirname, 'settingPreload.js'),
        },
      });
    
      if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
        const settingUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/settings`;
        settingsWindow.loadURL(settingUrl);
      } else {
        // 增加此行代码:hash: 'settings', 保证打包好的生产环境下正常跳转
        settingsWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`), { hash: 'settings' });
      }
    
      settingsWindow.once('ready-to-show', () => {
        settingsWindow.webContents.postMessage('settings-port', null, [port2])
      })
    
      settingsWindow.webContents.openDevTools();
    };
    // 其他代码...

项目结构

相关推荐
烛阴33 分钟前
秒懂 JSON:JavaScript JSON 方法详解,让你轻松驾驭数据交互!
前端·javascript
拉不动的猪41 分钟前
刷刷题31(vue实际项目问题)
前端·javascript·面试
zeijiershuai43 分钟前
Ajax-入门、axios请求方式、async、await、Vue生命周期
前端·javascript·ajax
恋猫de小郭1 小时前
Flutter 小技巧之通过 MediaQuery 优化 App 性能
android·前端·flutter
只会写Bug的程序员1 小时前
面试之《webpack从输入到输出经历了什么》
前端·面试·webpack
拉不动的猪1 小时前
刷刷题30(vue3常规面试题)
前端·javascript·面试
狂炫一碗大米饭1 小时前
面试小题:写一个函数实现将输入的数组按指定类型过滤
前端·javascript·面试
最胖的小仙女1 小时前
通过动态获取后端数据判断输入的值打小
开发语言·前端·javascript
yzhSWJ1 小时前
Vue 3 中,将静态资源(如图片)转换为 URL
前端·javascript·vue.js
Moment1 小时前
🏞 JavaScript 提取 PDF、Word 文档图片,非常简单,别再头大了!💯💯💯
前端·javascript·react.js