我正在创建一个 Electron 应用程序,用户可以在其中创建和处理“项目”。我正在尝试实现一个“保存”按钮,以便用户可以保存他们的进度。我认为此功能有两个步骤:(i)从用户那里获取文件名/位置,然后(ii)将数据保存到文件中。
对于步骤 (i) 我已经实施了
const get_file_name = async () => {
try {
return await window.showSaveFilePicker({
types: [{
accept: { "application/octet-stream": [".custom"], }
}],
});
// catches when the user hits cancel
} catch(err) {}
}
}
}
但是,我收到消息
dyn.age80g55r is not a valid allowedFileType because it doesn't conform to UTTypeItem
因为我使用 custom
作为我的文件扩展名。有没有办法使用自定义文件扩展名?我希望它特定于我正在创建的应用程序。
对于第 (ii) 步,我实施了:
get_file_name().then((file) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.downlaod = file.name;
link.href = url;
link.click();
});
但是,这只是打开一个显示
blob
内容的新页面,而不是下载新文件。有谁知道我如何将blob
下载到文件中?
更一般地说,有没有更好的方法让用户将他们的进度保存到文件?
您当前的方法仅基于渲染端/过程中的实现思路。
Electron 具有多进程架构。阅读有关流程模型的更多信息。
利用这种多进程模型,我们可以将文件对话框选择窗口和下载(以下代码中称为保存功能)移动到主进程。
如果你有能力,让你的渲染过程尽可能简单。仅将它们用于呈现 UI 和 UI 交互。即:没有繁重的工作。
下面的最小可重现示例执行以下操作:
textarea
字段。Save
按钮以将示例数据保存到选定的文件扩展名(在本例中为.txt
文件)。您提到您还希望使用自定义文件扩展名。下面的代码演示了它的使用。您可以将“自定义”文件扩展名更改为您想要的任何内容,甚至是只有您的应用程序才能识别和理解的非标准文件扩展名。
虽然您的要求可能与下面的示例代码有很大不同,例如:
.json
数据保存到文件而不是纯文本。Save
按钮触发的文件。一切顺利,这对你来说应该很容易实现,因为只有你知道你的应用程序的要求。
main.js
(主要过程)
// Import required electron modules
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronDialog = require('electron').dialog;
const electronIpcMain = require('electron').ipcMain;
// Import required Node modules
const nodeFs = require('fs');
const nodeFsPromises = require('node:fs/promises');
const nodePath = require('path');
// Prevent garbage collection
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile(nodePath.join(__dirname, 'index.html'))
// Send path to render process to display in the UI
.then(() => { window.webContents.send('populatePath', electronApp.getPath('documents')); })
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
electronIpcMain.handle('openPathDialog', (event, path) => {
let options = {
defaultPath: path,
buttonLabel: 'Select',
filters: [{
name: 'My Custom Extension',
extensions: ['txt']
}]
};
// Return the path to display in the UI
return openSaveDialog(window, options)
.then((result) => {
// Returns "undefined" if dialog is cancelled
if (result.canceled) { return }
return path = result.filePaths[0];
})
});
electronIpcMain.on('saveData', (event, object) => {
// Check the path (file) exists
nodeFsPromises.readFile(object.path, {encoding: 'utf8'})
.then(() => {
// Save the data to the file
nodeFs.writeFileSync(object.path, object.data);
})
// Show invalid file path error via main process dialog box
.catch(() => {
let options = {
type: 'warning',
title: 'Invalid Path',
message: 'Please select a valid path before saving.'
};
openMessageBoxSync(window, options);
})
})
function openSaveDialog(parentWindow, options) {
// Return selected path back to the UI
return electronDialog.showOpenDialog(parentWindow, options)
.then((result) => { if (result) { return result; } })
.catch((error) => { console.error('System file dialog error: ' + error); });
}
function openMessageBoxSync(parentWindow, options) {
return electronDialog.showMessageBoxSync(parentWindow, options);
}
preload.js
(主要过程)
// Import the necessary Electron modules
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels
const ipc = {
'channels': {
// From render to main
'send': [
'saveData'
],
// From main to render
'receive': [
'populatePath'
],
// From main to render (once)
'receiveOnce': [],
// From render to main and back again
'sendReceive': [
'openPathDialog'
]
}
};
// Exposed protected methods in the render process
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods
'ipcRenderer', {
// From render to main
send: (channel, args) => {
if (ipc.channels.send.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render
receive: (channel, listener) => {
if (ipc.channels.receive.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From main to render (once)
receiveOnce: (channel, listener) => {
if (ipc.channels.receiveOnce.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.once(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again
invoke: (channel, args) => {
if (ipc.channels.sendReceive.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
以及如何使用它...
/**
*
* Main --> Render
* ---------------
* Main: window.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRenderer.receive('channel', (data) => { methodName(data); });
*
* Main --> Render (Once)
* ----------------------
* Main: window.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRenderer.receiveOnce('channel', (data) => { methodName(data); });
*
* Render --> Main
* ---------------
* Render: window.ipcRenderer.send('channel', data); // Data is optional.
* Main: electronIpcMain.on('channel', (event, data) => { methodName(data); })
*
* Render --> Main (Once)
* ----------------------
* Render: window.ipcRenderer.send('channel', data); // Data is optional.
* Main: electronIpcMain.once('channel', (event, data) => { methodName(data); })
*
* Render --> Main (Value) --> Render
* ----------------------------------
* Render: window.ipcRenderer.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', (event, data) => { return someMethod(data); });
*
* Render --> Main (Promise) --> Render
* ------------------------------------
* Render: window.ipcRenderer.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', async (event, data) => {
* return await myPromise(data)
* .then((result) => { return result; })
* });
*
* Main: function myPromise(data) { return new Promise((resolve, reject) => { ... }); }
*
*/
index.htm
(渲染过程)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<label for="path">Path: </label>
<input type="text" id="path" value="" style="width: 44em;">
<input type="button" id="openPathDialog" value="...">
<hr>
<textarea id="data" rows="10" cols="80" spellcheck="true" autofocus></textarea>
<br><br>
<input type="button" id="save" value="Save">
</body>
<script>
let pathField = document.getElementById('path');
let dataField = document.getElementById('data');
// Populate file path field on creation of window
window.ipcRenderer.receive('populatePath', (path) => {
pathField.value = path;
});
document.getElementById('openPathDialog').addEventListener('click', () => {
// Send message to main process to eopn file selector
window.ipcRenderer.invoke('openPathDialog', pathField.value)
.then((path) => {
// Display path if dialog was not closed by "Cancel" button or "ESC" key
if (path !== undefined) { pathField.value = path; }
});
})
document.getElementById('save').addEventListener('click', () => {
// Send file path and data to main process for saving
window.ipcRenderer.send('saveData', {
'path': pathField.value,
'data': dataField.value
});
});
</script>
</html>