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 and part 8.
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:
1electron-app 2├── assets 3├── app 4├── src 5│ └── main 6│ └── main.js 7│ └── renderer 8│ └── App.jsx 9│ └── index.ejs 10├── node_modules 11├── 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:
1// ./config/webpack/renderer.mjs 2 3import webpack from "webpack"; 4import HtmlWebpackPlugin from "html-webpack-plugin"; 5 6const configuration = { 7 target: ["web", "electron-renderer"], 8 entry: "src/renderer/App.jsx", 9 output: { 10 path: "app/dist/renderer", 11 publicPath: "./", 12 filename: "renderer.js", 13 library: { 14 type: "umd", 15 }, 16 }, 17 module: {...}, // Module configuration (loaders, etc.) 18 optimization: {...}, // Optimization settings (minification, etc.) 19 plugins: [ 20 // Other plugins... 21 new HtmlWebpackPlugin({ 22 filename: "app.html", 23 template: "src/renderer/index.ejs", 24 }), 25 ], 26}; 27 28export 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:
1webpack --config ./config/webpack/renderer.mjs
This will produce an app.html file in the app/dist/renderer directory, along with renderer.js.
1<!DOCTYPE html> 2<html> 3 <head> 4 <meta charset=utf-8> 5 <meta http-equiv=Content-Security-Policy content="script-src 'self' 'unsafe-inline'"> 6 <title>MyApp</title> 7 <script defer=defer src=./renderer.js></script> 8 </head> 9 <body> 10 <div id=root></div> 11 </body> 12</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.
1const appWindow = new BrowserWindow({ 2 show: false, 3 width: 1408, 4 height: 896, 5}); 6appWindow.loadURL("app/dist/renderer/app.html"); 7appWindow.on("ready-to-show", () => { 8 appWindow.show(); 9});
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:
1electron-app 2├── assets 3├── app 4├── src 5│ └── main 6│ └── main.js 7│ └── renderer 8│ └── App.jsx 9│ └── Settings.jsx 10│ └── index.ejs 11├── node_modules 12├── 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:
1// ./config/webpack/renderer.mjs 2 3import webpack from "webpack"; 4import HtmlWebpackPlugin from "html-webpack-plugin"; 5 6const configuration = { 7 target: ["web", "electron-renderer"], 8 entry: { 9 app: "src/renderer/App.jsx", 10 settings: "src/renderer/Settings.jsx", 11 }, 12 output: { 13 path: "app/dist/renderer", 14 publicPath: "./", 15 filename: "[name].js", // Use placeholders to generate separate bundles 16 library: { 17 type: "umd", 18 }, 19 }, 20 // Other configuration options... 21}; 22 23export 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:
1// ./config/webpack/renderer.mjs 2 3import webpack from "webpack"; 4import HtmlWebpackPlugin from "html-webpack-plugin"; 5 6const configuration = { 7 target: ["web", "electron-renderer"], 8 entry: { 9 app: "src/renderer/App.jsx", 10 settings:"src/renderer/Settings.jsx" 11 }, 12 output: { 13 path: "app/dist/renderer", 14 publicPath: "./", 15 filename: '[name].js', 16 library: { 17 type: "umd", 18 }, 19 }, 20 module: {...}, 21 optimization: {...}, 22 plugins: [ 23 // Other plugins... 24 new HtmlWebpackPlugin({ 25 filename: "app.html", 26 template: "src/renderer/index.ejs", 27 chunks: ['app'], // Load only the 'app' bundle 28 }), 29 new HtmlWebpackPlugin({ 30 filename: "settings.html", 31 template: "src/renderer/index.ejs", 32 chunks: ['settings'], // Load only the 'settings' bundle 33 }), 34 ], 35}; 36 37export 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:
1<!-- app.html --> 2<!DOCTYPE html> 3<html> 4 <head> 5 <meta charset=utf-8> 6 <meta http-equiv=Content-Security-Policy content="script-src 'self' 'unsafe-inline'"> 7 <title>MyApp</title> 8 <script defer=defer src=./app.js></script> 9 </head> 10 <body> 11 <div id=root></div> 12 </body> 13</html> 14 15<!-- settings.html --> 16<!DOCTYPE html> 17<html> 18 <head> 19 <meta charset=utf-8> 20 <meta http-equiv=Content-Security-Policy content="script-src 'self' 'unsafe-inline'"> 21 <title>MyApp</title> 22 <script defer=defer src=./settings.js></script> 23 </head> 24 <body> 25 <div id=root></div> 26 </body> 27</html>
Finally, from the main process, we can easily create two browser windows, each with its own renderer code:
1const appWindow = new BrowserWindow({ 2 show: false, 3 width: 1408, 4 height: 896, 5}); 6appWindow.loadURL("app/dist/renderer/app.html"); 7appWindow.on("ready-to-show", () => { 8 appWindow.show(); 9}); 10 11const settingsWindow = new BrowserWindow({ 12 show: false, 13 width: 1408, 14 height: 896, 15}); 16settingsWindow.loadURL("app/dist/renderer/settings.html"); 17settingsWindow.on("ready-to-show", () => { 18 settingsWindow.show(); 19});
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.