一份关于Electron桌面开发指北

发布时间:2024年01月24日

Electron教程
本文涉及到的只是在自动化工具使用的部分,具体详情请前往官方文档查阅。
好的,我会尽力把这篇关于Electron和TypeScript的文章翻译成中文:

如何使用ipcMain和ipcRenderer

在使用Electron、TypeScript和Electron时,我遇到了一些问题。在我的模板(el3um4s/memento-svelte-electron-typescript)的第一个版本中,我满足于一个可以工作的结果。但是这不是最好的结果。然后我通过做一些改进来修改代码。我不知道我的建议是不是最优解,但肯定我更喜欢它比第一个版本。

在第一个版本中,有一些主要问题。一个是在Svelte端(我在本文的结尾谈到它),两个是在Electron端。简而言之,所有的Electron逻辑都被压缩到两个大的文件中,“index.ts”和“preload.ts”。更重要的是,逻辑是混合的。我使用“preload.ts”来确保Svelte和Electron之间的接口,但函数在另一个文件上。另一方面,index.ts本身不得不管理图形界面创建和所有来自Svelte的通信。

文件夹结构

在一个小项目中,比如我上传的仓库,这不是大问题。但是它不需要很长时间就会增加整体复杂性,并制造一团漂亮的“意大利面条代码”。所以我决定退后一步,让一切都更模块化。不仅仅是两个文件,我又创建了几个文件,每个文件都有一个任务要执行:

> src
  > frontend 
  > electron
    - index.ts
    - mainWindow.ts 
    - preload.ts
    > IPC  
      - systemInfo.ts
      - updateInfo.ts 
      > General 
        - channelsInterface.ts
        - contextBridge.ts
        - IPC.ts

乍一看,这个项目的复杂性似乎没有什么理由增加,但我认为它是值得的:

  • index.ts仍然是主文件,但只负责调用必要的函数,而不是定义它们

  • preload.ts仍然允许在BrowserWindow和Electron之间进行安全通信,但只包含可用频道的列表

  • mainWindow.ts包含一个Main类来构建Electron中的一个BrowserWindow

Main类

将Main类保持独立允许我保持index.ts里面的代码更干净。

首先,我需要一些基本设置的默认值和一个构造函数

const appName = "MEMENTO - Svelte, Electron, TypeScript";

const defaultSettings = {
  title:  "MEMENTO - Svelte, Electron, TypeScript",
  width: 854,
  height: 480
}

class Main {
    settings: {[key: string]: any};

    constructor(settings: {[key: string]: any} | null = null) {
        this.settings = settings ? {...settings} : {...defaultSettings}
    }
}

然后,我需要一个方法来创建主程序窗口


import { app, BrowserWindow } from 'electron';
import path from "path"

class Main {
    //...
    createWindow() {
        let settings = {...this.settings}
        app.name = appName;
        let window = new BrowserWindow({
        ...settings,
        show: false,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            enableRemoteModule: true,
            preload: path.join(__dirname, "preload.js")
        }
        });

        window.loadURL(path.join(__dirname, 'www', 'index.html'));
        window.once('ready-to-show', () => {
        window.show()
        });

        return window;
    }
    //...
}

然后是一些附加方法:

class Main {
    //...
    onWindowAllClosed() {
        if (process.platform !== 'darwin') {
        app.quit();
        }
    }

    onActivate() {
        if (!this.window) {
        this.createWindow();
        }
    }
    //...
}

现在我把它们都放在构造函数中:

class Main {
    //...
    window!: BrowserWindow;

    constructor(settings: {[key: string]: any} | null = null) {
        this.settings = settings ? {...settings} : {...defaultSettings}

        app.on('ready', () => { 
            this.window = this.createWindow(); 
        });
        app.on('window-all-closed', this.onWindowAllClosed);
        app.on('activate', this.onActivate);
    }
    //...
}

如果我想的话,我可以在这里停止。但是我还需要一件事来拦截窗口创建的时刻。我可以在index.ts中使用一个app.on(“browser-window-created”,funct)事件,或者在Main类本身内部。相反,我更喜欢使用一个自定义的EventEmitter(链接)。


import EventEmitter from 'events';

class Main {
    //...
    onEvent: EventEmitter = new EventEmitter();
    //...
    constructor() {
        //...
         app.on('ready', () => { 
            this.window = this.createWindow(); 
            this.onEvent.emit("window-created");
        });
        //...
    }
    //...
}

最后,我导出Main类以便我可以使用它:

export default Main;

在进入IPC文件夹之前,我在index.ts中使用Main类



import Main from "./mainWindow";

let main = new Main();

main.onEvent.on("window-created", ()=> {
    //...
});

我已经插入了main.onEvent.on(“window-created”,funct):我将使用它来定义启动时要执行的操作。这是与ipcMain、webContents和autoUpdater相关的代码。我把代码放在两个不同的文件中,“systemInfo.ts”和“updateInfo.ts”。这些只是如何使用模板的两个例子,可以根据意愿替换、删除或修改。当然,你也可以把它们作为基础来添加其他功能和频道。

