electron进程通信实战,实现截图功能

主进程和渲染进程

主进程使用 BrowserWindow 创建实例,主进程销毁后,对应的渲染进程回被终止。主进程与渲染进程通过 IPC 方式(事件驱动)进行通信。

  • 主进程: 启动项目时运行的 main.js 脚本就是我们说的主进程。在主进程运行的脚本可以以创建 Web 页面的形式展示 GUI。主进程只有一个

  • 渲染进程: 每个 Electron 的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。

预加载脚本

Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境;渲染进程默认跑在网页页面上,而并非 Node.js里。

Electron 的主进程和渲染进程有着清楚的分工并且不可互换,为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。

使用 Electron 的 ipcMain 模块和 ipcRenderer 模块来进行进程间通信。 为了从你的网页向主进程发送消息,你可以使用 ipcMain.handle 设置一个主进程处理程序(handler),然后在预处理脚本中暴露一个被称为 ipcRenderer.invoke 的函数来触发该处理程序(handler)。

单向通信

从渲染器进程到主进程,也可以理解为:在UI界面向主进程发送消息

可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收。

1、先在预加载脚本里定义方法,暴露全局对象

src-electron 目录下定义一个名为 preload.js的脚本文件

使用 contextBridge.exposeInMainWorld 向window暴露一个名为 electronAPI的对象,然后定义一个changeTitle的方法,在这里使用ipcRenderer.send向主进程发送一个消息

js 复制代码
const { contextBridge, ipcRenderer } = require('electron/renderer')

contextBridge.exposeInMainWorld('electronAPI', {
  setTitle: (title) => ipcRenderer.send('set-title', title)
})

2、在主进程中监听方法,并处理

在主进程main.js 文件中引入预加载脚本,然后使用ipcMain.on 进行监听

js 复制代码
 const win = new BrowserWindow({
    width: 1000,
    height: 800,
    title:'董员外',
    icon: join(__dirname, '../public/logo.ico'),
    webPreferences: {
        nodeIntegration: true,
  	contextIsolation: false
        // 引入预加载脚本
        preload: join(__dirname, 'preload.js')
    }

})

ipcMain.on('set-title', (event, title) => {
    // 获取当前的窗口
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)

    // 设置窗口标题  setTitle是electron内置的方法
    win.setTitle(title)
})

3、在UI页面调用暴露的方法

js 复制代码
const changeTitle = ()=>{
    window?.electronAPI.setTitle(title.value)
}

小结

通过这个demo可以了解到,单向通信无非就三步

1、在预加载文件里暴露方法,使用ipcRenderer.send向主进程发送消息

2、 在主进程中用 ipcMain.on接收消息,进行操作

3、 调用全局方法

至此形成一个闭环

双向通信

ipcRenderer.sendipcRenderer.invoke 都是 Electron 渲染进程中用于发送 IPC(进程间通信)消息的方法,他们在用法上有一些区别

  • ipcRenderer.send: 发送消息后不返回任何值,它是单向的,发送消息到主进程后不会等待主进程的响应。
  • ipcRenderer.invoke: 发送消息后会返回一个 Promise,该 Promise 在主进程处理完成后被解析,可以获取到主进程返回的结果。
  • 对于 ipcRenderer.send,主进程通过 ipcMain.on 来监听并处理消息。
  • 对于 ipcRenderer.invoke,主进程通过 ipcMain.handle 来注册处理程序,处理程序返回的 Promise 在处理完成后被解析,将结果返回给渲染进程。

1、预加载暴露全局对象

preload.js 文件 定义并暴露方法

js 复制代码
const { contextBridge, ipcRenderer } = require('electron/renderer')

contextBridge.exposeInMainWorld('electronAPI', {
  setTitle: (title) => ipcRenderer.send('set-title', title),
  postMessage: () => ipcRenderer.invoke('post-message')
})

2、在主进程中监听

ipcMain.handle返回一个 Promise

js 复制代码
ipcMain.handle('post-message', async ()=>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('支付宝到账100万元');
        }, 2000);
      });
})

3、调用方法

js 复制代码
  const sentMessage = async ()=>{
    message.value = await window.electronAPI.postMessage()
  }

全局注入ipcRender的方法

无论是单向通信还是双向通信,都需要在预加载脚本里加入一个方法,然后在主进程中监听。这样当方法数量比较多时,代码添加量也批量上升

