-1

I have a button element on index.html that invokes a function signIn:

<button onclick="signIn()">Sign-in</button>

I have a few basic options on where this function can be defined:

  1. index.html contained directly in a script tag.
  2. signin.js a separate file, included on index.html a la the script tag and its src attribute.
  3. signin.js a separate file, ran by the renderer process (similar to preload.js).
  4. signin.js a separate file, ran by the main process (similar to main.js).

However, depending on my choice, the invocation of this function becomes increasingly difficult from 1 to 4, and with good reason.

I believe, that the function should be defined using option 3, but only because I'm not well versed enough in Electron, Node, or React to know for sure that it should go in 1, 2, or 4. My reasoning is that I'd like to save data related to the user's account in a json file, which is a bit easier with options 3 and 4, though I don't believe I need to dive to the main processor to do this.


Where should my element invoked function be defined, and why?

Hazel へいぜる
  • 1,165
  • 1
  • 8
  • 19
  • Note that `signIn` currently has no functionality defined, though it may need to create a new browser window, which *might* need to be done on the main processor, which restricts me to 4, but I'm not an expert and still researching the topic. Feel free to take that with a grain of salt, or use it to drive your answer. – Hazel へいぜる Feb 09 '22 at 05:48
  • Related, but not a duplicate: [How do you 'merge' several JS files into one?](https://softwareengineering.stackexchange.com/q/436114/118878). My answer addresses some of these options. – Greg Burghardt Feb 09 '22 at 12:39

1 Answers1

2

Options 2 and 4 gives you code seperation and prevents the UI from being blocked during a signin process.

In the render thread, seperating your javascript from your html is common practice as it seperates languages. If you were to place a substantial amount of javascript between <script> tags in your html file then your html file could become difficulty to navigate, especially if more js code is added. Splitting apart code into their own responsibilities and including them with <script> tags when needed would make you code much easier to navigate and read. Additionally, there is no performance hit like there is over the wire when including multiple <script> tags in your html as they are all locally served files packaged within your Electron app.

Through the use of Inter-Process Communication and Context Isolation you can create an app that clearly defines main and render thread functionality. You will want to have a clear understanding of ipcMain and ipcRender

Render thread performance is upheld when its primary responsibily is painting, DOM updating and render side event management. Including cycle hungry functionality like requests or hitting the db will slow down or possibly block any UI interaction or updating. Therefore, functionality such as that should ideally be processed in the main thread.

What I would do is have 2 signin scripts. One in the main thread to handle the heavy processes like touching the json file, etc and one in the render thread to handle the button event, feedback (EG: A spinner whilst signing in) and any errors (EG: json file is missing).

I believe interacting with the json file should be done from the main thread and not the render thread.

I hope the below code helps you a tremendous amount as it took me some time refactoring my own code to work out a simple, logical structure like shown below. PS: Add some directory structure as well to make it easier to navigate whilst coding.

index.html (render thread)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</ttile>
        <link rel="stylesheet" type="text/css" href="style.css">
        <script src="script.js">
    </head>
    <body>
        <input type="text" id="username" name="username">        
        <input type="password" id="passowrd" name="password">
        <input type="button" id="signin" value="Sign-In"> 
        <div id="signin_error"><div>
        ...
    <body>
</html>

script.js (render thread)

let signinButton;
let signinError;

(function() {
    username = document.getElementById('username');
    password = document.getElementById('password');
    signinError = document.getElementById('signin_error');

    signinButton.addEventListener('click', () => {
        signin({
            username: username.value,
            password: password.value
        );
    })

    window.ipcRender.receive('signin:error', (message) => { 
        signinError(message);
    });
})();

function signin(details) {
    signinButton.classList.add('spinner');
    window.ipcRender.send('signin:signin', details);
}

function signinError(message) {
    signinButton.classList.remove('spinner');
    signinError.innerText = message;    
}

preload.js (main thread)

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

// White-listed channels.
const ipc = {
    'render': {
        // From render to main.
        'send: [
            'signin:signin'
        ],
        // From main to render.
        'receive': [
            'signin:error'
        ],
        // From render to main and back again.
        'sendReceive': []
    }
};

contextBridge.exposeInMainWorld(
    'ipcRender',
        // From render to main.
        send: (channel, args) => {
            let validChannels = ipc.render.send;
            if (validChannels.includes(channel)) {
                ipcRender.send(channel, args);
            }
        },
        // From main to render.
        receive: (channel, listener) => {
            let validChannels = ipc.render.receive;
            if (validChannels.includes(channel)) {
                ipcRender.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);
            }
        }
    }
};

mainWindow.js (main thread)

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

const nodePath = require('path');

let mainWindow;

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

    mainWindow.loadFile(nodePath.join(__dirname, 'main.html'))
        .then(() => { mainWindow.show(); })
}

function get() {
    return mainWindow;
}

module.exports = {create, get}'

main.js (main thread - entry point)

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

const nodePath = require('path');

let appMainWindow = require(nodePath(__dirname, 'mainWindow.js'));

require(nodePath.join(__dirname, 'signin.js'));

let mainWindow = null;

(function() {
    // Application is now ready to start.
    electronApp.on('ready', () => {
        mainWindow = appMainWindow.create();

    // Re-activate Application when in macOS dock.
    electronApp.on('activate', () => {
        if (electronBrowserWindow.getAllWindows().length === 0) {
            appMainWindow.create(mainWindow);
        }
    });

    // Close Application completely if not on macOS.
    electronApp.on('window-all-closed', () => {
        if (process.platform !== 'darwin') {
            electronApp.quit();
        }
    });
})();


signin.js (main thread)

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

let appMainWindow = require(nodePath(__dirname, 'mainWindow.js'));

(function() {
    electronIpcMain.on('signin:signin', (event, details) => { signin(details); })

})();

function signin(details) {
    // Try signing in.
    console.log(details);

    // If successful, update json file and redirect to a new page.

    // If there is an error.
    appMainWindow.get().webContents.send('signin:error', 'There was an error signin in.'})
}
  • Question: if one is using ES6 modules (`import`) instead of node's `require`, would that change any of this reasoning? Thanks. – user949300 Feb 21 '22 at 07:50
  • 1
    @user949300 No, it shouldn't change any of the reasoning behind this answer. The overall concept I believe would be the same. Implementation could vary slightly depending on use of `require` / `import` or framework, but as an overarching concept the use of threads to separate heavy from light / short from long running tasks and backend from frontend (UI) tasks would be the desired goal. Running tasks in parallel (where possible) and async / event usage makes for a quick running, loosely coupled responsive app in any programming language. The trick is to keep it maintainable and easy to read. – midnight-coding Feb 21 '22 at 09:16