Electron30 文件和文件夹选择、ipcMain和ipcRenderer的推荐用法

前言

之前都是在渲染进程中直接设置的可调用Node的方法,这样是不太合理的。

合理的做法是所有的NodeJS的方法都在主进程中调用

如果需要渲染进程发起,则使用IPC事件交互,ipcRenderer则通过preload注入。

窗口及preload

窗口设置

1
2
3
4
5
6
7
8
9
10
11
12
13
win = new BrowserWindow({
width: 800,
height: 600,
icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
title: "图片工具",
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
nodeIntegration: false,
webSecurity: true,
contextIsolation: true,
webviewTag: true
},
})

其中

  • nodeIntegration 是否可以直接使用 Node.js API(如 require, process, fs, path 等)。

  • contextIsolation渲染进程的 JavaScript 上下文是否与预加载脚本(preload)隔离

    建议设置为true防止原型污染、API 劫持contextBridge导出的变量能被访问但不能被修改。

preload.ts

注入脚本,这样渲染进程则可以使用window.ipcRenderer获取ipcRenderer对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ipcRenderer, contextBridge } from 'electron'

// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('ipcRenderer', {
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args
return ipcRenderer.off(channel, ...omit)
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args
return ipcRenderer.send(channel, ...omit)
},
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args
return ipcRenderer.invoke(channel, ...omit)
},

// You can expose other APTs you need here.
// ...
})

文件选择

主进程

1
2
3
4
5
6
7
8
9
10
11
12
import { ipcMain, dialog } from "electron";

// 监听来自渲染进程的打开文件对话框请求
ipcMain.handle("open-file-dialog", async () => {
return dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "文本", extensions: ["txt"] },
{ name: "All Files", extensions: ["*"] },
],
});
});

如果允许多选

1
2
3
4
5
6
7
8
9
10
// 监听来自渲染进程的打开文件对话框请求
ipcMain.handle("open-file-dialog", async () => {
return dialog.showOpenDialog(win, {
properties: ["openFile", "multiSelections"],
filters: [
{ name: "文本", extensions: ["txt"] },
{ name: "All Files", extensions: ["*"] },
],
});
});

渲染进程

1
2
3
4
5
6
7
8
9
10
11
12
async selectFileClick() {
try {
const result = await window.ipcRenderer.invoke("open-file-dialog");
if (!result.canceled) {
console.log("选择的文件路径:", result.filePaths);
} else {
console.log("用户取消了选择");
}
} catch (error) {
console.error("选择文件时出错:", error);
}
},

注意

返回的路径是数组

文件夹选择

主进程

1
2
3
4
5
6
7
8
9
10
11
import {ipcMain, dialog} from "electron";

// 监听来自渲染进程的打开文件夹对话框请求
ipcMain.handle('open-folder-dialog', async () => {
if (!win) {
return ""
}
return dialog.showOpenDialog(win, {
properties: ['openDirectory'] // 指定为选择文件夹
});
});

渲染进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const folderPath = ref("")

async function selectFolderClick() {
try {
const result = await window.ipcRenderer.invoke('open-folder-dialog');
if (!result.canceled) {
if (result.filePaths.length > 0) {
folderPath.value = result.filePaths[0]
}
} else {
console.log('用户取消了选择');
}
} catch (error) {
console.error('选择文件夹时出错:', error);
}
}

文件弹窗保存

主进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { ipcMain,dialog } = require("electron");
const path = require("path");

ipcMain.on("savefile", (event, arg) => {
dialog
.showSaveDialog({
defaultPath: path.join(
global.sharedObject.appBasePath,
arg ? arg : "filename.txt"
), // 保存文件的默认路径和名称
})
.then((result) => {
if (!result.canceled) {
const filePath = result.filePath;
// 在这里执行保存文件的逻辑
console.log("保存路径:", filePath);
event.reply("savefile-result", filePath);
} else {
console.log("取消保存文件");
}
})
.catch((err) => {
console.log("保存文件出错:", err);
});
});

渲染进程

1
2
3
4
window.ipcRenderer.send("savefile", "新脑图.txt");
window.ipcRenderer.on('savefile-result', (event, arg) => {
console.log(arg)
})