可以将ipcRender的send,on,once,removeListener,sendSync,invoke方法注入到全局中,这样直接可以在应用层直接进行使用,而不需要再次在预加载脚本中添加代码

先在预加载脚本中,注入ipcRenderer 的各类方法

js 复制代码
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('ipcRenderer', {
    send: (channel, ...args) => {
        if (args?.length > 0) {
            ipcRenderer.send(channel, ...args)
        } else {
            ipcRenderer.send(channel)
        }
    },
    on: (channel, func) => {
        ipcRenderer.on(channel, func)
    },
    once: (channel, func) => {
        ipcRenderer.once(channel, func)
    },
    removeListener: (channel, func) => {
        ipcRenderer.removeListener(channel, func)
    },
    sendSync: (channel, ...args) => {
        if (args?.length > 0) {
            return ipcRenderer.sendSync(channel, ...args)
        } else {
            return ipcRenderer.sendSync(channel)
        }
    },
    invoke: (channel, ...args) => {
        try {
            return ipcRenderer.invoke(channel, ...args)
        } catch (error) {
            console.error(`Error invoking API: ${channel}`, error)
        }
    },
})

以后就可以直接在应用层进行调用方法

js 复制代码
  const print = ()=>{
    // (window as any).ipcRenderer?.send('方法名');
    (window as any).ipcRenderer?.send('consolelog');
  }
  const print2 = ()=>{
    (window as any).ipcRenderer?.send('consolelog_2',222);
  }
  const print3 = ()=>{
    (window as any).ipcRenderer?.send('consolelog_3',333);
  }

然后在主进程中添加监听方法就行

js 复制代码
ipcMain.on('consolelog', () => {
  console.log("打印 this is 111111")
})
ipcMain.on('consolelog_2', (event, data) => {
  console.log("打印 this is ",data)
})
ipcMain.on('consolelog_3', (event, data) => {
  console.log("打印 this is ",data)
})

接下来就用一个截屏demo实战一下

实现截屏

需要先安装一个截屏插件

js 复制代码
npm install js-web-screen-shot -D

js-web-screen-shot提供了两种截图模式: webrtc 和 html2canvas,如果不开启 enableWebRtc那么就会使用html2canvas 截图模式

html2canvas截图

新建一个截屏页面

js 复制代码
<template>
    <h1>
        这是 截屏 页面
    </h1>
    <div>
        <img src="/public/logo.png" alt="">
    </div>
    <button @click="begainScreen">截屏</button>
    <img class="box-img" :src="imgSrc" alt="">
</template>

<script setup lang="ts">
import ScreenShot  from "js-web-screen-shot";
import { ref } from "vue";
const imgSrc = ref("")
const begainScreen = () => {
    console.log("开始截屏")
    new ScreenShot ({
        enableWebRtc: false,  
        level: 9999,  //层级级别
        completeCallback: callback
    });
}
const callback = (base64data:any)=>{
    console.log(base64data);
    imgSrc.value = base64data.base64
}

</script>

<style scoped>
.box-img{
    width: 200px;
    position: fixed;
    right: 10px;
    top: 10px;
    border: 2px solid red; 
}
</style>

点击 '截屏' 会初始化截屏方法,在callback回调方法里会获取到图片的base64数据,可以作为图片的src。

此时内嵌一个 iframe页面,就会出现一个大bug,截屏时截取不到iframe的内容

js 复制代码
<iframe 
    style="width: 600px;height: 400px;border: 1px solid red;"
    src="https://www.juejin.cn" 
    disablewebsecurity 
    allowpopups
    webpreferences="nodeIntegration, contextIsolation=no" 
/>

这是因为 html2canvas截图是遍历页面的所有dom节点,然后转化为canvas。但是iframe内的dom节点无法获取

WebRtc 截图

因为electron环境下无法直接调用webrtc来获取屏幕流,所以需要获取设备的窗口,然后主线程发送一个IPC消息

1、全局注入ipcRenderer

在之前的通信中已经介绍了如何全局注入 ipcRenderer 了,只需要在预加载脚本中添加代码即可

js 复制代码
import { contextBridge, ipcRenderer } from "electron";


