用户将进度保存到电子/反应应用程序中的文件

问题描述 投票:0回答:1

我正在创建一个 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
下载到文件中?

更一般地说,有没有更好的方法让用户将他们的进度保存到文件?

javascript reactjs electron savefiledialog save-as
1个回答
0
投票

您当前的方法仅基于渲染端/过程中的实现思路。

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>
© www.soinside.com 2019 - 2024. All rights reserved.