Electron 在预加载时获取 AppData

发布于 2025-01-09 21:13:48 字数 2294 浏览 0 评论 0原文

如何获取预加载的 AppData 目录?

background.js

[...]

async function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__static, "preload.js"),
            nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
            contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
        },
    })
}

[...]

preload.js

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld(
    'configManager', 
    require("../src/utils/config-manager")
)

config-manager.js

const app = require("electron").app
const fs = require("fs")
const resourcePath = app.getPath('appData').replaceAll("\\", "/") + "my-custom-path" // <---
const configPath = resourcePath + "config.json"
const defaultConfig = [ ... ]
let config;

function createFilesIfNotExists(){
    if (!fs.existsSync(resourcePath))
        fs.mkdirSync(resourcePath)
    
    if (!fs.existsSync(configPath)){
        fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4))
        return true
    }

    return false
}

module.exports = {
    loadConfig() {
        createFilesIfNotExists()

        [...]

        return config
    }
}

如果我运行它,我会收到此错误。

TypeError: Cannot read property 'getPath' of undefined
    at Object.<anonymous> (VM77 config-manager.js:3)
    at Object.<anonymous> (VM77 config-manager.js:65)
    at Module._compile (VM43 loader.js:1078)
    at Object.Module._extensions..js (VM43 loader.js:1108)
    at Module.load (VM43 loader.js:935)
    at Module._load (VM43 loader.js:776)
    at Function.f._load (VM70 asar_bundle.js:5)
    at Function.o._load (VM75 renderer_init.js:33)
    at Module.require (VM43 loader.js:959)
    at require (VM50 helpers.js:88)
(anonymous) @ VM75 renderer_init.js:93

我认为发生这种情况是因为“app”稍后初始化。

我的最终目标是从 AppData 目录读取 json 配置。 如果有更好的方法来做到这一点,请随时告诉我。 用户不必能够在运行时更改配置。但我必须能够将默认值从 defaultConfig 写入配置文件。

How can I get the AppData directory in preload?

background.js

[...]

async function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__static, "preload.js"),
            nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
            contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
        },
    })
}

[...]

preload.js

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld(
    'configManager', 
    require("../src/utils/config-manager")
)

config-manager.js

const app = require("electron").app
const fs = require("fs")
const resourcePath = app.getPath('appData').replaceAll("\\", "/") + "my-custom-path" // <---
const configPath = resourcePath + "config.json"
const defaultConfig = [ ... ]
let config;

function createFilesIfNotExists(){
    if (!fs.existsSync(resourcePath))
        fs.mkdirSync(resourcePath)
    
    if (!fs.existsSync(configPath)){
        fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4))
        return true
    }

    return false
}

module.exports = {
    loadConfig() {
        createFilesIfNotExists()

        [...]

        return config
    }
}

If I run this, I get this error.

TypeError: Cannot read property 'getPath' of undefined
    at Object.<anonymous> (VM77 config-manager.js:3)
    at Object.<anonymous> (VM77 config-manager.js:65)
    at Module._compile (VM43 loader.js:1078)
    at Object.Module._extensions..js (VM43 loader.js:1108)
    at Module.load (VM43 loader.js:935)
    at Module._load (VM43 loader.js:776)
    at Function.f._load (VM70 asar_bundle.js:5)
    at Function.o._load (VM75 renderer_init.js:33)
    at Module.require (VM43 loader.js:959)
    at require (VM50 helpers.js:88)
(anonymous) @ VM75 renderer_init.js:93

I think this happens because "app" gets initialized later.

My final goal is to read a json config from the AppData directory.
If there is a better way to do this, feel free to tell me.
The user does not have to be able to change the config in runtime. But I must be able to write default values from the defaultConfig into the config file.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

慵挽 2025-01-16 21:13:48

app.getPath() 方法仅在应用“就绪”后可用。使用 app.on('ready' () => { ... }); 检测 'ready' 事件。有关更多信息,请参阅 Electron 的事件:'ready' 事件。

关于您的 preload.js 脚本,直接在其中包含函数有时可能会导致难以阅读和理解(即使仅通过 require 实现)。目前,该文件没有关注点分离。 IE:您的“配置”功能混合在您的预加载脚本中。如果您希望分离问题,那么您应该从 preload.js 文件中重构您的“配置”代码,并将其放在自己的文件中。这样,您的 preload.js 文件仅用于配置 IPC 通道和传输关联数据(如果有)。


好吧,让我们看看如何解决 app.getPath('appData') 问题。

在您的 main.js 文件中,检测您的应用何时“就绪”,然后通过您的 config-manager.js 文件获取 appData 目录。

main.js (主线程)

const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;

let appConfig = require('config-manager');
let appMainWindow = require('mainWindow');

let mainWindow;

app.whenReady().then(() => {
    // Load the config.
    let configStatus = appConfig.loadConfig();
    console.log(configStatus);

    let config = appConfig.getConfig();
    console.log(config);

    // Create your main window.
    mainWindow = appMainWindow.create()

    ...

    })
})

在您的 config-manager.js 文件中,我已将您的“path”变量移至 loadConfig() 函数中范围,因为它们仅由该函数使用。如果您需要将它们公开以供文件中其他位置使用,则需要将它们移回到 loadConfig() 函数范围之外。

我将对 electronApp.getPath('appData') 的引用移至 loadConfig() 函数中,因为这是在应用程序之后从 main.js 调用的已“准备就绪”。

我添加了辅助函数 pathExists() 因为它的实现被多次使用。