contextBridge.exposeInMainWorld('ipcRenderer', {
  send: (channel, ...args) => {
      if (args?.length > 0) {
          ipcRenderer.send(channel, ...args)
      } else {
          ipcRenderer.send(channel)
      }
  },
  on: (channel, func) => {
      ipcRenderer.on(channel, func)
  },
  once: (channel, func) => {
      ipcRenderer.once(channel, func)
  },
  removeListener: (channel, func) => {
      ipcRenderer.removeListener(channel, func)
  },
  sendSync: (channel, ...args) => {
      if (args?.length > 0) {
          return ipcRenderer.sendSync(channel, ...args)
      } else {
          return ipcRenderer.sendSync(channel)
      }
  },
  invoke: (channel, ...args) => {
      try {
          return ipcRenderer.invoke(channel, ...args)
      } catch (error) {
          console.error(`Error invoking API: ${channel}`, error)
      }
  },
})

2、封装注入Api

在 渲染线程 封装发送消息的方法获取指定窗口的媒体流 的方法

utils/tool.ts

js 复制代码
// 发送消息
export const getDesktopCapturerSource = async () => {
  return await (window as any).ipcRenderer.invoke("ev:send-desktop-capturer_source", []);
};


// 获取指定id设备的视频流
export const  getInitStream = async (
    source: { id: string },
    audio?: boolean
  ): Promise<MediaStream | null> =>{
    return new Promise((resolve, _reject) => {
      // 获取指定窗口的媒体流
      // 此处遵循的是webRTC的接口类型  暂时TS类型没有支持  只能断言成any
      (navigator.mediaDevices as any)
        .getUserMedia({
          audio: audio
            ? {
                mandatory: {
                  chromeMediaSource: "desktop",
                },
              }
            : false,
          video: {
            mandatory: {
              chromeMediaSource: "desktop",
              chromeMediaSourceId: source.id,
            },
          },
        })
        .then((stream: MediaStream) => {
          resolve(stream);
        })
        .catch((error: any) => {
          console.log(error);
          resolve(null);
        });
    });
  }
  

3.主线程获取媒体流

在主线程 添加获取 窗口信息和媒体流的代码

js 复制代码
const selfWindws = async () =>
  await Promise.all(
    webContents
      .getAllWebContents()
      .filter((item) => {
        const win = BrowserWindow.fromWebContents(item);
        return win && win.isVisible();
      })
      .map(async (item) => {
        const win = BrowserWindow.fromWebContents(item);
        const thumbnail = await win?.capturePage();
        // 当程序窗口打开DevTool的时候  也会计入
        return {
          name:
            win?.getTitle() + (item.devToolsWebContents === null ? "" : "-dev"), // 给dev窗口加上后缀
          id: win?.getMediaSourceId(),
          thumbnail,
          display_id: "",
          appIcon: null,
        };
      })
  );

// 获取设备窗口信息
ipcMain.handle(
  'ev:send-desktop-capturer_source',
  async (_event, _args) => {
    console.log('222')
    return [
      ...(await desktopCapturer.getSources({ types: ["window", "screen"] })),
      ...(await selfWindws()),
    ];
  }
);

4、调用webrtc截图

在页面中截图时初始化 截图事件参数,获取传入屏幕流数据,主要依靠前两步封装的方法

js 复制代码
import ScreenShot from "js-web-screen-shot";
import { getDesktopCapturerSource,getInitStream } from '../../utils/tool'
js 复制代码
const begainScreen = async () => {
  // 下面这两块自己考虑  
  const sources = await getDesktopCapturerSource(); // 这里返回的是设备上的所有窗口信息
  // 这里可以对`sources`数组下面id进行判断  找到当前的electron窗口  这里为了简单直接拿了第一个
  console.log("sources",sources);
  const stream = await getInitStream(sources.filter(e => e.name == "董员外")[0]);
  console.log("🚀 ~ doScreenShot ~ stream:", stream)

  new ScreenShot({
    enableWebRtc: true, // 启用webrtc
    screenFlow: stream!, // 传入屏幕流数据
    level: 999,
    completeCallback: callback,
  });
}


const callback = (base64data:any) => {
  console.log("base64data",base64data);
  imgSrc.value = base64data.base64
}

可以看到 iframe也可以截取到

完整的代码放到github上了:electron桌面端工具

相关推荐
昨天;明天。今天。1 小时前
案例-表白墙简单实现
前端·javascript·css
数云界1 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd1 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常1 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer1 小时前
Vite:为什么选 Vite
前端
小御姐@stella1 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing2 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd2 小时前
前端知识汇总(持续更新)
前端
万叶学编程5 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js