前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一文搞懂Electron的四种视图容器和它们之间的IPC通信机制

一文搞懂Electron的四种视图容器和它们之间的IPC通信机制

原创
作者头像
WendyGrandOrder
发布于 2022-12-20 13:03:26
发布于 2022-12-20 13:03:26
11.8K00
代码可运行
举报
文章被收录于专栏:RESTART POiNTERRESTART POiNTER
运行总次数:0
代码可运行

Electron作为一种基于JS语言搭建的桌面框架,其基础视图容器是包含了Chromium内核的窗口,称为BrowserWindow。对于更复杂的项目,如果需要在窗口内部嵌入第三方业务的页面,则有BrowserView、webView Tag和Iframe三种方案可供选择。

这四类视图容器的实现原理各不相同,和主进程、宿主窗口以及其它兄弟窗口的通信方式也各不相同。官方文档中(截止Electron20版本)的描述较为散乱,本文集中梳理它们各自的特性以及通信方式,并给出推荐的封装模式,以供各位开发者参考。

一、Electron的视图容器层级

1.webContents

Electron的渲染进程是基于Chromium搭建的,下图是Chromium官方文档中关于视图容器的层级划分

其中和Electron关系最紧密的概念是Webcontents,它相当于一个独立的渲染上下文,在Chrome里,每增加一个tab就会创建一个独立的WebContents,它们可以加载各自不同的url,彼此互相独立。

在Electron里,当我们创建一个基础窗口对象,就能够通过它的引用拿到WebContents。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  })

win.loadFile('index.html')
console.log(win.webContents)

它是一个EventEmitter对象,可以通过它来发送跨进程消息,监听其它进程发来的事件,这是Electron内建ipc通信的基础。

此外,Electron还给每个webcontents对象提供了一个上下文隔离(Isolated Context)的预加载环境,并且在其中执行开发者指定的preload脚本。它会在渲染器加载页面之前运行, 可以同时访问 DOM 接口和 Node.js 环境,并且可以通过 contextBridge 接口将特权接口暴露给渲染器。

因为Electron封装的跨进程通信对象ipcMain和ipcRenderer都是基于nodejs环境的api,而出于安全性考虑,通常需要在生产环境中关闭渲染进程的node权限(设置窗口的nodeIntegration为false),以防恶意脚本破坏操作系统。但这样一来,主进程和渲染进程的通信就会变得麻烦。

Preload脚本给我们提供了一种折中方案。我们可以在隔离上下文里把通信通道建立完毕,然后把有限的接口暴露到渲染上下文,供业务使用。并且在暴露接口时做一些参数检查,过滤的工作,避免非法脚本到达主进程。

所以,尽管官方提供的一些demo会把ipcRenderer直接引入渲染进程,但在生产环境下,我们要尽量避免这样做。包括下文的所有demo代码里,ipcRenderer都应该是经过preload检查过滤后的对象,而非原始的node对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 暴露渲染进程访问的对象,也可以换一个别名
contextBridge.exposeInMainWorld('ipcRenderer', {
      send: async (channel: string, ...args: any) => {
        // 可以在这里做一些业务上的合法性检查和过滤
        ipcRenderer.send(channel, ...args);
      },
      invoke: async (channel: string, ...args: any) => {
      // 可以在这里做一些业务上的合法性检查和过滤
        return await ipcRenderer.invoke(channel, ...args);
      },
});

2. frame

