阅读 628

Electron快速入门,聊聊跨进程通信那些事儿

前言

有句话叫做“有需求就有市场”,技术领域也同样是如此。在过往的前端领域之上,当面临需要涉及操作系统的时候,前端coder往往显得力不从心。这便是桌面应用的需求造就了 Electron 的到来。

什么是Electron?

简介

打开官网,我们便可以看到其介绍,使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。顾名思义,我们可以完全自主控制地去构建跨平台桌面应用了,无需强依赖于桌面应用原生开发人员,有效降低沟通成本,再也不用求爷爷告奶奶去协调资源,完全可以自主访问以往受限的操作系统相关底层API。

当然,这也并不意味着百利而无一害,毕竟获得更多 power 的同时,也会承担更多 Risk。

优秀应用

  • Visual Studio Code

  • Atom

  • Postman

  • 社交通讯 WhatsApp

  • MongoDB 桌面管理工具 Compass

  • 接口管理工具 Apifox

  • ... ...

技术选型

Electron核心组成

Electron 是基于 ChromiumNode 实现的,才使得我们可以无缝轻松使用其开发跨平台桌面应用,降低了学习门槛,更加轻松上手开发。

为了弥补前端访问系统API方面的不足,Electron 内部对系统API进行了封装,相关譬如系统对话框、系统托盘、系统菜单、剪切板等。而其他诸如网络访问控制、本地文件系统等则由 Node 提供底层支持。

Electron 通过各操作系统之间的消息循环打通 Node 和 Chromium 的事件循环,保证了其两者的松耦合。进而推出了主进程渲染进程的概念。

Electron 起了一个新到安全线程去轮询, 当 Nodejs 有新的事件之后,通过 PostTask 转发到 Chromiums 的事件循环当中,完成 Electron 的事件融合

具体相关源码:github.com/electron/el…

Electron 工作机制

啥也不说,先上个图

左侧是我们传统开发中前端人员所能控制展示的区域,而当基于 Electron 去开发桌面应用时,我们可控区域如右侧所示,全部交由前端自主开发。

而 Electron 开发中,页面不再是用户手动输入打开,而是开发着自主硬编码好的。

Electron应用程序主要分为主进程渲染进程两个部分,即对应着右侧图中上下两个部分。

进程

一个 Electron 应用程序由一个主进程(有且只有一个) + 多个渲染进程组成。

主进程

功能:桥梁作用,连接操作系统和渲染进程,负责管理所有窗口及其对应的渲染进程。

  • 有且只有一个,整个应用入口

  • 创建、管理渲染进程

  • 控制应用生命周期

  • 使用 NodeJS 特性

  • 调用操作系统 API

  • ...

渲染进程

功能:负责完成渲染页面、接收用户输入、相应用户交互等工作。

  • 渲染页面

  • 使用部分 Electron 模块 API

  • 使用 NodeJS 特性

  • 一个应用可存在多个渲染进程

  • 控制用户交互逻辑

  • 访问 Dom API

核心模块归属情况

上图为笔者整理的常用模块归属情况,详细主进程、渲染进程会在后续的实战部分进行部分讲解。

IPC 通信

大概了解完两个进程的功能之后,我们接下去该考虑一下这两者之间,是如何进行协调通信的。

Electron 中通过提供ipcMain、ipcRenderer来作为主进程、渲染进程之间的通信桥梁。

从接口定义中不难推断出其管道IPC是通过继承 EventEmitter 来实现IpcMain、IpcRenderer,并拓展了其他工具类方法。纵使翻阅 electron 源码也是如此,感兴趣的同学可以自己去研究研究,这里只做简单了解。

讲到这里,对于主进程和渲染进程的通信就变得十分容易理解了,通过管道IPC,采用熟知的发布订阅模式进行两者之间的通信。

窗口获取

  • BrowserWindow.getFocusedWindow(): 获取当前激活状态窗口

  • remote.getCurrentWindow(): 获取当前渲染进程关联窗口

  • BrowserWindow.fromI(id): 根据id获取窗口实例

  • BrowserWindow.getAllWindow(): 获取所有窗口

remote

在讲实际项目基本操作之前,先介绍一下一个比较特殊的 remote 模块

