Electron教程
本文涉及到的只是在自动化工具使用的部分,具体详情请前往官方文档查阅。
好的,我会尽力把这篇关于Electron和TypeScript的文章翻译成中文:
在使用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类保持独立允许我保持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”。这些只是如何使用模板的两个例子,可以根据意愿替换、删除或修改。当然,你也可以把它们作为基础来添加其他功能和频道。
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开始。这个模块只有一个出口和一个入口通道,用它来请求和获取一些关于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文件中。
所以我从创建一些支持常量开始:
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);
}
}
完成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端只剩下两件事需要修复。在第一个版本中,我把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', () => {})