在webcontets之上还运行着若干frame,我们可以在主进程遍历出一个窗口的所有frame对象,如果某个窗口打开了devtool,或者加载了iframe标签,frame对象都会新增。而每个webcontents都有一个mainFrame,就是窗口直接加载的主体对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    this.win.webContents.on('did-frame-finish-load',(event, isMainFrame, frameProcessId, frameRoutingId)=>{
     // 每个frame加载完毕后都会触发这个事件
      console.log("aaaaaa did-frame-finish-load", isMainFrame, frameProcessId, frameRoutingId);
       // 遍历窗口所有frame对象,比对routingId,可以找出当前的frame并打印其基础信息
      this.win.webContents.mainFrame.frames.forEach(frame => {
        if(frame.routingId === frameRoutingId){
          const url = new URL(frame.url)
          console.log(“当前frame加载的url ", url);
        }
      })
    })

frame 也有一系列的属性和生命周期钩子,但他并不是EventEmitter,无法通过它和其它进程通信。如果需要跨frame交换消息,需要采取迂回的方案,我们将在后文加以说明。

二、基础窗口BrowserWindow

BrowserWindow是Electron里最基本的视口单位,通过主进程创建和调度,一个BrowserWindow等同于一个独立的Chrome进程。

1. BrowserWindow和主进程的通信

主进程和窗体之间通信几乎是所有业务的刚需,Electron官方提供了基于IpcMain和IpcRenderer的封装,鉴于官方文档已经描述得非常清晰,此处不再罗列代码,只用图总结一下。

从窗口调用主进程分为send和invoke两种模式,前者是单向发送,适用于执行特定操作不关心返回值的场景,后者则会返回一个结果,相当于一来一回,并且是异步的。官方也提供了同步调用接口sendSync,但会造成进程阻塞,实际业务中尽量不要用。

从主进程到窗口,则要借助webcontents的send方法来发送,官方只提供了单向调用的封装,可能是因为主进程是运行在后台的,并没有视图,所以通常情况下不存在由主进程主动发起,并依赖渲染进程返回的场景,但如果实际业务中确实有需求,也可以在send的时候带上唯一标识ID,由渲染进程处理完毕后,携带id发起send,通过两次通信模拟出同样的效果。

2. 两个BrowserWindow之间的通信

由于ipc通信的基础是webcontents,而两个独立的窗口之间无法直接交换渲染上下文的信息,所以需要借助主进程的帮助。如果请求次数少,每次都由主进程转发也问题不大。但如果请求次数多,考虑到多窗口应用的性能问题,最好能够建立窗口对窗口的直接通信。

有两种方式可以实现:

(1) 使用 ipcRenderer.sendTo

该方法支持传入一个webContentsId作为发送目标,发送到特定的渲染上下文,通过它我们可以实现窗口对窗口的直接通信,但首先需要通过主进程来获取另一个窗口的webContentsId。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// A窗口
const targetId = await ipcRenderer.invoke(“GetIWindowBId”) //主进程需要通过ipcMain监听该事件并返回窗口B的id
ipcRenderer.sendTo(targetId,"CrossWindow”,”窗口A发给窗口B)

// B窗口
ipcRenderer.on("CrossWindow",(event,...params)=>{
  console.log("CrossWindow Request from ",event.senderId,...params) // B窗口可以把senderId记录下来,并通过它给A窗口发送消息
  ipcRenderer.sendTo(event.senderId,"CrossWindow”,”窗口B发给窗口A)
})

一旦两个窗口都获悉对方的webContentsId,后续就可以自由地发送消息了(事件名可以任意指定)

(2) 使用MessagePort

MessagePort并不是Electron提供的能力,而是基于MDN的web标准API,这意味着它可以在渲染进程直接创建。同时Electron提供了nodejs侧的实现,所以它也能在主进程创建。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在渲染进程
const messageChannel = new MessageChannel();
console.log(messageChannel.port1);
console.log(messageChannel.port2);

// 在主进程
import { MessageChannelMain } from 'electron';
const messageChannel = new MessageChannelMain();
console.log(messageChannel.port1);
console.log(messageChannel.port2);

两侧创建的port对象,在能力上是对称的,由主进程创建的对象,可以通过

win.webContents.postMessage('port', null, [port1])

方法发送给BrowserWindow,在窗口侧需要监听同名事件

ipcRenderer.on('port', e => {})

拿到e.ports[0]对象并保存下来。

主进程只需要把port分发给A和B窗口,两个窗口之后各自持有port1和port2之后,就可以通过他们进行通信了。

细节代码参见官方文档: https://www.electronjs.org/docs/latest/tutorial/message-ports

看起来MessagePort似乎不如sendTo方便,对于简单的窗口通信,一般来说sendTo就足够用了。

但它和ipcRenderer.sendTo的最大区别在于,后者是基于WebContents的,所以只有具备webContents的对象才能使用,但messagePort是web标准,还适用于webWorker或者iframe,这意味着我们可以直接建立A窗口/主进程和B窗口的worker或iframe的通信链路。在特定业务场景下,这是非常方便的能力。在后面介绍iframe的部分,会给出实践。

三、独立视图容器BrowserView

BrowserView也是由主进程创建的独立视图容器,可以内嵌在其它BrowserWindow里,加载另一个url,有点类似于Iframe,但比iframe工作在更底层,拥有独立的webContents。

原理上来说,创建一个BrowserView相当于在Chrome浏览器里增加一个Tab。一个窗口可以内嵌多个BrowserView,创建时可以指定相对宿主窗口的偏移坐标。在需要给业务窗口嵌入第三方子页面的时候,使用BrowserView可以保证子页面的独立性,避免影响到宿主页面的运行。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const win = new BrowserWindow({ width: 800, height: 600 })
const view = new BrowserView()
win.setBrowserView(view)
view.setBounds({ x: 0, y: 0, width: 300, height: 300 }) // 指定view相对于宿主窗口的位置
view.webContents.loadURL('https://electronjs.org') //view也有独立的webContents对象

但BrowserView也有局限性,由于它是主进程创建并“贴”在宿主窗口上的,所以它的渲染环境完全独立,游离在宿主页面的dom树之外,意味着一旦创建,宿主页面的其它元素都无法通过设置z-index的方式透显在它上面。

1. BrowserView和主进程通信

因为BrowserView有独立的webcontents,并且可以挂载proload脚本,所以它在ipc通信层面的地位和BrowserWindow完全一样,我们可以通过同样的方式,直接在主进程和它交换消息,无需经过宿主转发。不同的BrowserView之间也可以通过sendTo来互相通信。

2. BrowserView和宿主页面通信

正因为BrowserView的上下文是完全独立的,所以无法直接和宿主页面互通。当它需要和素主页面交换消息的时候,同样需要使用窗口对窗口的方式,交换webContentsid或者MessagePort。这是它和传统内嵌页面iframe的最大的区别。

四、内嵌DOM标签<Iframe>

Iframe的概念相信每个web开发都很熟悉,它和Electron框架无关,是浏览器dom标准里自带的内嵌标签,也是最为基础的内嵌方案。在Electron里,iframe没有webContents,而是以宿主页面contents下面的一个frame的形式存在。

1. Iframe和宿主页面通信

和宿主页面的通信方式,就是我们熟悉的postMessage,完全的web标准,这里不再赘述。

2. Iframe和主进程通信

因为iframe没有独立的webContents,无法直接和主进程建立连接,那么最容易想到的方式,就是通过宿主页面转发,先使用postMessage把所有请求发到外层,再通过ipcRenderer发到主进程,拿到结果之后再发回给iframe。

这样固然可以,但实现起来还是颇为繁琐,而且每个请求都要二次通信,在请求较多的情况下也会影响性能。

前文提到messageChannel的特性在渲染侧和node侧都有对称的实现,那么我们可以把宿主页面作为“中介”,只进行一次端口交换,后续让主进程和iframe直接经由端口来通信。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 主进程
this.win.once('ready-to-show', () => {
        const { port1, port2 } = new MessageChannelMain()
        this.win.webContents.postMessage('sendPort', null, [port1])
        port2.start();//注意,这里一定要调用一次start,否则消息会一直pending而不触发回调
	// 使用port2给iframe发消息,也可以接收iframe发来的消息
        port2.on('message',(event)=>{
          console.log("主进程收到iframe发来的消息",event.data);
        })
        setTimeout(()=>{
          port2.postMessage("主进程发给iframe的消息 ");
        },5000)
    })



// 宿主页面
ipcRenderer.on('sendPort', event => {
  const port2 = event.ports[0]
  const iframe = document.querySelector("iframe");
  // 注意,如果父窗口和iframe跨域了,第二个参数要设成*
  iframe.contentWindow.postMessage("sendPortToIframe", '*', [port2]);
})


// iframe内部
let messagePort;
  window.addEventListener("message", function (event) {
    messagePort = event.ports[0];
    // 监听宿主发来的消息,把port存下来,就可以直接和主进程通信了
    messagePort.onmessage = function (event) {
      console.log('iframe 收到主进程发来的消息',event)
    };
    // 用 port给主进程发消息
    messagePort.postMessage('iframe给主进程发消息');
  });

可以看出,连接建立过程中有三个角色参与,但宿主页面只需要转发一次port,后续就可以抽身而出,不必再关心iframe和主进程的通信了。

经过笔者实践,上述代码基于Electron20版本可以正常运行。只不过iframe创建的时机不一定是宿主窗口的ready-to-show,也有可能是后续切特定路由的时候,那么相应的,new messageChannel的时机也要做出调整,整体而言,流程还是有些繁琐。

而且由于iframe没有类似preload的预加载脚本,这些初始化的代码需要侵入到子业务代码里完成,跨业务的开发协作起来也是比较麻烦的。

五、内嵌视图容器 <webview> Tag

通过前文可以看出,BrowserView和iframe各有各的局限,前者独立于宿主的文档流之外,无法跟随宿主页面的排版规则,也没办法覆盖一些全局的弹窗和浮层,使用上受到很大限制。后者没有独立的运行环境,和其它进程建立通信比较麻烦,而且容易影响到宿主页面的运行。

<webview> Tag折中了二者的机制,它和<iframe>Tag一样,可以嵌入宿主页面的文档流里,但却像BrowserView似的拥有独立的WebContents,并且支持挂载私有的proload脚本。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html>
<html lang="">
  <body>
    <div id="drag-area">webview测试</div>
      <webview
    id="testWebview"
    src="file:///xxxx/embedpage.html?subBusinessType=someBusiness"
    style="width: 400px; height: 480px; position: absolute; top: 0; left: 0; z-index: 1000"
    preload="file:///xxxx/testpreload.js"
  ></webview>
  </body>
  <script>
      const webview = document.getElementById('testWebview');
  </script>
</html>

我们通过dom query api拿到的webview对象,会被Electron劫持并替换成一个shadow Dom,它是一个HTMLElement,但同时也具备EmittEvent的功能,可以把它当作一个webContents来使用。

注意,Electron里的<webview>tag是基于chrome app的标准开发的,由于后者已经被Chrome抛弃,所以Electron开发者也无法保证后续版本的可用性。

但因为它实在太过方便,在依赖版本可控的情况下,还是值得一试的。如果未来真的废弃了,也可以把它迁移回iframe,作为降级替代方案。

1. <webview>和宿主窗口通信

因为选中的<webview>对象具有send方法,等同于ipcRenderer.send,使用它可以直接从宿主窗口抛送事件到webview内部,在内部需要通过ipcRenderer.on来监听。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 从宿主到webview

// 宿主侧
webview.send("HostToWebview","hello webview")

// webview侧
ipcRenderer.on("HostToWebview",(event,...params)=>{
   console.log("from host:",...params) })
});

反之,在Webview内部,可以通过ipcRenderer.sendToHost发送事件,在宿主页面通过给webview对象增加ipc-message的事件监听器来接收处理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 从webview到宿主

//  webview侧
ipcRenderer.sendToHost("WebviewToHost","hello host")

// 宿主侧
webview.addEventListener("ipc-message", (event) => {
   console.log("from webview:", event.channel, event.args); 
});

和上面提到的原则一样,webview一侧调用ipcRenderer要限定在proeload里面,避免直接把原生对象暴露到渲染上下文。

2. <webview>和主进程通信

我们知道<webvw>Tag是有独立webConents的,意味着主进程可以直接和它通信,但这里有个特殊之处,它是由宿主窗口在渲染进程里创建的,所以当它创建的时候,主进程并不知道它的存在,需要要由它先发送一个通知。

注意和iframe不同的是,通知的过程可以在webview自己的preload里进行,无需宿主页面转发。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// webview侧(通常是在preload里)

// 发送注册请求,subBusinessType可以是一个标识业务类型的字符串,方便主进程区分,也可以省略。
ipcRenderer.invoke("webviewRegister", subBusinessType)
// 监听主进程发来的事件
ipcRenderer.on(“MainToWebview”,, (event, ...params) => {
    console.log("收到住进程的事件”,…params)
})

// 主进程

// 处理注册请求
ipcMain.handle('webviewRegister', (event, subBusinessType:SubBusinessType) => {
 // 通过event拿到processId和frameId,作为后续发送事件的标识。
 // 注意,之所以需要processId,是因为webview和宿主页面跨域的情况下,二者是运行在不同进程里的,需要通过[processId, frameId]二元对来标识,不可省略。 
  console.log('webview注册subBussiness类型', subBusinessType, event.processId, event.frameId); 
  const processId = event.processId; 
  const frameId = event.frameId; // 拿到sender(webview的webContents对象)并且进行发送。 
  event.sender.sendToFrame([processId, frameId],“MainToWebview”,“helloWebview”); 
})

注意到这其中的神奇之处了么?webview的webContents对象可以直接通过事件event的sender属性获取,无需通过宿主的win对象来获得。

如此一来,<webview>就和窗体解藕了,当我们引入一些第三方子业务的时候,主进程不用关心具体是哪个窗口里嵌入了<webview>标签,只需要关心业务本身,做出对应的处理。iframe方案就无法做到这一点。

<webview>还有一个优势,注册的过程可以在preload脚本里执行,而preload脚本由父业务维护。子业务代码加载之前,我们就可以建立好和主进程之间的通道,并且把子业务需要调用的接口,封装成类似于jsApi的形式,暴露到渲染上下文,而无需入侵子业务的任何代码,还可以考虑不同子业务的公共接口复用,从架构来说比iframe要优雅得多。

整体通讯机制如图所示

六、ipc通信的封装模式实践

上文讲到的通信方式,在实际业务中,还需要进行一定的封装才会更便捷。笔者基于最近参与的新版QQ项目,分享介绍一些窗口和主进程之间的ipc通道封装经验。

这里以采用<webview>Tag嵌入的业务窗口和主进程的通信为例(其他的容器对象原理类似),封装的原则主要有两个:

1. 隔离执行环境

前文也强调过,为了应用的安全性(避免被脚本注入攻击等),我们要禁止业务直接用到原生的ipc对象,为此我们需要把执行环境在封装层面隔离开,避免直接暴露给业务代码。

2. 隔离底层细节

业务侧通常不关心通道建立的细节,只希望能够获取数据,执行命令,我们希望把ipc通信封装得尽可能简单简便,方便业务侧理解和使用。

首先我们需要明确需求,当复数个业务存在的情况下,哪些是通用的,哪些是业务私有的,我们使用基类容纳通用的部分,子类继承基类提供私有的部分。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 主进程
class baseApiHelper{
    public handlers = {
	// 假设写日志是一个通用的api
        writelog(ctx:IpcWebviewCtx, logType:string, ...info:any){
            loggerService.log('[+'+ctx.subBusinessType++],…info);
        },
 }}

class SomeBusinessApiHelper extends BaseApiHelper{
    public handlers = {
        ...super.handlers, // 继承自基类的通用api
        openFile:async (ctx:IpcWebviewCtx,...params:any)=>{
	    // 省略具体的实现
            return 'mock openFile done';
        }
    }
}

type IpcWebviewCtx = {
    subBusinessType:SubBusinessType,
    processId:number,
    frameId:number,
}

其中IpcWebviewCtx是我们定义的上下文类型,包括子业务的类型标识,发送方的processId和frameId,方便handler函数针对不同的业务做一些特殊处理。

每个Helper都是一个单例,可以使用一些依赖注入框架来管理,也可以简单地new出来并且导出,总之当成单例使用即可。

接下来我们实现一个通用的注册事件,在app启动之后就执行绑定,后续任何子业务<webview>被创建,都会触发注册流程。

为了方便管理,我们把子业务标识和它的发送方id拼装起来,作为该容器私有的channelName,并为它注册监听函数,取得调用的方法名,添加上下文之后分发给hanlder函数处理。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    // 主进程
    // 处理全局的webview注册事件
    ipcMain.handle('webviewRegisterSubBussiness', (event, subBusinessType:SubBusinessType) => {
        console.log('webview注册subBussiness类型', subBusinessType, event.processId, event.frameId);
        const processId = event.processId;
        const frameId = event.frameId;

        const channelName = 'ipc-webview-'+subBusinessType+'-'+frameId;

        // 取出对应业务的Helper
        const helper:any = ipcWebviewContainer.get(subBusinessType);

        // 处理来自特定webview的invoke方法,添加上下文之后分配给对应的helper
        ipcMain.handle(channelName, async (event: IpcMainInvokeEvent, eventName: string, ...payload)=>{
            if(helper.handlers[eventName]){
                return await helper.handlers[eventName]({
                    subBusinessType,
                    processId:processId,
                    frameId:frameId
                } as IpcWebviewCtx,
                ...payload);
            }
            return Promise.reject("ipc hanlder not found")
        }) 

        // 处理来自特定webview的send方法,添加上下文之后分配给对应的helper
        ipcMain.on(channelName, (event: IpcMainInvokeEvent, eventName: string, ...payload)=>{
            if(helper.handlers[eventName]){
                helper.handlers[eventName]({
                    subBusinessType,
                    processId:processId,
                    frameId:frameId
                } as IpcWebviewCtx,
                ...payload);
            }else{
                console.warn("ipc hanlder not found")
            }
        })
        return Promise.resolve(true)
    });

而在渲染进程一侧,preload脚本启动后,我们就发送webviewRegisterSubBussiness事件给主进程,并且把调用器暴露到渲染上下文。业务里直接调用ipcApi.invoke或者ipcApi.send,就能执行到对应的方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// webview preload

// 从url里取出页面的业务类型(或者任意其他方式)
const subBusinessType = parseQuery(location.search).subBusinessType;
const channelName = 'ipc-webview-'+subBusinessType+'-'+frameId;

let registerIpcPromiseReslover = ()=>{};
const registerIpcPromise = new Promise((resolve) => {
    registerIpcPromiseReslover = resolve;
  });
ipcRenderer.invoke("webviewRegisterSubBussiness", subBusinessType).then(res=>{
    registerIpcPromiseReslover();
});

contextBridge.exposeInMainWorld('ipcApi',{
    invoke: async (cmd,...params)=>{
        await registerIpcPromise;
        console.log("call ipcApi invoke ",cmd)
        return await ipcRenderer.invoke(channelName,cmd,...params); 
    },
    send: async (cmd,...params)=>{
        await registerIpcPromise;
        console.log("call ipcApi send ",cmd)
        ipcRenderer.send(channelName,cmd,...params); 
    },
})

子业务需要调用的时候,直接使用window对象上的ipcApi就可以了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 子业务
window.ipcApi.send('writelog','info', ‘hello IPC);
const res = await window.ipcApi.invoke('openFile’, somefileName)

注意,这里创建了一个registerIpcPromise,这是因为注册事件到达主进程是异步的,主进程为业务的私有channel注册处理器也需要一些时间,那么在极端情况下,如果业务代码刚启动就调用了api,有可能主进程还没有完成注册,此时可能会调用失败。为了避免情况,我们用一个promise对象让invoke和send请求等一等,注册完成之后再扭转,保证所有的调用都能够被正确处理。

接下来再处理由主进程抛送的通知。

抛送通知给子业务,触发点一定是在某个主进程模块里,我们提供一个触发器给该模块,让它通过子业务类型拿到对应的触发器,触发事件。

我们把触发器也封装在baseApiHelper里,并且用一个Map来维护,这是为了兼容一个子业务有多个实例的情况(当然实际业务场景下,这种情况应该不会很多,可以酌情简化)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 主进程
class baseApiHelper{
    private emiiterMap = new Map<string,Function>;
    public handlers = {
        ……
    }
    public addEmtter(key:string,emitFunc:Function){
        this.emiiterMap.set(key, emitFunc);
    }
    public removeEmtter(key:string){
        this.emiiterMap.delete(key);
    }
    public emitEvent(eventName:string, ...params:any){
        this.emiiterMap.forEach((emitFunc)=>{
            emitFunc(eventName,...params);
        })
    }
}

在子业务注册的时候,我们收集发送对象sender,放进emiiterMap里(还是上面的demo代码,省略重复部分)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    // 主进程 
    // 处理全局的webview注册事件
    ipcMain.handle('webviewRegisterSubBussiness', (event, subBusinessType:SubBusinessType) => {
        console.log('webview注册subBussiness类型', subBusinessType, event.processId, event.frameId);
        const processId = event.processId;
        const frameId = event.frameId;
        const channelName = 'ipc-webview-'+subBusinessType+'-'+frameId;

        const helper:any = ipcWebviewContainer.get(subBusinessType);

        // 处理来自特定webview的invoke方法,添加上下文之后分配给对应的helper
        ……

        // 处理来自特定webview的send方法,添加上下文之后分配给对应的helper
        ……

        // 添加temitter到helper,业务可以通过helper给特定webview发送事件
        helper.addEmtter(processId+'-'+frameId, (eventName:string, ...params:any)=>{
            event.sender.sendToFrame([processId, frameId],channelName, eventName, ...params);
        })

        return Promise.resolve(200)
    });

而在子业务一侧,去注册对sender事件的监听,并且依次触发业务的监听器就可以了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// webview preload
const eventCbMap = {}
ipcRenderer.on(channelName, (event, eventName, ...params) => {
    eventCbMap[eventName]?.forEach(cb=>{
        cb(...params);
    })
})

contextBridge.exposeInMainWorld('ipcApi',{
    on:(eventName, cb)=>{
        console.log("页面注册监听", eventName)
        if(!eventCbMap[eventName]){
            eventCbMap[eventName] = []
        }
        eventCbMap[eventName].push(cb);
    },
    ……
}

这样一来,通道就建立好了,需要抛事件的模块里,只要拿到对应helper,就可以触发emitter了,业务也可以通过ipcApi.on来绑定监听器,收到通知。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 主进程任意业务模块
const someBusinessApiHelper = ipcWebviewContainer.get<SomeBusinessApiHelper>(SubBusinessType.SomeBusiness);
someBusinessApiHelper.emitEvent('helloIPC',`主进程发给webview`); 

当然注册过的事件都是需要提供卸载逻辑的,可以在注册函数末尾返回一个disposer对象,用于注销监听器。

主进程的也emitter也需要在<webview>生命周期结束后予以卸载,可以选择在webview的beforeunload事件里给主进程发送一个卸载请求,并清理对应helper上的emitter对象,具体的逻辑这里不再赘述。

这样,对子业务的ipc封装就完成了,只需要约定需要哪些能力,由开发在主进程去实现,子业务在自己的代码里就可以通过ipcApi去调用,而无需关心其中的细节。

最后一点,因为<webview> Tag是可以通过渲染进程的脚本创建的,其中的preload属性又指向一个本地脚本,为了安全性,我们应该拦截'will-attach-webview’事件,检查其中的参数,规定只允许挂载我们自己的脚本,避免第三方脚本恶意篡改。也可以对webview里的一些行为做出限制,比如禁止重定向等等,具体可以参阅Electron官方文档。

七、总结

本文介绍了Electron里的四种视图容器的特点以及各自的ipc通信方式。

其中三种子视图的作用接近,都可以用来内嵌第三方业务,实际使用时,可以根据业务场景,选择最合适的方案。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
人人皆可二次元!小姐姐生成不同风格动漫形象,肤色、发型皆可变
机器之心报道 编辑:杜伟、陈萍 一张输入人脸图像,竟能生成多样化风格的动漫形象。伊利诺伊大学香槟分校的研究者做到了,他们提出的全新 GAN 迁移方法实现了「一对多」的生成效果。 在 GAN 迁移领域,研究人员可以构建一个以人脸图像为输入并输出人脸动漫形象的映射。相关的研究方法已经出现了很多,如腾讯微视此前推出的迪士尼童话脸特效等等。 在迁移过程中,图像的内容(content)部分可能会被保留,但风格(style)部分必须改变,这是因为同一张脸在动画中能以多种不同的方式表示。这意味着:迁移过程是一个一对多的映
机器之心
2023/03/29
4820
人人皆可二次元!小姐姐生成不同风格动漫形象,肤色、发型皆可变
神经风格迁移研究概述:从当前研究到未来方向(附论文和代码)
选自arXiv 作者:Yongcheng Jing 等 机器之心编译 风格迁移是近来人工智能领域内的一个热门研究主题,机器之心也报道了很多相关的研究。近日,来自浙江大学和亚利桑那州立大学的几位研究者在 arXiv 上发布了一篇「神经风格迁移(Neural Style Transfer)」的概述论文,对当前神经网络风格迁移技术的研究、应用和难题进行了全面的总结。机器之心对该论文的部分内容进行了编译介绍,论文原文请访问:https://arxiv.org/abs/1705.04058。此外,为方便进一步的扩展
机器之心
2018/05/07
2.2K0
神经风格迁移研究概述:从当前研究到未来方向(附论文和代码)
实时把你的脸变成名画,手机摄像头新玩法
梦晨 发自 凹非寺 量子位 报道 | 公众号 QbitAI 这款叫做“FaceBlit”的最新的风格迁移技术,能够实时把镜头前你的脸变成指定画像的风格,还能匹配你的表情动作。 性别不同也没问题。 甚至可以用雕像和草稿。 这一切都能在手机上实时进行,不需要拍好视频再等待处理。这意味着,本项技术可以应用于直播和视频通话,而不仅是上传拍好的视频作品。 它还可以反过来用,通过镜头捕捉你的表情动作,让画像同步动起来。 来看看这么惊艳的效果是如何做到的吧。 位置匹配+外观匹配 首先是位置匹配,通过下巴的轮廓确
量子位
2023/03/10
3250
实时把你的脸变成名画,手机摄像头新玩法
你也可以拥有「宋慧乔妆」,美图MakeupGan妆容迁移算法开启个性化妆容时代
虚拟试妆技术一直是美妆、美颜市场最重要的技术之一。当前该领域流行的主流技术为传统素材贴妆,该技术指由专业设计师按指定格式设计好妆容素材,再利用人脸关键点检测把妆容素材贴到对应的五官位置上。
机器之心
2020/09/24
1.5K0
你也可以拥有「宋慧乔妆」,美图MakeupGan妆容迁移算法开启个性化妆容时代
【技术综述】人脸妆造迁移核心技术总结
美颜和美妆是人脸中很常见的技术,在网络直播以及平常的社交生活中都有很多应用场景。常见的如磨皮,美白,塑形等美颜技术我们已经比较熟悉了,而本文重点介绍的是人脸妆造迁移的核心技术及其相关资源。
用户1508658
2020/07/14
1.4K0
【技术综述】人脸妆造迁移核心技术总结
朋友圈变美靠AI:新型美颜技术实现细粒度颜值提升
图 1:表述为多对多图像转译问题的人脸美化:新提出的方法将基于风格的美颜表征与颜值预测模型整合到了一起,并能实现细粒度的控制。
CDA数据分析师
2019/12/24
7650
基于深度学习的图像真实风格迁移
本文介绍了神经风格迁移的算法原理、应用案例,以及基于深度学习的图像风格迁移技术的优势。同时,作者还探讨了在实现过程中所面临的挑战,并展望了未来研究方向。
蒋心为
2017/08/16
7K2
基于深度学习的图像真实风格迁移
你跳宅舞的样子很专业:不,这都是AI合成的结果
想展示自己的完美舞姿吗?你现在只需要一段别人跳舞的视频,和自己的一张照片。最近,来自上海科技大学和腾讯 AI Lab 的新研究着实让很多人跃跃欲试。
机器之心
2019/09/29
8800
你跳宅舞的样子很专业:不,这都是AI合成的结果
【深度学习 | 风格迁移】神经网络风格迁移,原理详解&附详细案例&源码
风格迁移这一想法与纹理生成的想法密切相关,在 2015 年开发出神经风格迁移之前,这一想法就已经在图像处理领域有着悠久的历史。但事实证明,与之前经典的计算机视觉技术实现相比,基于深度学习的风格迁移实现得到的结果是无与伦比的,并且还在计算机视觉的创造性应用中引发了惊人的复兴。
计算机魔术师
2024/01/13
3.9K0
【深度学习 | 风格迁移】神经网络风格迁移,原理详解&附详细案例&源码
用Python快速实现图片的风格迁移
上图是小编在甘南合作的米拉日巴佛阁外面拍下的一张照片,采用风格迁移技术后的效果为:
kbsc13
2019/08/16
1K0
JoJoGAN One-Shot Face Stylization:使用 StyleGAN 创建 JoJo风格人脸头像
JoJoGAN 是一种One-Shot风格迁移模型,可让将人脸图像的风格迁移为另一种风格。
deephub
2022/04/14
6940
JoJoGAN One-Shot Face Stylization:使用 StyleGAN  创建 JoJo风格人脸头像
突然间,ImageNet中的人脸就「变糊」了
作为 AI 领域的知名数据集,ImageNet 曾极大地推动了计算机视觉技术突破。自 ImageNet 论文 2009 年发布以来,它在 Google Scholar 上的引用量高达 26115 次,该论文也获得了 CVPR 2019 的经典论文奖。
机器之心
2021/03/30
5160
突然间,ImageNet中的人脸就「变糊」了
CVPR 2019 | STGAN: 人脸高精度属性编辑模型
classification也要训练的,和auto-encoder一起训练,介样练:
马上科普尚尚
2020/05/18
1.7K0
CVPR 2019 | STGAN: 人脸高精度属性编辑模型
英伟达再出GAN神作!多层次特征的风格迁移人脸生成器
这款新型 GAN 生成器架构借鉴了风格迁移研究,可对高级属性(如姿势、身份)进行自动学习和无监督分割,且生成图像还具备随机变化(如雀斑、头发)。该架构可以对图像合成进行直观、规模化的控制,在传统的分布质量指标上达到了当前最优,展示了更好的插值属性,并且能够更好地将潜在的变差因素解纠缠。
机器之心
2018/12/27
1.2K0
英伟达再出GAN神作!多层次特征的风格迁移人脸生成器
川普跳「鸡你太美」?这么专业,一定是AI合成的!
去年,来自上海科技大学和腾讯 AI Lab 的研究者的研究论文《Liquid Warping GAN: A Unified Framework for Human Motion Imitation, Appearance Transfer and Novel View Synthesis》入选计算机视觉顶会 ICCV 2019。经过一年的努力,该论文所提方法的改进版诞生了。先来看看效果如何?
机器之心
2021/01/06
1.2K0
川普跳「鸡你太美」?这么专业,一定是AI合成的!
周杰伦cos油画、钢铁侠穿越,北大微软新方法让换脸更惊艳
换脸是非常吸引人的一种应用,开发者可以用 VAE 或 GAN 做出非常炫酷的效果。一般而言,换脸会将 A 脸特征换到 B 脸上,同时保留 B 脸的神情或动态。像 FaceSwap 这样开源项目已经能生成非常真实的假脸视频,不过仔细看看仍然会发现有的地方存在模糊,有的地方转换不太自然。
机器之心
2020/02/12
5300
周杰伦cos油画、钢铁侠穿越,北大微软新方法让换脸更惊艳
CVPR 2022 | 实时渲染、可直接编辑,中科大提出高保真人头参数化模型HeadNeRF
机器之心发布 作者:中科大张举勇课题组 《黑客帝国: 觉醒》演示中的灵魂发问:当我们打造出的世界和我们自己的世界同等真实时,那现实到底意味着什么? 还记得去年 12 月,美国电子游戏与软件开发公司 Epic 发布的基于自家虚幻 5 打造的《黑客帝国: 觉醒》的演示吗?Demo 中所展示的主演人物的毛孔毛发级高真实感建模,着实让人惊叹 Epic 的强大技术能力。 据悉,以上演示 Demo 中的人物形象是由 Epic 名下的 MetaHuman Creator 创建生成,该应用可以让用户自由编辑调整目标数字形
机器之心
2022/05/16
9580
CVPR 2022 | 实时渲染、可直接编辑,中科大提出高保真人头参数化模型HeadNeRF
年轻的LeCun、吴恩达长啥样?升级版StyleGAN告诉你
英伟达提出的风格迁移模型 StyleGAN 系列,一直是人们用来进行各类脑洞画图实验的流行工具。从生成二次元「老婆」,照片修图,到人物的卡通化,最近几年基于这种技术的应用不一而足。
深度学习技术前沿公众号博主
2021/07/14
3650
年轻的LeCun、吴恩达长啥样?升级版StyleGAN告诉你
揭秘腾讯微视人脸技术「黑科技」,基于GAN的人脸魔法特效
随着小视频越来越流行,兼具趣味与人物个性的人脸特效成为小视频软件的标配,美颜自不必说,现在的人脸特效可谓“千变万化”,人脸年轻化、变欧美范儿、发型改变、各种表情、胖瘦等。
CV君
2021/03/12
2.3K0
揭秘腾讯微视人脸技术「黑科技」,基于GAN的人脸魔法特效
惊爆!研究提出新颖框架,集成CLIP空间扩展预训练StyleGAN能力,文本引导操作灵活,性能远超现有方法 !
他们面临在分布外图像上的困难。尽管编辑器优化技术非常灵活,但在推理时会带来巨大的计算成本。
AIGC 先锋科技
2025/02/25
1850
惊爆!研究提出新颖框架,集成CLIP空间扩展预训练StyleGAN能力,文本引导操作灵活,性能远超现有方法 !
推荐阅读
人人皆可二次元!小姐姐生成不同风格动漫形象,肤色、发型皆可变
4820
神经风格迁移研究概述:从当前研究到未来方向(附论文和代码)
2.2K0
实时把你的脸变成名画,手机摄像头新玩法
3250
你也可以拥有「宋慧乔妆」,美图MakeupGan妆容迁移算法开启个性化妆容时代
1.5K0
【技术综述】人脸妆造迁移核心技术总结
1.4K0
朋友圈变美靠AI:新型美颜技术实现细粒度颜值提升
7650
基于深度学习的图像真实风格迁移
7K2
你跳宅舞的样子很专业:不,这都是AI合成的结果
8800
【深度学习 | 风格迁移】神经网络风格迁移,原理详解&附详细案例&源码
3.9K0
用Python快速实现图片的风格迁移
1K0
JoJoGAN One-Shot Face Stylization:使用 StyleGAN 创建 JoJo风格人脸头像
6940
突然间,ImageNet中的人脸就「变糊」了
5160
CVPR 2019 | STGAN: 人脸高精度属性编辑模型
1.7K0
英伟达再出GAN神作!多层次特征的风格迁移人脸生成器
1.2K0
川普跳「鸡你太美」?这么专业,一定是AI合成的!
1.2K0
周杰伦cos油画、钢铁侠穿越,北大微软新方法让换脸更惊艳
5300
CVPR 2022 | 实时渲染、可直接编辑,中科大提出高保真人头参数化模型HeadNeRF
9580
年轻的LeCun、吴恩达长啥样?升级版StyleGAN告诉你
3650
揭秘腾讯微视人脸技术「黑科技」,基于GAN的人脸魔法特效
2.3K0
惊爆!研究提出新颖框架,集成CLIP空间扩展预训练StyleGAN能力,文本引导操作灵活,性能远超现有方法 !
1850
相关推荐
人人皆可二次元!小姐姐生成不同风格动漫形象,肤色、发型皆可变
更多 >
LV.0
全球人工智能信息服务
目录
  • 一、Electron的视图容器层级
  • 1.webContents
    • 2. frame
  • 二、基础窗口BrowserWindow
    • 2. 两个BrowserWindow之间的通信
      • (1) 使用 ipcRenderer.sendTo
      • (2) 使用MessagePort
  • 三、独立视图容器BrowserView
    • 1. BrowserView和主进程通信
    • 2. BrowserView和宿主页面通信
  • 四、内嵌DOM标签<Iframe>
    • 1. Iframe和宿主页面通信
    • 2. Iframe和主进程通信
  • 五、内嵌视图容器 <webview> Tag
    • 1. <webview>和宿主窗口通信
    • 2. <webview>和主进程通信
  • 六、ipc通信的封装模式实践
  • 七、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档