Configuring webpack to handle multiple browser windows in Electron

Farhan CK

Farhan CK

November 12, 2024

Configuring webpack to handle multiple browser windows in Electron

Recently, we built NeetoRecord, a loom alternative. The desktop application was built using Electron. In a series of blogs, we capture how we built the desktop application and the challenges we ran into. This blog is part 4 of the blog series. You can also read about part 1, part 2, part 3, part 5, part 6, part 7 part 8 and part 9 .

When developing desktop applications with Electron, managing multiple browser windows within a single app is often necessary. Whether we need to display different types of content or create a more complex user interface, handling multiple windows efficiently can be challenging.

In this blog, we'll explore how to configure Webpack to manage multiple browser windows in our Electron application, ensuring that each window operates smoothly in our project.

Configuring Webpack for a Single Browser Window

Before diving into the setup for multiple windows, let's first review how to configure Webpack for a single browser window. This example focuses on the renderer process, which is responsible for rendering the UI and handling interactions. If interested in learning how to configure Webpack for the entire Electron app, including the main process, check out this blog.

Consider the following typical folder structure for an Electron project:

electron-app
├── assets
├── app
├── src
│   └── main
│     └── main.js
│   └── renderer
│     └── App.jsx
│     └── index.ejs
├── node_modules
├── package.json

This structure separates the code for the main and renderer processes, which is a standard practice in Electron projects.

Here's how we can configure Webpack for a single browser window:

// ./config/webpack/renderer.mjs

import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";

const configuration = {
  target: ["web", "electron-renderer"],
  entry: "src/renderer/App.jsx",
  output: {
    path: "app/dist/renderer",
    publicPath: "./",
    filename: "renderer.js",
    library: {
      type: "umd",
    },
  },
  module: {...},  // Module configuration (loaders, etc.)
  optimization: {...},  // Optimization settings (minification, etc.)
  plugins: [
    // Other plugins...
    new HtmlWebpackPlugin({
      filename: "app.html",
      template: "src/renderer/index.ejs",
    }),
  ],
};

export default configuration;

In this configuration:

  • The target is set to ["web", "electron-renderer"], enabling both standard web and Electron renderer environments.
  • The entry specifies the entry point for the renderer process, which is src/renderer/App.jsx.
  • The output is bundled into a single file named renderer.js, which is stored in the app/dist/renderer directory. In an Electron app, it's often preferable to bundle everything into a single file since the files are loaded locally.
  • The HtmlWebpackPlugin generates an app.html file from a template (index.ejs), embedding the necessary script to load renderer.js.

We can compile and bundle our frontend code using the following command:

webpack --config ./config/webpack/renderer.mjs

This will produce an app.html file in the app/dist/renderer directory, along with renderer.js.

<!DOCTYPE html>
<html>
   <head>
      <meta charset=utf-8>
      <meta http-equiv=Content-Security-Policy content="script-src 'self' 'unsafe-inline'">
      <title>MyApp</title>
      <script defer=defer src=./renderer.js></script>
   </head>
   <body>
      <div id=root></div>
   </body>
</html>

The HtmlWebpackPlugin correctly injects the <script/> tag to load renderer.js. This app.html can now be loaded into a browser window from the main process.

const appWindow = new BrowserWindow({
  show: false,
  width: 1408,
  height: 896,
});
appWindow.loadURL("app/dist/renderer/app.html");
appWindow.on("ready-to-show", () => {
  appWindow.show();
});

Configuring Webpack for Multiple Browser Windows

With the single window setup complete, let's add another browser window to the app. For example, let's say we want to create a Settings.jsx component within the renderer folder:

electron-app
├── assets
├── app
├── src
│   └── main
│     └── main.js
│   └── renderer
│     └── App.jsx
│     └── Settings.jsx
│     └── index.ejs
├── node_modules
├── package.json

Previously, we bundled all JavaScript code into a single renderer.js file. However, since we're now working with multiple windows, it makes sense to create separate bundles for each window—one for the App window and another for the Settings window. To achieve this, we can specify multiple entry points in Webpack:

// ./config/webpack/renderer.mjs

import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";

const configuration = {
  target: ["web", "electron-renderer"],
  entry: {
    app: "src/renderer/App.jsx",
    settings: "src/renderer/Settings.jsx",
  },
  output: {
    path: "app/dist/renderer",
    publicPath: "./",
    filename: "[name].js", // Use placeholders to generate separate bundles
    library: {
      type: "umd",
    },
  },
  // Other configuration options...
};

export default configuration;

In this configuration:

  • The entry property now contains two entry points: app and settings. Webpack will generate separate bundles for each, named app.js and settings.js respectively.
  • The filename in the output section uses the [name] placeholder to dynamically generate filenames based on the entry point names.

Next, we need to generate two HTML files—one for each window. We can achieve this by adding another instance of HtmlWebpackPlugin to the plugins array:

// ./config/webpack/renderer.mjs

import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";

const configuration = {
  target: ["web", "electron-renderer"],
  entry: {
    app: "src/renderer/App.jsx",
    settings:"src/renderer/Settings.jsx"
  },
  output: {
    path: "app/dist/renderer",
    publicPath: "./",
    filename: '[name].js',
    library: {
      type: "umd",
    },
  },
  module: {...},
  optimization: {...},
  plugins: [
    // Other plugins...
    new HtmlWebpackPlugin({
      filename: "app.html",
      template: "src/renderer/index.ejs",
      chunks: ['app'],  // Load only the 'app' bundle
    }),
    new HtmlWebpackPlugin({
      filename: "settings.html",
      template: "src/renderer/index.ejs",
      chunks: ['settings'],  // Load only the 'settings' bundle
    }),
  ],
};

export default configuration;

By specifying the chunks property for each HtmlWebpackPlugin instance, we ensure that each HTML file only includes the appropriate JavaScript bundle. The final output will include two HTML files:

<!-- app.html -->
<!DOCTYPE html>
<html>
   <head>
      <meta charset=utf-8>
      <meta http-equiv=Content-Security-Policy content="script-src 'self' 'unsafe-inline'">
      <title>MyApp</title>
      <script defer=defer src=./app.js></script>
   </head>
   <body>
      <div id=root></div>
   </body>
</html>

<!-- settings.html -->
<!DOCTYPE html>
<html>
   <head>
      <meta charset=utf-8>
      <meta http-equiv=Content-Security-Policy content="script-src 'self' 'unsafe-inline'">
      <title>MyApp</title>
      <script defer=defer src=./settings.js></script>
   </head>
   <body>
      <div id=root></div>
   </body>
</html>

Finally, from the main process, we can easily create two browser windows, each with its own renderer code:

const appWindow = new BrowserWindow({
  show: false,
  width: 1408,
  height: 896,
});
appWindow.loadURL("app/dist/renderer/app.html");
appWindow.on("ready-to-show", () => {
  appWindow.show();
});

const settingsWindow = new BrowserWindow({
  show: false,
  width: 1408,
  height: 896,
});
settingsWindow.loadURL("app/dist/renderer/settings.html");
settingsWindow.on("ready-to-show", () => {
  settingsWindow.show();
});

With this setup, each browser window will load its own dedicated JavaScript bundle, ensuring that our Electron application is both efficient and modular. This approach not only makes our code easier to manage but also enhances the performance of our application by reducing unnecessary code loading.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.