remote:这是一个 Electron 内部的模块,渲染进程可以通过此模块访问到主进程的模块、对象和方法。包括在渲染进程创建窗口、创建菜单等类似本应该由主进程完成的操作通过 remote 依然可以在渲染进程进行完成。前提是创建窗口的时候,开启了 nodeIntegration 配置,让渲染进程有能力去访问 Node.js 相关API。但是其背后的机制是一样的,通过通知主进程,主进程接收消息后再进行相关操作,然后把相关的实例以远程对象形式返回到渲染进程。

局限性

当然,remote虽然极大便利了开发者,但是也带来了一些局限性

  • 性能损耗大:跨进程操作

  • 制造混乱:异步导致执行顺序错乱

  • 制造假象:代理对象导致数据混乱

  • 安全问题:恶意代码攻击

在不久的将来,remote 模块将从 electron 内部移除,但是还很漫长,保持关注即可。

实战

从这里开始,我们将从实际的项目基本功能演练进行相关核心模块的使用演示。

进程互访

渲染进程TO主进程

其核心原理是因为暴露了 remote 模块,让开发者可以相对随心所欲的进行访问。

比如我们在主进程里想要获取应用程序的程序路径,我们可以在主进程这么获取:

import { app } from 'electron' //  获取应用程序路径 const ROOT_PATH = app.getAppPath() 复制代码

而在渲染进程中,有了 remote 模块,此类简单属性获取也变得更加方便:

const { app } = require('electron').remote //  获取应用程序路径 const ROOT_PATH = app.getAppPath() 复制代码

然鹅,其不仅可以访问主进程的属性,还可以调用相关方法,再举个栗子:

const { remote } = require('electron') //  渲染进程打开开发者工具 remote.getCurrentWindow().webContents.openDevTools() 复制代码

结论:通过 remote 模块,我们可以方便的访问主进程的模块、对象和方法。

主进程TO渲染进程

渲染进程是由主进程控制的,通过创建的渲染进程的窗口win.webContents对象,可以轻易地访问渲染进程相关内容。

这里官网的相关事例说明相对完善,可以自行查看。

const {BrowserWindow} = require('electron') let win = new BrowserWindow({width: 800, height: 600}) win.loadURL('http://github.com') //  获取当前网页窗口的网址 let currentURL = win.webContents.getURL() 复制代码

进程通信

其核心即为管道IPC通信,上文有所说明,不再赘述。

主进程TO渲染进程

主要有两种方式进行通信:

  • ipcMain 接收渲染进程消息

  • webContents 发送给渲染进程

比方说呢,项目里我有一个地方需要监听用户通过 a 标签打开外链,但是我又不想它重新创建一个窗口,所以需要系统干预进行处理。

我的解决方案就是通过 进程通信 + shell 模块来通过系统默认浏览器来打开目标链接。

<a href="www.baidu.com" target="_blank">百度</a> 复制代码