最后,我添加了 getConfig() 函数,以便在需要时从应用程序主线程中的任何位置轻松获取配置对象(只要将其包含在需要使用它的文件中即可。IE: let appConfig = require('config-manager').

config-manager.js (主线程)

const electronApp = require("electron").app

const nodeFs = require("fs")

const defaultConfig = [ ... ];

let config;

function loadConfig() {
    let resourcePath = app.getPath('appData').replaceAll("\\", "/") + "my-custom-path";
    let configPath = resourcePath + "config.json";

    if (! pathexists(resourcePath)) {
        nodeFs.mkdirSync(resourcePath);
    }

    if (! pathexists(configPath)) {
        nodeFs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4));
        config = defaultConfig;
    } else {
        config = JSON.parse(nodeFs.readFileSync(configPath , 'utf8'));
    };
}

function getConfig() {
    return config;
}

function pathExists(path) {
    return (fs.existsSync(path)) ? true : false;
}

module.exports = {loadConfig, getConfig}

典型的 preload.js 脚本如下所示像这样的东西。

const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;

// White-listed channels.
const ipc = {
    'render': {
        // From render to main.
        'send': [
            'config:updateConfig' // Example only
        ],
        // From main to render.
        'receive': [
            'config:showConfig' // Exmaple only
        ],
        // From render to main and back again.
        'sendReceive': []
    }
};

contextBridge.exposeInMainWorld(
    // Allowed 'ipcRenderer' methods.
    'ipcRender', {
        // From render to main.
        send: (channel, args) => {
            let validChannels = ipc.render.send;
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, args);
            }
        },
        // From main to render.
        receive: (channel, listener) => {
            let validChannels = ipc.render.receive;
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender`
                ipcRenderer.on(channel, (event, ...args) => listener(...args));
            }
        },
        // From render to main and back again.
        invoke: (channel, args) => {
            let validChannels = ipc.render.sendReceive;
            if (validChannels.includes(channel)) {
                return ipcRenderer.invoke(channel, args);
            }
        }
    }
);

注意,上面的preload.js文件仅用于IPC通道配置和实现。 IE:主线程和渲染线程之间的通信。

如果您需要帮助了解 IPC 通道的实现以及如何在主线程或渲染线程中发送/接收它们,那么只需提出一个新问题。

The app.getPath() method is only available once the app is 'ready'. Use app.on('ready' () => { ... }); to detect the 'ready' event. See Electron's Event: 'ready' event for more information.

Regarding your preload.js script, having functions directly in there can make things difficult to read and understand at times (even if it is only by require). Currently, there is no separation of concern with this file. IE: Your 'config' functionality is mixed inside you preload script. If you wish to separate concerns then you should refactor your 'config' code out of the preload.js file and place it in its own file. That way, your preload.js file is only used for configuring IPC channels and transferring associated data if any.


Ok, let's see how you would get you app.getPath('appData') problem solved.

In your main.js file, detect when your app is 'ready', then through your config-manager.js file get the appData directory.

main.js (main thread)

const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;

let appConfig = require('config-manager');
let appMainWindow = require('mainWindow');

let mainWindow;

app.whenReady().then(() => {
    // Load the config.
    let configStatus = appConfig.loadConfig();
    console.log(configStatus);

    let config = appConfig.getConfig();
    console.log(config);

    // Create your main window.
    mainWindow = appMainWindow.create()

    ...

    })
})

In your config-manager.js file I have moved your 'path' variables into the loadConfig() function scope as they are only use by that function. If you need them exposed for use elsewhere in your file then they will need to be moved back up out of the loadConfig() function scope.

I moved reference to electronApp.getPath('appData') into the loadConfig() function as this is called from main.js after the app is 'ready'.

I added the helper function pathExists() as it's implementation is used more than once.

Lastly, I added the getConfig() function for ease of getting the config object when needed from anywhere in your app's main thread (as long as you include it in the file that needs to use it. IE: let appConfig = require('config-manager').

config-manager.js (main thread)

const electronApp = require("electron").app

const nodeFs = require("fs")

const defaultConfig = [ ... ];

let config;

function loadConfig() {
    let resourcePath = app.getPath('appData').replaceAll("\\", "/") + "my-custom-path";
    let configPath = resourcePath + "config.json";

    if (! pathexists(resourcePath)) {
        nodeFs.mkdirSync(resourcePath);
    }

    if (! pathexists(configPath)) {
        nodeFs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4));
        config = defaultConfig;
    } else {
        config = JSON.parse(nodeFs.readFileSync(configPath , 'utf8'));
    };
}

function getConfig() {
    return config;
}

function pathExists(path) {
    return (fs.existsSync(path)) ? true : false;
}

module.exports = {loadConfig, getConfig}

Your typical preload.js script would look something like this.

const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;

// White-listed channels.
const ipc = {
    'render': {
        // From render to main.
        'send': [
            'config:updateConfig' // Example only
        ],
        // From main to render.
        'receive': [
            'config:showConfig' // Exmaple only
        ],
        // From render to main and back again.
        'sendReceive': []
    }
};

contextBridge.exposeInMainWorld(
    // Allowed 'ipcRenderer' methods.
    'ipcRender', {
        // From render to main.
        send: (channel, args) => {
            let validChannels = ipc.render.send;
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, args);
            }
        },
        // From main to render.
        receive: (channel, listener) => {
            let validChannels = ipc.render.receive;
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender`
                ipcRenderer.on(channel, (event, ...args) => listener(...args));
            }
        },
        // From render to main and back again.
        invoke: (channel, args) => {
            let validChannels = ipc.render.sendReceive;
            if (validChannels.includes(channel)) {
                return ipcRenderer.invoke(channel, args);
            }
        }
    }
);

Note that the above preload.js file is only used for IPC channel configuration and implementation. IE: Communication between the main thread and render thread(s).

If you need help in understanding the implementation of IPC channels and how to send / receive them in either the main thread or render thread(s) then just open a new question.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文