-1

I am working on an electronJS app, where during development I have had nodeintegration: true and contextisolation: false to make the development phase less restrictive for the placement of the functions and reduce the number of function calls. As a result many .js files that are used by the html files as scripts use require which can open security issues if content is loaded from outside of the local file system.

There are currently many functions in modules required from the .js files (renderer process) which access the database after requiring the db module as well. From what is understood, it is best to place these functions in the main process and have the preload with the contextbridge provide the ability to relay the data between processes.

My question is, how to structure the placement of all those function calls to the DB in the main process? Is there a typical pattern for the function list to reside in the main process without the main process file growing to become too big to easily maintain? The DB module files together are >1K lines and I hope that the main process can somehow use a pattern of possibly requiring modules itself which can listen to the IPCrenderer emitted signals as before reliably? But, this does not appear to work as each module required in main must have a specific function it provides called explicitly from main.js. And as a minor question 'will placing the DB functionality into the main process not make debugging a greater challenge?'

Vass
  • 2,682
  • 13
  • 41
  • 60

1 Answers1

1

Placing your DB code in your main process with nodeIntegration: false and contextIsolation: true is definitely the way to go when locking-down your application. Doing so will also prevent your render processes from freezing during heavy / lengthy DB calls.

There is no "typical pattern" for the structure of your main.js (main process) file.

That said, placing all your main process code in your main.js file will quickly lead to an unmaintainable file.

The file structure of your Electron Application is totally up to you but usually it is best to structure it in a logical hierarchical order. For example:

├─ dist
├─ node_modules
├─ src
|  ├─ main-process
|  |  ├─ db
|  |  |  ├─ file-1.js
|  |  |  ├─ file-2.js
|  |  |  └─ file-3.js
|  |  ├─ windows
|  |  |  └─ main-window.js
|  |  ├─ main.js       <-- Entry point
|  |  └─ preload.js
|  └─ render-process
|        ├─ main.html
|        ├─ style.css
|        └─ render.js
├─ package.json
└─ package-lock.json

Then, within your main.js file, just require the files necessary to get your application up and running.

main.js (main process)

// Import the necessary electron modules.
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;

// Import the necessary Node modules.
const nodePath = require('path');

// Import the necessary Application modules.
const appDb = require(nodePath.join(__dirname, './db/file-1'));
const appMainWindow = require(nodePath.join(__dirname, './windows/main-window'));

// Prevent garbage collection.
let mainWindow = null;

electronApp.on('ready', () => {
    mainWindow = appMainWindow.create();

    // Do DB connection here...
});

electronApp.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        electronApp.quit();
    }
});

electronApp.on('activate', () => {
    if (electronBrowserWindow.getAllWindows().length === 0) {
        appMainWindow.createWindow();
    }
});

main-window.js (main process)

// Import the necessary Electron modules.
const electronBrowserWindow = require('electron').BrowserWindow;

// Import the necessary Node modules
const nodePath = require('path');

// Define the main window.
let mainWindow;

// Create the main window.
function create() {
    mainWindow = new electronBrowserWindow({
        x: 0,
        y: 0,
        width: 800,
        height: 600,
        show: false,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            preload: nodePath.join(__dirname, '../preload.js')
        }
    });

    mainWindow.loadFile(nodePath.join(__dirname, '../../render-process/main.html')
        .then(() => { window.show(); });

    return mainWindow;
}

// Get the main window instance.
function get() {
    return mainWindow;
}

module.exports = {create, get}

Communicating between processes will be via IPC through the use of your preload.js script.

Examples of various forms of preload.js scripts can be found below .

For main process modules that need to receive events and data from the render process (EG: Your DB scripts), just include and use ipcMain within your file.

const electronIpcMain = require('electron').ipcMain;

electronIpcMain.on('channelName', (event, message) => {
    console.log(message);
})

For main process modules that need to transmit events and data to the render process(es), they will require reference to the window. If your module does not have a reference to the window, use your windows module get() method. For example:

// Import the necessary Application modules.
const appMainWindow = require(nodePath.join(__dirname, './windows/main-window'));

function name() {
    let mainWindow = appMainWindow.get();
    let data = {'message': 'hello'}

    mainWindow.webContents.send('channelName', data);
}

If you need to communicate between modules in your main process, instead of tightly coupling your module methods together you could use Node's event system. This nicely separates methods, files and domains for ease of maintainability and loose coupling.


To knit this all together, require and use the modules that are necessary to get your application up and running within your main.js file.

Within these imported modules, you can require other modules that are needed for their functionality.

The use of module.exports allows for the exporting of publicly available methods. Structure and separation is needed within your files when doing this else circular reference may occur.

Without seeing exactly how your DB files are separated or structured, it is hard to give further detail.


Lastly, debugging in the main process can be just as easy as debugging in the render process when setup correctly. See the below points for more information.

midnight-coding
  • 2,857
  • 2
  • 17
  • 27
  • so how big do main.js files become typically? Can the file be split over other files without using code stubs? – Vass Jun 13 '22 at 20:07
  • 1
    @Vass As big or as small as the programmer likes. Most Electron examples show everything for the main process being crammed into the `main.js` file which is not a good thing in the long run. I am not familiar with the term "code stubs" outside of testing with stubs and mocks. If you mean importing them with `require` then, absolutely. This is the recommended way to split up you code base into separate logical, domain driven files / groups of files. Remember, Node.js caches your `require` file(s) on first import. So, it is “like” a form of class instantiation is you are familiar with classes. – midnight-coding Jun 14 '22 at 07:30
  • thanks! I was wondering if it is possible to delegate some function calls to module by default. That if the main.js lacks a function eg fn1(x) but it exists in a required module, that the call goes directly to the module... is that possible? – Vass Jun 14 '22 at 07:43
  • 1
    Not really. To use `fn1(x)`, the module the function is defined in must be imported into `main.js` first. EG: const myFunctions = require('./myFunctions');`. Then you would call `myFunctions.fn1(x);`. The `myFunctions` module can export functions imported from other modules to make it look like `myFunction` owns them. [Object prototypes](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) and class [extends](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends) may be close to what you are after. – midnight-coding Jun 15 '22 at 09:25
  • so if there are like 100s of different fns calls using the nodejs apis, there are going to be 100s of lines in main.js? how can the class extends be used instead? – Vass Jun 18 '22 at 10:04
  • 1
    Node.js methods can be required (imported) at any time into any of your application files, therefore there is no need to import them all into your `main.js` file. Just import your immediately needed applications files into your `main.js` file and within those application files, import the necessary Node.js (and other) modules. Class `extends` is used if your write your modules as classes and you instantiate those classes (with the `new` keyword), just like classes in PHP. It is really just syntactic sugar for prototyping. PS: Updated `main-window.js` file with`module.exports = {create, get} – midnight-coding Jun 18 '22 at 11:13