Home > Net >  nodeIntegration vs preload.js vs IPC in Electron
nodeIntegration vs preload.js vs IPC in Electron

Time:02-16

I've read through Electron's context isolation, IPC, and security docs, along with this post about using nodeIntegration, and this post about preload.js. It looks like there are a lot of different ways to accomplish similar tasks and I'm not sure which is the best (safe, easy, etc.).

I know you can simply enable nodeIntegration in renderer processes to access Node outside of the main process. Every source recommended against that for the most part.

Here's where I'm confused. An example from Electron's documentation says you can do something like what's below.

preload.js

// preload with contextIsolation disabled
window.myAPI = {
  doAThing: () => {}
}

renderer.js

// use the exposed API in the renderer
window.myAPI.doAThing()

preload.js has access to Node APIs so technically I could load in all Node processes and then access them in my renderers.

However, I also read about IPC.

part of main.js

ipcMain.on('set-title', (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
})

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

renderer.js

const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
    const title = titleInput.value
    window.electronAPI.setTitle(title)
});

For example, let's say I wanted to implement a function using an external npm module. Would I incorporate it in preload.js and call it from my renderer, or incorporate it in main.js, use ipcRenderer in preload.js with a channel for the function, and then call it from my renderer?

CodePudding user response:

preload.js has access to Node APIs so technically I could load in all Node processes and then access them in my renderers.

This is not always true actually. If sandbox is enabled, only a subset of Node APIs are accessible in preload. Source: (emphasis added)

preload scripts attached to sandboxed renderers will still have a polyfilled subset of Node.js APIs available

If you're using an external npm module and sandbox is enabled (it probably should be), then you won't be able to import the npm module in your preload script and will have to use ipc and trigger it in the main process.

If sandbox is disabled, and you can accomplish everything you want from the renderer process, then I would say to just import the module in your preload and use it directly.

CodePudding user response:

The code you pasted from the Context Isolation docu shows how you could use methods and modules exposed (via preload) to the renderer in earlierer versions of electron, before context isolation was set to true by default. This is however not how you should do it. With contextisolation = true you need to use the contextbridge in you preload, because the window objects are kept isolated.

So the code you pasted from IPC is the way you should do it in 2022. I'd also keep my fingers off NodeIntegration, unless you really know what you're doing.

Concerning your last question: Generally I'd follow a least-privilege approach. The less power you give to the renderer, the safer.

I'll give you one example from my recent project. I need to make screenshots and then save them somewhere. For that I need Browserwindow.webcontents.capturePage() and fs.writeFile() which are both only available to the main process. It would definitely be risky to expose the fs.writeFile method to the renderer so I'm not doing that. Instead I keep my logic for writing the screenshots to my filesystem in the main process. However I want to initiate the Screenshots from the renderer (my UI). To expose as little as possible I only expose a function that calls the invoke method in preload's contextBridge that looks like this:

contextBridge.exposeInMainWorld("ipcRenderer", {
  invokeSnap: async (optionsString: string) =>
    await ipcRenderer.invoke("snap", optionsString),
});

In main I have a listener that handles the incoming request:

ipcMain.handle("snap", async (_event, props: string) => {
  // screenshot logic
  return //sth that the renderer will receive back once the process is finished;
});

The renderer sends the the request and handles the response or errors like that:

window.ipcRenderer.invokeOpen(JSON.stringify(options))
.then(...)
.catch(...)

As a sideeffect this puts the heavy weight on the main process, which might not be desirable in bigger projects - idk. Maybe a more experienced electron developer can say more about that.

  • Related