electron-userland / electron-webpack

Scripts and configurations to compile Electron applications using webpack
https://webpack.electron.build/
905 stars 170 forks source link

Multiple renderer entry points for multiple renderer to run background process #47

Open walleXD opened 6 years ago

walleXD commented 6 years ago

Hey all,

It's possible to create multiple main entries. Is it possible to do the same for the renderer process? I am trying to create a separate hidden browserWindow, with separate entry point, to run background tasks.

Thanks a bunch in advance

toddb commented 6 years ago

I am looking for the same solution and came across here that poses a solution and then someone suggests (at the end) HtmlWebpackPlugin.

walleXD commented 6 years ago

I tried using the htmlwebpackplugin but there are 2 main problems:

  1. the default html template electron-webpack builds includes all js files and so that means if I have another renderer entry point called background.js that file will also be included with the main
  2. the default template using by the webpack configs include some electron-specific settings and it would take some time to dig in deep to get things like module imports to work properly.

If, there's another entry exposed, just like the main thread's, then electron-webpack can use the existing build pipe line for the extra entries

develar commented 6 years ago

Extra renderer points are not supported yet. Help wanted (or you can https://webpack.electron.build/modifying-webpack-configurations, but as you pointed, " it would take some time to dig in deep to get things like module imports to work properly").

walleXD commented 6 years ago

I tried to get it working but the second renderer file(s) messes up the main renderer since the htmlwebpackplugin is setup to include all chunks. TBH completely reworking the rendering build pipeline would take out current ease associated with using electron-webpack during development

walleXD commented 6 years ago

@develar any plans on adding extra renderer entries?

toddb commented 6 years ago

If it any help, for my situation (and now with a little more experience) I am not finding it a major problem. The design I had to work with has four types of renders: some are web workers, others straight windows. I don't actually think you need to complicate the design (personally) and thus wouldn't encourage the ability to do multiple renderers. Instead, I would encourage simplicity which in this case would have forced better design!

Regardless, for what it is worth and may help others, my kludge used electron-window to help switching renderers at runtime (that was my best shot and if there are better ways I would like to know). I'll include some code here because it does require getting it "just right" for it to work on the webpack dev server and a packaged version (ie get the slash in the right direction). Note: my file structure is the default from electron-webpack using src/main, src/renderer and I have a src/common/window file.

I it helps someone find a much better solution or realise not to design your application like this. Now that we are in control of the existing application we plan to remove all of this. Have one window and run the web workers in the background. Previously separate windows will have parent/child relationships.

common\window.js

/*global process __dirname */

import window from 'electron-window';
import { IsDevelopmentEnvironment } from '../../constants';
import log from 'electron-log';

/**
 * There are four windows that make up the application. Each need their own renderer.
 * @enum {string}
 */
export const windowName = {
    application: 'main',
    detectionDialog: 'alert',
    backgroundTasks: 'tasks',
    thirdPartyIntegrationConfiguration: 'config'
};

/**
 * Makes the correct uri or path to locate the auto-generated index.html file from electron-webpack.
 *
 * In development `electron-webpack dev` requires serving on http://:
 *   - makes 'dist/renderer/index.html'
 *   - starts webpack development server
 *   - servers up this file on '/'
 *
 *  In production `Auror.exe` requires a path and `electron-window` requires NO 'file://'
 *   - makes 'dist/win-unpacked/resources/app/index.html'
 *   - that is bundled up into the package
 *   - this is run as a local 'file://' that under windows is backslash '\' delimited
 *   - NOTE: `path.resolve` is not available here in the render for resolution
 *
 * WARNING: production builds are brittle. To test, make and run an unpacked version:
 *
 *   1. `electron-webpack`
 *   2. `electron-builder --dir`
 *   3. `dist\uwnin-unpacked\Auror.exe`
 *   4. Check that windows load
 *   5. Check no errors on console (Control-Shift-I)
 *
 *   NOTE: loading issues can also be related `electron-windows` uri parsing with params
 *
 * @return {string} uriOrPath
 */
export function makeUrlToHtmlFile () {
    return IsDevelopmentEnvironment
        ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}/`
        : `${__dirname}\\index.html`;  \\ MUST be backslashes for packaged version
}

/**
 * @class WindowOptions
 * @property {RendererWindowOptions} data params passed through to the renderer (to cross process boundaries)
 * @property {BrowserWindowConstructorOptions} windowOptions options passed through to the {@link BrowserWindow}
 */

/**
 * @class RendererWindowOptions
 * @property {string} windowName type of window to load {@link windowName}
 * @property {WindowLoadCallback) callback act on the electron window post showURL which 'shows' by default and ignores hide
 */

/**
 * @class WindowLoadCallback
 * @param {Electron.Window} window
 */

/**
 * Open a window from the renderer toggling the load based on the options. The window is opened via the
 * 'electron-window' mechanism that shares data via the url params.
 * @param {WindowOptions} options
 * @return {Electron.BrowserWindow}
 */
export function makeRendererWindow (options = {}) {

    // ... stripped out default options for sample
    let aWindow = window.createWindow(options);

    const uriOrPath = makeUrlToHtmlFile();
    log.debug(`Rendering using: '${uriOrPath}'`);
    aWindow.showURL(uriOrPath, options.data, () => {
        // most times you don't need a post-construction hook
        // but electron-window is NOT equivalent to wdinow.loadURL
        if (typeof options.callback === 'function') {
            options.callback(aWindow);
        }
        log.info(`Window '${options.data.windowName}' opened`);
    });

    return aWindow;
}

src\main.js

import { makeRendererWindow, windowName } from '../common/window';

// ... standard stuff

// Create main BrowserWindow when electron is ready
app.on('ready', () => {

    log.debug('Starting app window');

    /**
     * This is the main window that should only be showing.
     */
    mainWindow = makeRendererWindow(
            {
                data: {
                    windowName: windowName.application
                },
                windowOptions: {
                    width: 1110,
                    minWidth: 1110 // ... and some more
                }

            });

    otherWindow = makeRenderWindow() // ... now into psuedo code
});

src\renderer.js

import { windowName } from '../common/window';

// here's the actual javascript code to run
import { default as runMain } from '../app';   // this is angular
import { default as runTasks } from '../background'; // these are web workers
import { default as runDetection } from '../detection';  // these are angular
import { default as runConfig } from '../config';  // this is angular

import log from 'electron-log';

/**
 * This renderer is called via 'electron-window' which passes through an object via params and
 * attaches on `window.__args__`
 *
 * @type {RendererWindowOptions}
 */
const options = window.__args__;

if (options) {
    const toRun = options.windowName;
    log.info('In renderer', options.windowName);

    // load up the correct javascript for the window. This gets around the multiple renders problem.
    switch (toRun) {
        case windowName.application:
            runMain();
            break;
        case windowName.detectionDialog:
            runDetection();
            break;
        case windowName.thirdPartyIntegrationConfiguration:
            runConfig();
            break;
        case windowName.backgroundTasks:
            runTasks();
            break;
        default:
            log.error(`Nothing to run, found '${toRun}'`);
    }

} else {
    log.info('Renderer has been called without using \'electron-window\' see https://github.com/jprichardson/electron-window#usage');
}
walleXD commented 6 years ago

@toddb holy guacamole, this is a great solution. I am already using electron-window. My index.js for renderer

import { parseArgs } from 'electron-window'

parseArgs()
const { name } = window.__args__

const initRenderer = async name => {
  switch (name) {
    case 'main': return import('./ui')
    case 'test': return import('./test')
    default:
      console.error('Nothing to run')
  }
}

initRenderer(name)

Using dynamic import here makes sure the renderer process only loads up the code it will be using base on the switch cases

& my createWindow.js

import isDev from 'electron-is-dev'
import { createWindow, windows } from 'electron-window'
import windowStateKeeper from 'electron-window-state'

export default (
  name,
  { devURL, prodURL },
  windowStateOpts = {},
  browserWindowOpts = {}
) => {
  const windowState = windowStateKeeper({
    defaultWidth: 1000,
    defaultHeight: 800,
    ...windowStateOpts
  })
  const window = createWindow({
    x: windowState.x,
    y: windowState.y,
    width: windowState.width,
    height: windowState.height,
    minWidth: 900,
    minHeight: 620,
    preload: true,
    ...browserWindowOpts
  })

  windowState.manage(window)

  const url = isDev ? devURL : prodURL
  window.showURL(url, { name })

  return window
}

export { windows }

The only issue that this brings up is how do we keep the other windows hidden? By default, electron-window load and shows the window it creates. So, I used _loadURLWithArgs function on the window object. The only thing is that it requires a callback to work, even if you aren't using it to do anything

toddb commented 6 years ago

@walleXD

  1. I'll take a look at the inline import (require). I really dislike inline but can see benefits here. Ah, I've just seen that you sneaked async in there too!
  2. Your question to supress windows, you'll see that I pass a callback in the RendererWindowOptions:options. In practice, the post-load hook is actually needed for wiring up code that lives on the parent scope. But, electron-windows probably needs a little more love that we don't need to do this (I haven't looked at why they do this).