Home > other >  In Electron, how can I securely build my renderer script out of multiple modules?
In Electron, how can I securely build my renderer script out of multiple modules?

Time:03-22

I understand why it's a best practice not to expose Node or Electron's full API in renderer processes. And I understand how to expose only a necessary subset of these APIs using preload scripts.

But what I don't understand is how to write the renderer logic using this model.

My main BrowswerWindow's renderer.js script will have to contain pretty much all of the UI logic, listening for all of the window events and modifying the page's DOM accordingly.

Ordinarily, I'd want to build this mammoth script in a modular way, but Electron's isolated context means that Node's module system won't be available. The Electron docs suggest that a packaging tool such as webpack or parcel might be the solution. But no examples are provided.

(And I note that in the source code for VSCode and Electron's own Fiddle application, they seem to just shrug their shoulders and enable Node integration.)

But in the ideal case, how is this actually meant to work?

For example, every time I want to test-run the app, should I have a script to minify all of my src content into 3 scripts - main.min.js, rederer.min.js, and preload.min.js? Put those in, say, an app directory? Then copy over my static content before running electron from there?

Is that the idea, or is there something I'm not getting?

CodePudding user response:

You do not need to use webpack or parcel to build a secure Electron application. Those are used to bundle your code, not secure it.

It is hard to find good, best practice design documentation for Electron. As you have already discovered, the recommendation of setting nodeIntegration to false without implementing it themselves can be confusing.

nodeIntegration can be set to true if you do not load any remote content in your render. Ref: Do not enable Node.js integration for remote content. I just always set mine to false and utilise my preload.js script to communicate / transfer data between the main and render processes.

Regarding "how to write the renderer logic using this model", when you take a step back and look at the wider picture it will hopefully become clearer.

You must think of Electron in processes (though I tend to use the term threads). The main process and the render process.

As you know, you use IPC to communicate between processes. Within the main and render processes, you can use events. The issue of modules becoming tightly coupled is removed when using Node's events with your main process.

Now, the preload.js script...

I see many people trying to directly access concrete implementations hardcoded into their preload.js script(s). This can become very complicated and very confusing in a short space of time.

I take a very different approach. I only use one preload.js script for my entire project. The purpose of my preload script is not to define / implement concrete models, but instead to use the preload.js script purely as a channel of communication. IE: Channel names (and optional data) to send "events" back and forth between the main process and render process(es). Let scripts in your main process handle the concrete Electron / Node implementations and scripts in your render process handle html interactivity.

Main Process

The main process contains Electron's core and Node.js

You can split your code up into logical chunks and import them using Node's require function. Remembering that once they are called, they are cached. They also effectively have their own scope which is great.

To easy the burden of trying to work out paths, just use Node's path.join(...) module.

Use Node's events to communicate between your application modules.

Use Electron's ipcMain.on(...) and ipcMain.handle(...) to listen for IPC events from the render process and contents.send(...) to send IPC events to your specific render process.

Render Process

The render process(es) contain your html view(s), Javascript to make your html view(s) interactive and of course, the html view(s) associated CSS.

Use ES6 modules here to separate your code. import them into your html Javascript file, remembering to only export your publicly available functions.

You can reference all paths as relative paths. Any build tools you may use should be able to handle this when setup correctly.

Use Electron's ipcRender.on(...) to listen for IPC events from the main process and ipcRender.send(...) and ipcRender.invoke(...) to send IPC events to the main process.

To use these above commands easily, setup your preload.js script like so.

preload.js (main process)

// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;

// White-listed channels.
const ipc = {
    'render': {
        // From render to main.
        'send': [
            'message:fromRender' // Example channel name
        ],
        // From main to render.
        'receive': [
            'message:fromMain' // Example channel name
        ],
        // From render to main and back again.
        'sendReceive': [
            'message:fromRenderAndBackAgain' // Example channel name
        ]
    }
};

// Exposed protected methods in the render process.
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);
            }
        }
    }
);

Building your channel name whitelist in your preload.js script provides one source of truth. Including the same (one and only) preload.js script in every created window becomes easy and error free.

If you need for me to add to this answer the use of the preload.js script in the main and render processes just let me know.

  • Related