Home  >  Q&A  >  body text

Electron gets AppData on preload

How to get the preloaded AppData directory?

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" is initialized later.

My ultimate goal is to read the json configuration from the AppData directory. If there is a better way to do this, please feel free to let me know. The user does not have to be able to change the configuration at runtime. But I have to be able to write the default values ​​from defaultConfig into the config file.

P粉351138462P粉351138462108 days ago275

reply all(1)I'll reply

  • P粉569205478

    P粉5692054782024-04-07 00:44:04

    app.getPath() The method is only available when the application is "ready". Use app.on('ready' () => { ... }); to detect the 'ready' event. For more information, see Electron's Events: 'ready' event.

    Regarding your preload.js script, including functions directly in it sometimes makes the content difficult to read and understand (even if it is only implemented by require). Currently, the document does not have separation of concerns. IE: Your "configure" functionality is mixed in the preload script. If you wish to separate the issue, then you should refactor your "config" code from the preload.js file and put it in its own file. This way, your preload.js file is only used to configure the IPC channel and transport associated data (if any).


    Okay, let's see how to solve the app.getPath('appData') problem.

    In your main.js file, detect when your application is "ready" and then get the appData via your config-manager.js file Table of contents.

    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 to the loadConfig() function scope since they are only used by that function . If you need to expose them for use elsewhere in the file, you will need to move them back outside the loadConfig() function scope.

    I moved the reference to ElectronApp.getPath('appData') into the loadConfig() function because after the application is "ready" from main. js Call this function.

    I added the helper function pathExists() because its implementation is used multiple times.

    Finally, I added the getConfig() function to make it easy to get the configuration object from anywhere in the main thread of the application when needed (just include it in the file where you need 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}
    

    A typical preload.js script looks 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);
                }
            }
        }
    );
    

    If you need help understanding the implementation of IPC channels and how to send/receive them in the main thread or render thread, just ask a new question.

    reply
    0
  • Cancelreply