前言
由于自己在使用 electron 开发一个类似微信的桌面客户端App,开发这个App的目的纯粹是为了总结一下自己对于 electron 的积累总结吧,先贴出项目的源码地址:github.com/1111mp/elec… 。目前整个项目的基础架构都实现差不多了,包括整个应用的数据持久化(使用的是 sqlite,因为我比较喜欢用 sequelize)、多语言、主题设置、多窗口之间的通信方式 和 带有底部上滑动画的弹窗通知 等,总得来说项目的质量还可以吧,有兴趣的同学可以提前去看一下代码。第一次写这种文章,望谅解。然后废话不多说,直奔主题吧。
主/渲染进程
electron 的主进程和渲染进程:ipcMain, ipcRenderer 我想这部分应该不用介绍了,就贴出官方文档链接了,不了解的同学可以先去了解一下,然后再接着往下看。
通信方式的实现
基于主/渲染进程的事件收发机制来实现多窗口之间的通信
这种通信方式的原理就是基于主进程 ipcMain 的 on 方法向主进程注册一个事件,然后在窗口页面中通过 send 方法向渲染进程发送事件,然后触发到主进程中之前注册过的事件,事件名都是各自方法的第一个参数,用起来其实就跟 EventBus 一样,以下是官方的例子:
// 在主进程中.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.reply('asynchronous-reply', 'pong')
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
//在渲染器进程 (网页) 中。
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
然后我们来实现多窗口的通信方式,比如我们有两个窗口: A 和 B,然后我们在 B 窗口获取 A 窗口的数据,以下(伪代码):
// 在主进程中.
const { BrowserWindow, ipcMain } = require('electron');
let A = new BrowserWindow({...options}); // options为一些new BrowserWindow的配置
A.loadURL('A.html');
let B = new BrowserWindow({...options}); // options为一些new BrowserWindow的配置
B.loadURL('A.html');
function getDataFromMainWindow(name, callback) {
ipcMain.once('get-data-success', (_event, error, value) => {
callback(error, value)
});
A.webContents.send('get-data');
}
// 主进程相当于一个中间层 做转发
ipcMain.on('get-data', event => {
if (A && A.webContents) {
getDataFromA(name, (error, value) => {
const contents = event.sender;
if (contents.isDestroyed()) {
return;
}
contents.send('get-success-data', error, value);
});
}
});
// A 页面中
const { ipcRenderer } = require('electron');
// 注册 get-data 事件,name为需要获取的数据的名称
ipcRenderer.on('get-data', (event, name) => {
let data = getDatafromA(name); // 通过name获取到A页面中的数据 伪代码
ipcRenderer.send('get-data-success', data);
})
// B 页面中
const { ipcRenderer } = require('electron');
const getData = (name, a) => {
return new Promise((resolve, reject) => {
ipcRenderer.once('get-success-data', (event, value) => {
if (error) {
return reject(error);
}
return resolve(value);
});
ipcRenderer.send('get-data');
})
}
总结一下就是:如果 B 想要获取 A 的数据,B先想主线程发送一个事件get-data,然后主进程收到这个事件之后,想A发送一个事件get-data, 然后页面A中收到这个事件之后,就开始通过参数name去获取到自己页面中对应的数据,然后向主进程发送事件get-success-data,并把对应的数据以参数形式传递过去,然后主进程接收到A发送的事件之后,拿着A给的数据又向B发送一个事件get-success-data,然后B收到这个事件之后就能拿到自己想要的数据了。
到这里你肯定会想,我就想拿个数据还要这么麻烦,而且这个过程不耗时吗,这样的话我的应用不就会表现的很卡。。。我看过很多别人开源的应用,好像基本上都是这么实现的,但是我受不了。
在此基础上的改进
刚刚说了一种通信方式,但是整个过程比较繁琐,性能不能保证,那么有没有办法来优化呢,是有的: 还是上面那个场景,页面 B 获取页面 A 的数据,当页面 A 加载渲染展示的时候,我们可以通过一个事件把一些需要共享的数据提前发送到主进程,然后主进程通过一个变量储存,然后后续如果 B 页面想要获取 A 的对应数据的时候,只需要 B 发送一个事件给主进程,然后主进程直接把自己已经储存的数据给返回给 B就行了。这种方式机制没变,但是从过程线来看,整个过程缩短了 1/2,也属于一种优化,代码就不贴了,大家应该都懂。
但是接下来有个问题了,如果 A 页面的数据有变化了怎么办,B获取到的数据不还是之前旧的数据吗,要解决这个问题,那么肯定需要维护一套页面 A 和主进程数据的同步机制,又是一个坑。 就没有直接点的办法吗,答案是肯定有的。
通过 webContents 的 executeJavaScript 方法实现通信
executeJavaScript 的文档链接:www.electronjs.org/docs/api/we… 这个 api 的用处大致就是可以在主进程中让对应的 webContent 去执行一段 js 脚本。这样一来不就跟移动端混合开发一样了吗,就是 jsBridge 那一套东西,关于jsBridge原理可以参考这个文章 JSBridge的原理,这里就不过多介绍了。当然我们所有需要共享的数据还是放在window对象下,好操作。那么接下来还是那个场景:页面 B 获取页面A的数据:
这时候我们可以这么实现了(伪代码)
// 主进程中
const { ipcMain } = require('electron');
/** 从A的webContent获取挂载在全局window对象上的对应key的数据 */
async function getDataFromA(keys: string[]) {
let script = '{';
if (keys.length === 0) {
script = `${keys[0]}}`;
} else {
keys.forEach((key: string, index: number) => {
if (index === keys.length - 1) {
script += `${key}}`;
} else {
script += `${key},`;
}
});
}
// 这里为什么这么拼 js 脚本 是因为在执行js脚本的时候会把对应的字符串当作变量
// `
// let res = {}
// ${keys}.forEach(key => {
// res[key] = window[key]
// })
// Promise.resolve(res)
// `
// 这种写法执行的时候会报错
if (A && A.webContents) {
return A.webContents.executeJavaScript(`Promise.resolve(${script});`).then((data) => {
return data;
}).catch((err) => {
throw new Error(err);
});
} else {
throw new Error('Cannot find the mainWindow!');
}
}
// 同步方法 支持同时获取多个key的数据
ipcMain.on('get-data', async (event, name) => {
const data = await mainProcess.getDataFromMainWindow(keys);
event.returnValue = data;
});
// 异步方法 支持同时获取多个key的数据
ipcMain.handle('get-data-async', async (event, name) => {
const data = await mainProcess.getDataFromMainWindow(keys); return Promise.resolve(data);
})
然后在页面 B 中这样就可以获取到数据了
const { ipcRenderer } = require('electron');
let data = ipcRenderer.send('get-data', 'theme')
console.log(data)
// or
ipcRenderer.invoke('get-data', ['theme']).then(res => {
console.log(res)
})
当然如果 B 页面想要调用 A 页面中的方法也很容易实现:
const { ipcMain } = require('electron');
/** 调用A的webContent全局的funcname方法 */
function invokeMainWindowFunc({ funcname, args }) {
if (A && A.webContents) {
A.webContents.executeJavaScript(`window.${funcname}(${JSON.stringify(args)})`);
}
}
// 只需要加上如下代码
ipcMain.on('invoke-func', (event, data) => {
invokeMainWindowFunc(data)
})
// 然后B中调用
const { ipcRenderer } = require('electron');
ipcRenderer.send('invoke-func', {
funcname: 'setAppTheme',
args: {theme: 'dark'}
})
这样实现的话 上面的那些问题都解决了
最后
整个实现的完整的源码都在最开始提到的项目(electron_client)中有,主要在app/main-process/index.ts文件中,不过因为我整个项目在代码层面都做了一层封装,可能需要先了解一些项目代码,才容易看懂。
第一次写文章,比较菜,多多包涵。希望你们看懂了,然后u也有所收获。
如果觉得项目 electron_client 还不错还麻烦 star 一下。谢谢。