channelsInterface,contextBridge和IPC

General文件夹中的文件用于保持前面的文件清晰。

channelsInterface.ts包含一些接口的定义:

export interface APIChannels {
    nameAPI: string,
    validSendChannel: SendChannels,
    validReceiveChannel: string[]
}

export interface SendChannels {
    [key: string]: Function
}

主要文件是IPC.ts。IPC类是Electron和HTML页面之间进程间通信的基础:

import { BrowserWindow, IpcMain } from "electron";
import { APIChannels, SendChannels } from "./channelsInterface";

export default class IPC {
    nameAPI: string = "api";
    validSendChannel: SendChannels = {};
    validReceiveChannel: string[] = [];

    constructor(channels: APIChannels) {
        this.nameAPI = channels.nameAPI;
        this.validSendChannel = channels.validSendChannel;
        this.validReceiveChannel = channels.validReceiveChannel;
    }

    get channels():APIChannels {
        return {
            nameAPI: this.nameAPI,
            validSendChannel: this.validSendChannel,
            validReceiveChannel: this.validReceiveChannel
        }
    }

    initIpcMain(ipcMain:IpcMain, mainWindow: BrowserWindow) {
        if (mainWindow) {
            Object.keys(this.validSendChannel).forEach(key => {
                ipcMain.on(key, async( event, message) => {
                    this.validSendChannel[key](mainWindow, event, message);
                });
            });
        }
    }
}

IPC有三个属性。nameAPI用于定义从前端内部调用API的名称。然后是有效频道的列表。

与第一个版本相比,Electron的频道不仅仅是一个名称列表,而是一个对象。对于每个键,我都分配一个要调用的函数(我稍后会解释)。只有两个方法,get channels和initIpcMain - 我很快就会用到它们。

第三个通用文件包含generateContextBridge()函数的定义。它以IPC类型的对象数组为参数,并使用它们生成Electron和Svelte之间通信的安全通道列表。

我的解释可能相当令人困惑,但幸运的是这些文件不应该被改变。

systemInfo.ts

此外,更有趣的是理解如何使用它们。这就是为什么我包括了两个例子。我先从最简单的systemInfo.ts开始。这个模块只有一个出口和一个入口通道,用它来请求和获取一些关于Electron、Node和Chrome的信息。

首先,我导入刚才谈到的文件:

import { SendChannels } from "./General/channelsInterface";
import IPC from "./General/IPC";

所以我开始创建一些支持变量:

const nameAPI = "systemInfo";

// to Main
const validSendChannel: SendChannels = {
    "requestSystemInfo": requestSystemInfo
};

// from Main
const validReceiveChannel: string[] = [
    "getSystemInfo",
];

然后我创建一个requestSystemInfo()函数来调用。函数名不需要与开放频道名称相同:

import { BrowserWindow } from "electron";

function requestSystemInfo(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) {
    const versionChrome = process.versions.chrome;
    const versionNode = process.versions.node;
    const versionElectron = process.versions.electron;
    const result = {
        chrome: versionChrome,
        node: versionNode,
        electron: versionElectron
    }
    mainWindow.webContents.send("getSystemInfo", result);
}

最后,我创建一个要导出的常量:

const systemInfo = new IPC ({
    nameAPI,
    validSendChannel,
    validReceiveChannel
});

export default systemInfo;

在此之后,我需要在preload.ts中注册这些频道。我只需要写入:

import { generateContextBridge } from "./IPC/General/contextBridge"
import systemInfo from "./IPC/systemInfo";

generateContextBridge([systemInfo]);

然后我在index.ts中放入必要的函数

import { ipcMain } from 'electron';
import Main from "./mainWindow";

import systemInfo from './IPC/systemInfo';

let main = new Main();

main.onEvent.on("window-created", ()=> {
    systemInfo.initIpcMain(ipcMain, main.window);
});

updaterInfo.ts

现在事情变得有点更复杂了。我重建了自动更新应用程序的函数,同时保持所有代码都在updaterInfo.ts文件中。

所以我从创建一些支持常量开始:

import { SendChannels } from "./General/channelsInterface";
import IPC from "./General/IPC";

const nameAPI = "updaterInfo";

// to Main
const validSendChannel: SendChannels = {
    "requestVersionNumber": requestVersionNumber,
    "checkForUpdate": checkForUpdate,
    "startDownloadUpdate": startDownloadUpdate,
    "quitAndInstall": quitAndInstall,
};

// from Main
const validReceiveChannel: string[] = [
    "getVersionNumber",
    "checkingForUpdate",
    "updateAvailable",
    "updateNotAvailable",
    "downloadProgress",
    "updateDownloaded",
];

在创建IPC类的对象之前,我停下来思考了一下,决定扩展基类。为什么?因为我必须单独声明与autoUpdater相关的操作。

class UpdaterInfo extends IPC {
    initAutoUpdater() {
        // ...
    }
}

等一下我会展示什么要插入这个方法,但与此同时,我可以创建要导出的常量:

const updaterInfo = new UpdaterInfo ({
  nameAPI,
  validSendChannel,
  validReceiveChannel
});

