An Electron preload script acts as a bridge between the main process and the renderer process. It runs in the renderer process but has access to Node.js APIs. This is useful for exposing specific Node.js functionality to the renderer in a controlled way, while maintaining security by preventing direct access to all Node.js features.
mkdir electron-preload-typescript
cd electron-preload-typescript
npm init -y
npm install electron typescript --save - dev
tsconfig.json
file in the root directory:{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Create a src
directory in the root of your project and inside it, create a preload.ts
file.
// src/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// Expose a simple function to the renderer
contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (message: string) => ipcRenderer.send('message', message),
receiveMessage: (callback: (event: Electron.IpcRendererEvent, arg: any) => void) =>
ipcRenderer.on('reply', callback)
});
src/main.ts
):// src/main.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import * as path from 'path';
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../dist/preload.js')
}
});
mainWindow.loadFile('index.html');
ipcMain.on('message', (event, arg) => {
console.log(`Received message: ${arg}`);
event.sender.send('reply', 'Message received');
});
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all - closed', function () {
if (process.platform!== 'darwin') app.quit();
});
index.html
and renderer.js
):<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF - 8">
<title>Electron Preload with TypeScript</title>
</head>
<body>
<button id="sendButton">Send Message</button>
<script src="renderer.js"></script>
</body>
</html>
// renderer.js
document.getElementById('sendButton').addEventListener('click', () => {
window.electronAPI.sendMessage('Hello from renderer');
window.electronAPI.receiveMessage((event, arg) => {
console.log(arg);
});
});
When exposing Node.js APIs to the renderer process, it’s important to only expose the necessary functionality. Use contextBridge.exposeInMainWorld
to create a safe and controlled bridge between the main and renderer processes.
Use ipcRenderer
in the renderer process and ipcMain
in the main process to send and receive events. This allows for communication between the two processes.
In the preload script, add proper error handling to prevent crashes. You can use try - catch
blocks and log errors to the console or a file.
// src/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
try {
contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (message: string) => ipcRenderer.send('message', message),
receiveMessage: (callback: (event: Electron.IpcRendererEvent, arg: any) => void) =>
ipcRenderer.on('reply', callback)
});
} catch (error) {
console.error('Error in preload script:', error);
}
Break your preload script into smaller functions and modules. This makes the code easier to understand, test, and maintain.
Using Electron preload with TypeScript can significantly enhance the security, maintainability, and development experience of your Electron applications. By understanding the fundamental concepts, following the usage methods, and adopting common and best practices, you can build robust and reliable desktop applications.