An Electron app is made of several pieces of code running with different privileges. This chapter shows where the main process, preload script, and renderer live, and what package.json and the Forge configuration control.
Three kinds of code
The main process manages the app lifecycle and windows and talks to the operating system. A renderer behaves like a browser tab with no Node.js access by default. The preload script bridges the two and exposes only what the page needs.
Layout of a hand-built project
Adding a preload and a renderer script to the previous chapter's project gives this layout:
my-electron-app/
├── package.json # name, version, main, scripts, dependencies
├── main.js # main process
├── preload.js # preload script
├── index.html # renderer page
├── renderer.js # script that runs in the renderer
└── node_modules/
The preload script is attached when a window is created, through webPreferences.preload, as an absolute path. Build the path from __dirname so it still resolves after packaging.
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
The preload uses contextBridge to attach a single object to the renderer's window, and the page uses only that object.
// preload.js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
})
// renderer.js, loaded from index.html with <script src="./renderer.js"></script>
const info = document.getElementById('info')
info.innerText = `Chrome ${versions.chrome()}, Node.js ${versions.node()}, Electron ${versions.electron()}`
Layout of an Electron Forge template
A project created from the vite-typescript template keeps its sources in src/ and builds the main, preload, and renderer code separately with Vite.
my-app/
├── forge.config.ts # packaging, installers, plugins
├── forge.env.d.ts # types for globals injected by Forge
├── index.html # renderer page
├── package.json
├── src/
│ ├── main.ts # main process
│ ├── preload.ts # preload script
│ ├── renderer.ts # renderer entry
│ └── index.css
├── tsconfig.json
├── vite.main.config.ts
├── vite.preload.config.ts
└── vite.renderer.config.ts
In development the renderer comes from the Vite dev server; in a packaged app it is loaded from the built files. The template's main.ts tells them apart with globals defined by Forge:
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
} else {
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`))
}
The top of the template's main.ts also checks electron-squirrel-startup, so the app exits immediately when the Windows installer (Squirrel) launches it during install or uninstall.
package.json and forge.config
In a Forge project, package.json holds scripts that call the Forge CLI, and main points at the built main process file (.vite/build/main.js in the Vite templates).
{
"main": ".vite/build/main.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish"
}
}
forge.config.ts defines packaging options (packagerConfig), the installer formats to build (makers), and build plugins (plugins).
import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { VitePlugin } from '@electron-forge/plugin-vite'
const config: ForgeConfig = {
packagerConfig: { asar: true },
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerDeb({})],
plugins: [
new VitePlugin({
build: [
{ entry: 'src/main.ts', config: 'vite.main.config.ts', target: 'main' },
{ entry: 'src/preload.ts', config: 'vite.preload.config.ts', target: 'preload' },
],
renderer: [{ name: 'main_window', config: 'vite.renderer.config.ts' }],
}),
],
}
export default config
Conventions
- Keep operating system work (files, dialogs, notifications) in the main process.
- In the preload, expose small, clearly named functions instead of the whole
ipcRenderer object. - Build file paths from
__dirname or app.getAppPath() so they keep working after packaging. - The main process may use ES modules, but sandboxed preload scripts cannot, so write them in CommonJS style or bundle them.
- Renderers can use any web framework, such as React, Vue, or Svelte.
Summary
- The main process, preload script, and renderer have different privileges and live in separate files.
- The
main field in package.json points at the main process entry file. - Attach the preload through
webPreferences.preload and expose features with contextBridge. - Forge templates manage building and packaging through
src/, Vite config files, and forge.config.ts.
0 comments
Sign in · Sign in to leave a comment.