electron bridge 通信封装

一:首先要介绍一下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的最小组成,来直观感受下主进程和渲染进程的通信过程

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
  }

你可能感兴趣的:(electron bridge 通信封装)