一:首先要介绍一下electron
我们可以看一下官网给出的定义:
Electron is a framework for building desktop applications using JavaScript, HTML, and CSS. By embedding Chromium and Node.js into its binary, Electron allows you to maintain one JavaScript codebase and create cross-platform apps that work on Windows, macOS, and Linux — no native development experience required.
Electron由三个主要支柱组成
• Chromium 用于显示网页内容。
• Node.js 用于本地文件系统和操作系统。
• 自定义 APIs 用于使用经常需要的 OS 本机函数。
二:进程通信主要通过主进程和渲染进程
1: 主进程
- 主进程 通过创建浏览器窗口实例来创建个网页。 每一个 BrowserWindow 实例在其渲染过程中运行网页, 当一个 BrowserWindow 实例被销毁时,对应的渲染过程也被终止。
- 主进程 管理所有网页及其对应的渲染进程。
2: 渲染进程
- 渲染进程只能管理相应的网页, 一个渲染进程的崩溃不会影响其他渲染进程。
- 渲染进程通过 IPC 与主进程通信在网页上执行 GUI 操作。 出于安全和可能的资源泄漏考虑,直接从渲染器进程中调用与本地 GUI 有关的 API 受到限制
3: electron的最小组成,来直观感受下主进程和渲染进程的通信过程
4: 主进程和渲染进程如何通信的?
进程之间的通信可以通过 Inter-Process Communication(IPC) 模块进行:ipcMain和 ipcRenderer
ipcMain从主进程到渲染进程的异步通信。
ipcMain
是一个 EventEmitter 的实例。 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。
ipcRenderer 从渲染器进程到主进程的异步通信。 你可以使用它提供的一些方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。
Electron 提供了 IPC 通信模块,主进程的 ipcMain 和 渲染进程的 ipcRenderer, ipcMain、ipcRenderer 都是 EventEmitter 对象。
同步通信示例:
主进程:
const { ipcMain } = require('electron')
// 兼容渲染进程的事件
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
//回复同步信息时,需要设置event.returnValue
event.returnValue = 'pong'
})
渲染进程:
// 渲染进程(网页中)
const { ipcRenderer } = require('electron')
// 触发主进程注册事件
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
异步通信示例
主进程:
const { ipcMain } = require('electron')
// 监听渲染进程的事件
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
// 回调,可以使用event.reply(...)将异步消息发送回发送者, 触发渲染进程的事件
event.reply('asynchronous-reply', 'pong')
})
异步进程:
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
在electron 应用中封装一个主进程和渲染进程的一个异步通信
没有使用封装之前,我们的使用方法是上面的例子,每次都要引用,多人使用同一个方法时候,需要重复多次。
而我们封装的目的就是注册一次事件,可以供多人多次调用。之所以使用异步,也是性能方面的考虑,尽量使主进程保持轻量, 主进程卡,UI就会 阻塞。
我想实现的效果是这样使用:
- cb 是回调函数,并遵循错误优先原则
- windowName表示当前操作的窗口, 传入的参数settingsWindow是窗口名称
// 回调函数遵循错误优先原则
window.callElectronBridge.getWindowMaximized((err, res) => {
if (!err) {
this.isMaximized = res
}
}, 'settingsWindow')
window.addEventListener('resize', () => {
window.callElectronBridge.getWindowMaximized((err, res) => {
if (!err) {
this.isMaximized = res
}
})
}, 'settingsWindow')
我们建立一个 event.js文件,来注册事件
const {ipcRenderer} = require('electron')
const eventsMap = {}
/**
* @param {*} eventName 事件名
* @param {*} cb 主进程执行完后的回调
* @param {*} params 参数
*/
function registEvent(eventName, cb = null, params = {}) {
const stamp = String(new Date().getTime())
const opts = Object.assign({eventName, stamp}, params)
// 以stamp注册当前的cb
eventsMap[stamp] = cb // 注册唯一函数
// 触发一个异步事件
ipcRenderer.send('regist-event', opts) // 发送事件
}
// 主进程通知渲染进程,触发事件回调
ipcRenderer.on('fire-event', (event, arg) => {
// 通过 stamp 找到当前的回调
const cb = eventsMap[arg.stamp]
if (cb) {
if (arg.err) {
cb(arg.err, arg.payload)
} else {
cb(false, arg.payload)
}
// 执行完成之后,注销stamp回调,stamp 代表是当前eventName的一次事件请求, 请求完成之后,就清除
delete eventsMap[arg.stamp]
}
})
module.exports = {
// 原则是渲染进程能做的事情,就不要跟主进程进行通信了,否则需要借助主进程处理。
/**
* 在渲染进程调用,也就是index.html中调用。
* @param {*} cb 操作完成之后的回调,默认是null,因为有些不需要回调
* @param {*} window 当前操作的窗口, 默认当前操作主窗口
*/
// 手动关闭登录窗口(退出程序)
manualClose: (cb = null, windowName = 'mainWindow') => {
// 兼容一个参数的情况
if (typeof cb === 'string') {
windowName = cb
}
registEvent('manual-close', cb, {windowName})
},
windowClose: (cb = null, windowName = 'mainWindow') => {
registEvent('window-close', cb, {windowName})
},
// 打开登录窗口
openLoginWindow: () => {
registEvent('open-login-window')
},
windowMax: (cb = null, windowName = 'mainWindow') => {
registEvent('window-max', cb, {windowName})
},
windowMin: (cb = null, windowName = 'mainWindow') => {
registEvent('window-min', cb, {windowName})
},
getWindowMaximized: (cb = null, windowName = 'mainWindow') => {
// 兼容一个参数的情况
if (typeof cb === 'string') {
windowName = cb
}
registEvent('get-window-maximized', cb, {windowName})
},
// 关闭主窗口,打开登录窗口, 多窗口操作时,直接定义具体操作的窗口即可
mainWindowClose: (cb = null) => {
registEvent('main-window-close', cb, {mainWindow: 'mainWindow', loginWindow: 'loginWindow'})
},
// 关闭登录窗口,然后打开主窗口
loginWindowClose: (cb = null) => {
registEvent('login-window-close', cb, {mainWindow: 'mainWindow', loginWindow: 'loginWindow'})
},
// 新建一个窗口
createNewWindow: (cb = null, windowName, browserWindowOpt, HashRoute) => {
registEvent('create-new-window', cb, {windowName, browserWindowOpt, HashRoute})
},
// 窗口路由跳转
windowRouteChange: (cb = null, params) => {
registEvent('window-route-change', cb, params)
},
/**
* @description 实例之间互相通讯
* @params Object ipcName 实例名称
* @return Function
*/
windowIpc: (cb = null, params) => {
registEvent('window-ipc', cb, params)
}
}
bridge.js 将这些事件挂载到windows上
const event = require('./event.js')
const {ipcRenderer} = require('electron')
const {
manualClose,
windowClose,
windowMax,
windowMin,
getWindowMaximized,
mainWindowClose,
loginWindowClose,
createNewWindow,
windowRouteChange,
windowIpc,
openLoginWindow
} = event
window.callElectronBridge = {
getElectronToken,
manualClose,
windowClose,
windowMax,
windowMin,
getWindowMaximized,
mainWindowClose,
loginWindowClose,
createNewWindow,
windowRouteChange,
windowIpc,
openLoginWindow
}
接下来,我们需要在主进程注册事件,并触发回调
function isPromise(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
}
// 渲染进程调用主进程注册的事件,主进程处理完毕之后,通过主进程向渲染进程发送结果
// 统一采用异步通信方式
// 监听一个异步事件,处理所有的渲染进程触发的事件,请求主进程处理
ipcMain.on('regist-event', (event, params) => {
// 处理异步操作
const nativeEvent = eventsList[params.eventName]
if (nativeEvent) {
const result = nativeEvent(app, params)
// 异步通信,结果处理完,主进程向渲染进程发送消息
if (isPromise(result)) {
result.then(res => {
event.sender.send('fire-event', {
stamp: params.stamp,
payload: res
})
}).catch(err => {
event.sender.send('fire-event', {
stamp: params.stamp,
err
})
})
} else {
event.sender.send('fire-event', {
stamp: params.stamp,
payload: result
})
}
} else {
event.sender.send('fire-event', {
stamp: params.stamp,
err: new Error('event not support')
})
}
})
/**
* 维护主进程要处理的事件列表
* - 可以处理异步事件,通过 promise or async await
*/
const eventsList = {
// 手动关闭登录窗口(退出程序),仅仅用于登录窗口,原因为何: 历史原因
'manual-close': (app, params) => {
if (windowMap[params.windowName]) {
windowMap[params.windowName].close()
delete windowMap[params.windowName]
}
// 关闭登录窗口,直接退出应用
if (process.platform !== 'darwin') {
if (Object.keys(windowMap).length === 0) {
app.quit()
}
}
},
// 关闭窗口
'window-close': (app, params) => {
// 获取操作的窗口
if (windowMap[params.windowName]) {
// 防止异步没有关闭,导致判断失误
windowMap[params.windowName].close() && delete windowMap[params.windowName]
}
// TODO 主窗口关闭,windows app一定要退出,否则没法再次打开,暂时未定位到原因,暂时这么处理, Mac 不影响
if (process.platform !== 'darwin') {
if (Object.keys(windowMap).length === 0) {
app.quit()
}
}
},
// 最大化
'window-max': (app, params) => {
let xWindow = windowMap[params.windowName]
if (!xWindow) {
return
}
if (xWindow.isMaximized()) {
xWindow.restore()
} else {
xWindow.maximize()
}
},
// 最小化
'window-min': (app, params) => {
let xWindow = windowMap[params.windowName]
if (!xWindow) {
return
}
xWindow.minimize()
},
// 获取窗口是否最大化
'get-window-maximized': (app, params) => {
let xWindow = windowMap[params.windowName]
if (!xWindow) {
return
}
return xWindow.isMaximized()
},
// 关闭主窗口,打开登录窗口
'main-window-close': (app, params) => {
let mainWindow = windowMap[params.mainWindow]
let loginWindow = windowMap[params.loginWindow]
mainWindow && mainWindow.close() && delete windowMap.mainWindow
!loginWindow && createLoginWindow()
},
// 关闭登录窗口,然后打开新窗口
'login-window-close': (app, params) => {
let mainWindow = windowMap[params.mainWindow]
let loginWindow = windowMap[params.loginWindow]
loginWindow && loginWindow.close()
!mainWindow && createWindow()
},
'create-new-window': (app, params) => {
if (windowMap[params.windowName] && !windowMap[params.windowName].isDestroyed()) {
windowMap[params.windowName].show()
return
}
let {windowName, browserWindowOpt, HashRoute} = params
let xWindow = new BrowserWindow({
...browserWindowOpt,
webPreferences: {
// 在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径
// preload: process.cwd() + '/src/utils/electron/bridge.js',
preload: preloadFile(),
nodeIntegration: true,
contextIsolation: false,
plugins: true
}
})
windowMap[windowName] = xWindow
// '#/login'
xWindow.loadURL(winURL + HashRoute)
xWindow.on('closed', () => {
xWindow = null
delete windowMap[windowName]
console.log('windowMap =====>', windowMap)
})
},
// 窗口路由跳转
'window-route-change': (app, params) => {
let xWindow = windowMap[params.windowName]
if (!xWindow) {
return
}
xWindow.webContents.send('changeRoute', params)
},
'window-ipc': (app, params) => {
let xWindow = windowMap[params.windowName]
if (!xWindow) {
return
}
xWindow.webContents.send(params.ipcName, params)
},
// 环境切换的时候,重新打开登录窗口
'open-login-window': () => {
createLoginWindow()
}
}
bridge.js 需要在创建新窗口之前引入。
const preloadFile = () => {
if (process.env.NODE_ENV === 'production') {
return `${require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')}/electron/bridge.js`
}
return process.cwd() + '/src/utils/electron/bridge.js'
}
// 当窗口被创建时候,加入这个配置即可
webPreferences: {
// 在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径
preload: preloadFile(),
contextIsolation: false,
nodeIntegration: true,
plugins: true
}