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.
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:
target
is set to ["web", "electron-renderer"]
, enabling both standard
web and Electron renderer environments.entry
specifies the entry point for the renderer process, which is
src/renderer/App.jsx
.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.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();
});
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:
entry
property now contains two entry points: app
and settings
.
Webpack will generate separate bundles for each, named app.js
and
settings.js
respectively.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.