export default updaterInfo;

现在我定义通过validSendChannel注册的频道调用的函数

import { BrowserWindow, app } from "electron";
import { autoUpdater } from "electron-updater";

function requestVersionNumber(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) {
    const version = app.getVersion();
    const result = {version};
    mainWindow.webContents.send("getVersionNumber", result);
}

function checkForUpdate(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) {
    autoUpdater.autoDownload = false;
    autoUpdater.checkForUpdates();
}

function startDownloadUpdate(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) {
    autoUpdater.downloadUpdate();
}

function quitAndInstall(mainWindow: BrowserWindow, event: Electron.IpcMainEvent, message: any) {
    autoUpdater.quitAndInstall();
}

这些函数调用autoUpdater,它生成事件。所以我创建一个函数来拦截和管理这些事件:


function initAutoUpdater(autoUpdater: AppUpdater, mainWindow: BrowserWindow) {
    autoUpdater.on('checking-for-update', () => {
        mainWindow.webContents.send("checkingForUpdate", null);
    });

    autoUpdater.on('error', (err) => { });

    autoUpdater.on("update-available", (info: any) => {
        mainWindow.webContents.send("updateAvailable", info);
    });

    autoUpdater.on('download-progress', (info: any) => {
        mainWindow.webContents.send("downloadProgress", info);
    });

    autoUpdater.on("update-downloaded", (info: any) => {
        mainWindow.webContents.send("updateDownloaded", info);
    });

    autoUpdater.on("update-not-available", (info: any) => {
        mainWindow.webContents.send("updateNotAvailable", info);
    });
}

有了这个函数,然后,我完成UpdaterInfo类中的方法:

import { AppUpdater, } from "electron-updater";

class UpdaterInfo extends IPC {
    initAutoUpdater(autoUpdater: AppUpdater, mainWindow: BrowserWindow) {
        initAutoUpdater(autoUpdater, mainWindow);
    }
}

index.ts和preload.ts

完成updaterInfo.ts后,我终于完成了preload.ts文件:

import { generateContextBridge } from "./IPC/General/contextBridge"

import systemInfo from "./IPC/systemInfo";
import updaterInfo from './IPC/updaterInfo';

generateContextBridge([systemInfo, updaterInfo]);

此外,我有所有必要的元素来完成index.ts:

import { ipcMain } from 'electron';
import { autoUpdater } from "electron-updater";
import Main from "./mainWindow";

import systemInfo from './IPC/systemInfo';
import updaterInfo from './IPC/updaterInfo';

require('electron-reload')(__dirname);

let main = new Main();

main.onEvent.on("window-created", ()=> {
    systemInfo.initIpcMain(ipcMain, main.window);
    updaterInfo.initIpcMain(ipcMain, main.window);

    updaterInfo.initAutoUpdater(autoUpdater, main.window);
});

至此,我完成了关于Electron的部分。

Svelte

Svelte端只剩下两件事需要修复。在第一个版本中,我把index.html、global.css和favicon.png文件插入到dist/www文件夹中。然而,将dist文件夹专门用于Svelte和TypeScript生成的文件更有意义。我把这三个文件移到src/frontend/www文件夹中:

> src
    > electron 
    > frontend
        - App.svelte
        - global.d.ts
        - main.ts 
        - tsconfig.json 
        > Components
            - InfoElectron.svelte
            - Version.svelte  
        > www
            - favicon.png
            - global.css
            - index.html

然而,问题是在编译时将这些文件复制到dist/www中。幸运的是,你可以使用rollup-plugin-copy在编译时自动复制这些文件。

在命令行中:

npm i rollup-plugin-copy

然后我编辑rollup.config.js文件,添加:

最后,我更新Svelte组件调用的函数

InfoElectron.svelte

<script>
globalThis.api.send("requestSystemInfo", null);
globalThis.api.receive("getSystemInfo", (data) => {
    chrome = data.chrome;
    node = data.node;
    electron = data.electron;
});
</script>

成为

<script>
  globalThis.api.systemInfo.send("requestSystemInfo", null);
  globalThis.api.systemInfo.receive("getSystemInfo", (data) => {
    chrome = data.chrome;
    node = data.node;
    electron = data.electron;
});
</script>

我以类似的方式修改Version.svelte中的函数: globalThis.api.send(…)和globalThis.api.receive(…)变成了globalThis.api.updaterInfo.send(…)和globalThis.api.updaterInfo.receive(…)。

就是这些了。

总结

监听事件:

// 方式1
// 监听数据
ipcMain.on('close-task-setting', () => {})
// 发送数据
ipcRenderer.send('')
// 方式2
// 监听数据,(可返回数据)
ipcMain.handle('select-folder', async () => { return true })
// 发送数据
ipcRenderer.invoke('select-folder')

// 方式3 electron -> view
win.webContents.send('main-process-message', (new Date).toLocaleString())
ipcRenderer.on('main-process-message', () => {})

文章来源:https://blog.csdn.net/weixin_42429220/article/details/135758011
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。