const { ipcMain, shell } = require('electron'); ipcMain.on('open-url', (event, url) => {   //  'open-url' 为管道消息名称   //  event 为消息发送相关信息    //  event.sender 为渲染进程的webContents对象事例   //  url 为传递参数      //  通过系统默认浏览器打开目标外链   shell.openExternal(url) }) 复制代码

如果此时到这里之后,我们想告诉渲染进程我们已经成功接收并执行了,也就是回调,那么我们就可以通过渲染进程事例进行对渲染进程消息通知:

方法1: webContents 直接回传

const { ipcMain, shell } = require('electron'); const win = new BrowserWindow({   //... ... }) ipcMain.on('open-url', (event, url) => {   //  ... ...   //  通过系统默认浏览器打开目标外链   shell.openExternal(url)   //  向渲染进程进行消息通知   win.webContents.send('ready-open-url') }) 复制代码

方法2: ipcMain.on 接收消息通知时,event.sender 为渲染进程的webContents 对象事例,我们也可以直接进行消息通知:

const { ipcMain, shell } = require('electron'); const win = new BrowserWindow({   //... ... }) ipcMain.on('open-url', (event, url) => {   //  ... ...   //  通过系统默认浏览器打开目标外链   shell.openExternal(url)   //  向渲染进程进行消息通知   event.sender.send('ready-open-url') }) 复制代码

方法3: ipcMain.on 接收消息通知时,event 提供reply方法,相应消息给来源渲染进程,本质上与方法2逻辑一致。

//  ... ...  ipcMain.on('open-url', (event, url) => {   //  ... ...   //  通过系统默认浏览器打开目标外链   shell.openExternal(url)   //  向渲染进程进行消息通知   event.replay('ready-open-url') }) 复制代码

渲染进程TO主进程

主要是通过 ipcRenderer 模块进行向主进程进行消息通知。

还是拿上面的例子来说,打开外链,那么我们就需要在渲染进程中进行向主进程通知,我需要打开某个外链。具体如下:

本事例为在 Vue 中的实践

const { ipcRenderer } = require('electron') const links = document.querySelectorAll('a[href]') links.forEach(link => {   link.addEventListener('click', e => {     const url = link.getAttribute('href')     e.preventDefault()     ipcRenderer.send('open-url', url)   }) }) 复制代码

当然这是一个异步的消息队列~ 可能在某些需求场景下,我们需要传递的是同步消息, 那么我们只要在主进程里直接设置 returnValue 的值即可,而渲染进程不需要再重复监听。

还是拿上面的打开外链做个演示说明:

//  主进程 ipcMain.on('open-url', (event, url) => {   //  通过系统默认浏览器打开目标外链   shell.openExternal(url);   //  设置返回值   event.returnValue = 'success'; }) //  渲染进程 const returnVal = ipcRenderer.sendSync('open-url', url); console.log(returnVal) // success 复制代码

当然,同步通信会阻塞渲染进程,孰轻孰重需要谨慎选择~

渲染进程TO渲染进程

当我们程序相对复杂,创建了多个渲染进程的时候,就容易出现多个渲染进程之间相互通信的场景。

解决方案其实也是显而易见的,既然是一个爹(主进程)生的,那么直接通过主进程进行一个过渡中转,就可以实现双方的一个通信了。毕竟窗口的创建往往就是在主进程里完成的,其持有所有窗口的实例,只要拿到目标窗口的id即可进行通信。

每个窗口 webContents.getProcessId() 或者 webContents.id 即可获得对应窗口的id。

伪代码如下:

//  win1窗口发送消息 ipcRenderer.sendTo(win2.webContents.id, 'send-msg', params1, params2) //  win2窗口接收消息 ipcRenderer.on('send-msg', (event, params1, params2) => {   //  ... ... }) 复制代码

其中 ipcRenderer.sendTo 中,第一个参数为目标窗口id,第二个参数为管道消息名称,其余为传递参数。

当然,需要发送消息给到的目标窗口是打开的状态,否则可就接受不到了。

到此,三种场景的进程通信介绍完毕了。

有个小注意事项⚠️需要关注一下:

进程之间的通信过程中,发送的json对象都会被序列化和反序列化,所以传递的时候需要注意其方法和原型链上的数据是不会被传递的。

这一点,跟小程序 setData 进行视图层和逻辑层数据传输是十分类似的,evaluteJavascript 所实现的,最终都转化为字符串传递。

搭建开发环境

electron的安装,兴许是一个漫长的过程,这里强烈建议大家有条件的话能够科学上网,可以省掉不少破事。当然没有的话,也没关系(假的),我们也有解决方案。

包管理工具的话,大家就各自选择了,npm/yarn 都可以,这里以 yarn 进行说明。

初始化项目

yarn init 复制代码

electron 依赖包有点大,默认从github下载,所以巨艰难。 设置镜像

yarn config set set ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ 复制代码

全局安装 electron

yarn global add electron 复制代码

这是正常安装成功 node_modules/electron 里应有的文件结构,如果后续运行报错了,大概率就是安装失败了。

可以选择手工操作处理此类问题,如果你上网不够科学的话~

解决方案:

  • /node_modules/electron/ 目录下创建path.txt

    • win输入:electron.exe

    • mac输入:Electron.app/Contents/MacOS/Electron

  • /node_modules/electron/ 目录下创建dist目录

    • 版本包地址 下找到对应版本解压到dist目录

至此,electron 安装就算是成功了。

package.json 中配置“main” 入口文件即 electron 的启动文件,即主进程的相关代码。

下面贴一个以 Vue 框架进行开发的项目文件结构图。

引入现代框架

通过引用模板项目即可快速入手开发,一个字-香!

Angular

  • 官方维护版本:github.com/angular/ang… (缺点:停更许久)

  • 社区活跃版本:github.com/maximegris/…

React

  • electron-react-boilerplate

该项目模板汇集了 Electron、React、Redux、React Router、webpack、React Hot Loader等,对入手尝鲜 Electron 来说,简直是不要太香。

Vue

  • Vue CLI Plugin Electron Builder:github.com/nklayman/vu…

  • electron-vue: github.com/SimulatedGR… (也已基本停更)

通过引用前端三剑客框架,我们就可以快速投入到 Electron 的GUI应用开发之中,当然如果你执着于 jQuery,也是可以引用开发的,只是不建议而已,这就涉及到 Electron 性能相关了,这里不再展开。

发布打包

设置图标

  • 准备一张1024*1024尺寸的png图 放在public下

  • 安装 electron-icon-builder 插件

yarn add electron-icon-builder --dev 复制代码

容易安装失败 多装几次(科学上网)

  • package.json 添加指令配置

"build-icon": "electron-icon-builder --input=./public/logo.png --output=build --flatten" 复制代码

  • 执行

yarn build-icon 复制代码

生成应用图标到对应的build文件夹

打包安装包

yarn electron:build 复制代码

直到 Done 出来之后也就大功告成了~

一个 electron 应用也就生成好了。

核心模块演示

设置全局变量

项目开发中,经常有个需求便是主题换肤,在尝试过程中自然就想到了 mac 下的系统主题切换。由此来演示下如何设置全局变量,并在渲染进行获取。

主进程

import { nativeTheme } from 'electron'  /** 添加全局属性 * */ global.selfConfigs = {   nativeTheme: () => nativeTheme.shouldUseDarkColors } 复制代码

渲染进程

const nativeTheme = require('electron').remote.getGlobal('selfConfigs').nativeTheme() 复制代码

当然,直接通过 remote 调用 nativeTheme 也是可以的,just a 栗子。

脚本注入

  • 通过 preload 配置项,进行脚本注入

let win = new BrowserWindow({   webPreferences: {     preload: jsFilePath,     nodeIntegration: true   } }) 复制代码

  • 通过 executeJavaScript 注入脚本

比方说,在 window 上添加自定义属性

主进程

let win = new BrowserWindow({   //  ... }) win.webContents.executeJavaScript(`   window.onlyConfig = {a:1,b:2} `) 复制代码

渲染进程

console.log(window.onlyConfig) //  {a:1,b:2} 复制代码

实现系统消息通知

有两种可实现方式,两种方式的使用方法区别不大。

  • HTML API 发送消息通知,缺点就是需要用户授权同意之后

  • 主进程直接发送系统消息

    const { Notification } = this.$electron.remote     const notification = new Notification({       title: '新建通知', BVVv    body: '您新建了一个md文档,请点击查看'     })     notification.show()     notification.on('click', () => {}) 复制代码

实现系统托盘及相关菜单

系统托盘由 Tray 模块提供,用于添加托盘图标和上下文菜单至通知栏。

啥也不说了,先上大头贴

实现原理相对简单,通过定时器刷新托盘图标,并添加相对应的上下文菜单进行逻辑操作即可,更多功能可以自行DIY。

/** 添加系统托盘 * */   let toggleSwitch = true; let toggleFlag = false; let timer   const icon1 = path.join(__dirname, '../public/icon.png')   const icon2 = path.join(__dirname, '../public/icon2.png')   tray = new Tray(icon1)   tray.setToolTip('Electron 系统托盘')   tray.on('click', () => {     console.log('托盘单击')     win.isVisible() ? win.hide() : win.show()   })   tray.on('right-click', () => {     const menuConfig = Menu.buildFromTemplate([       {         label: toggleSwitch ? '开启闪烁图标' : '关闭闪烁图标',         click: () => {           if (toggleSwitch) {             timer = setInterval(() => {               if (toggleFlag) {                 tray.setImage(icon2)               } else {                 tray.setImage(icon1)               }               toggleFlag = !toggleFlag             }, 600)           } else {             tray.setImage(icon1)             clearInterval(timer)           }           toggleSwitch = !toggleSwitch         }       },       {         label: '退出',         click: () => app.quit()       }     ])     tray.popUpContextMenu(menuConfig)   })   /** 添加系统托盘 * */ 复制代码

实现系统右键菜单

以往,我们处理的思路是根据用户右键所在鼠标坐标生成一个右键菜单,相对麻烦并且还需要考虑边界状态。好比如编写此篇文章所用到的 mdnice ,即是用此方案使用了自定义右键菜单。

通过 electron 暴露的 screen 模块,获取到当前鼠标所在位置

window.oncontextmenu = () => {   const point = require('electron').screen.getCursorScreenPoint(); } 复制代码

而在 electron 里,我们可以直接自定义系统右键菜单,兼容性更佳。

//  监听右键菜单触发   win.webContents.on('context-menu', (event, params) => {     const selectEnabled = !!params.selectionText.trim().length     const template = [       {         label: '为当前页面生成二维码',         click: () => {           console.log(`当前页面地址为:${params.pageURL}`)         }       }     ]     if (selectEnabled) {       template.unshift(...[{         label: '复制',         role: 'copy',         visible: () => !selectEnabled       },       {         label: '剪切',         role: 'cut'       }])     }     const RightMenu = Menu.buildFromTemplate(template)     RightMenu.popup()   }) 复制代码

最终实现如下基础效果:

常见问题

npm 安装electron不成功

解决方案: 通过cnpm淘宝镜像安装 避免安装失败

报错 require is not defined

原因:electron12以后默认没法在渲染进程中引入Nodejs模块

解决方案:

找到 ./background.js里的 new BrowserWindow 添加配置项 nodeIntegration 设置为 true

导入electron.remote后,提示undefined

原因:  在electron10版本之后,remote默认关闭,需要手动开启

解决方案:

找到 ./background.js里的 new BrowserWindow 添加配置项

const win = new BrowserWindow({     width: 800,     height: 600,     webPreferences: {       enableRemoteModule: true, // 解决remote为undefined问题       nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION     }   }) 复制代码

mac 下快捷键失效的问题

发现在mac下,本该熟练的复制、剪切、粘贴等快捷失效了。你说说,作为一名合格的 CV 工程师,这你能忍?

这时候就想起尤大的表情包,看文档!

马不停蹄一股脑加了段代码,瞬间感觉牛逼哄哄

  // 判断 mac 下 注册快捷键   if (process.platform === 'darwin') {     const contents = win.webContents     globalShortcut.register('CommandOrControl+C', () => {       contents.copy()     })     globalShortcut.register('CommandOrControl+V', () => {       contents.paste()     })   } 复制代码

后面发现这个方案并不是有效的解决方案,注册完快捷键后发现 electron 占据了系统的原有快捷键,这才发现除了 electron 以外的其他应用,这些快捷键都失效了~

后面仔细研究一番之后,通过判断应用是否激活状态,来进行相关快捷键的注册/注销.

//  处理系统本身的快捷键 复制 全选 等   win.on('focus', () => {     // mac下快捷键失效的问题     if (process.platform === 'darwin') {       globalShortcut.register('CommandOrControl+C', () => {         console.log('注册复制快捷键成功')         contents.copy()       })       globalShortcut.register('CommandOrControl+V', () => {         console.log('注册粘贴快捷键成功')         contents.paste()       })       globalShortcut.register('CommandOrControl+X', () => {         console.log('注册剪切快捷键成功')         contents.cut()       })       globalShortcut.register('CommandOrControl+A', () => {         console.log('注册全选快捷键成功')         contents.selectAll()       })     }   })   win.on('blur', () => {     globalShortcut.unregister('CommandOrControl+C') // 注销键盘事件     globalShortcut.unregister('CommandOrControl+V') // 注销键盘事件     globalShortcut.unregister('CommandOrControl+X') // 注销键盘事件     globalShortcut.unregister('CommandOrControl+A') // 注销键盘事件   }) 复制代码

windows 下控制台出现中文乱码

常见的gb2312为936 utf8为65001 配置执行命令即可解决

"start": "chcp 65001 && electron ." 复制代码

Vue 构建的 history 模式项目打包空白

history 模式匹配不到对应静态资源,需要做一层处理,或者router 的 mode 切换为 hash 即可。

总结

electron 优势

  • 上手门槛低

  • 开发周期短

electron 不足

  • 打包后应用体积过大

  • 版本发布过快

  • 安全性问题

  • 资源消耗较大

  • 平台上架难


前端想象力

  • 无浏览器兼容问题

  • 支持 ES 高级语法

  • 无跨域问题

  • 支持 Node.js


作者:前端Sneaker
链接:https://juejin.cn/post/7023260494702051342


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