I'm building an app with React and Electron and trying to create a communication channel between the main (electron.js in my case) and renderer processes. By achieving this, I want to access the Store object created in my main process to save user preferences etc.
I have set up a preload script and provided the path in my main.js (electron.js). But when I try to access the method defined in the preload.js from the renderer like this window.electronAPI.sendData('user-data', this.state), it does not recognise the electronAPI (undefined variable electronAPI). Also the console.log in my preload.js is never shown when a window is loaded. Hence I assume the preload script never gets loaded and I don't know why. Any help is very much appreciated!
EDIT: The preload script seems to load now because the console.log gets printed. Maybe preload.js did load before too but I could not see it since I could only access the application by opening localhost:3000 in the browser. Now the app opens in a BrowserWindow. However the 'electronAPI' defined in the preload script cannot be accessed still.
Here is the code:
electron.js (main.js)
const electron = require('electron');
const path = require('path');
const {app, BrowserWindow, Menu, ipcMain} = electron;
let isDev = process.env.APP_DEV ? (process.env.APP_DEV.trim() === "true") : false;
function createWindow (){
const win = new BrowserWindow({
width: 970,
height: 600,
backgroundColor: '#0C0456',
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js')
}
});
//if in development mode, load window from localhost:3000 otherwise build from index.html
try {
win.webContents.loadURL(isDev
? 'http://localhost:3000'
: `file://${path.join(__dirname,'../build/index.html')}`
)
} catch(e){
console.error(e)
}
win.webContents.openDevTools({"mode":"detach"});
//Remove menu
Menu.setApplicationMenu(null);
win.once('ready-to-show', () => win.show());
console.log("main window is shown");
console.log(typeof path.join(__dirname, 'preload.js'));
win.on('crashed',() => {
console.error(`The renderer process has crashed.`);
})
}
app.on( 'ready', () => {
createWindow(); // open default window
} );
ipcMain.on('user-data', (event, args)=>{
console.log(args);
});
preload.js
const {contextBridge, ipcRenderer} = require("electron");
const validChannels = ['user-data'];
console.log("this is the preload script");
contextBridge.exposeInMainWorld('electronAPI', {
sendData: (channel, data) => {
if(validChannels.includes(channel)){
ipcRenderer.send(channel, data);
}
}
})
Related
I recently made an Electron app that is essentially a web browser and it has 3 windows and they all get destroyed when the user closes them, leaving no excess process behind this is the only complex part of the app but Kaspersky flags it as a Trojan.
my index.js
const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron');
const path = require('path');
app.commandLine.appendSwitch("disable-http-cache");
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
// eslint-disable-next-line global-require
if (require('electron-squirrel-startup')) {
app.quit();
}
function createMacroWindow(macroUrl) {
let macroWindow = new BrowserWindow({
width:800,
height:500,
frame:false,
show:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences:{
devTools: false,
webviewTag: true,
nodeIntegration: true,
contextIsolation: false
}
})
macroWindow.loadFile(path.join(__dirname, './windows/macro-window.html'));
macroWindow.webContents.on('did-finish-load', () => {
macroWindow.webContents.send("loadURL", macroUrl)
})
macroWindow.center();
macroWindow.openDevTools();
macroWindow.show();
macroWindow.on("close", () => {
macroWindow = null;
})
require("#electron/remote/main").enable(macroWindow.webContents);
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1000,
height: 700,
frame:false,
show:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences: {
webviewTag: true,
nodeIntegration: true,
devTools: true, // Geliştirici konsolunu kökten kısıtlayan kod (bu olmazsa CTRL+SHIFT+I yapınca yine açılır)
contextIsolation: false,
},
});
let splashScreen = new BrowserWindow({
width:1000,
height:700,
frame:false,
icon: path.join(__dirname, "./assets/icons/icon.png"),
webPreferences: {
devTools:false
}
})
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
splashScreen.loadFile(path.join(__dirname, './windows/splash-window.html'));
splashScreen.center();
function destroySplashScreen() {
splashScreen.close();
splashScreen = null;
}
mainWindow.webContents.on('did-finish-load', function() {
mainWindow.show();
mainWindow.maximize();
splashScreen.isDestroyed() ? console.log("splash screen already destroyed") : destroySplashScreen();
});
require('#electron/remote/main').initialize()
require("#electron/remote/main").enable(mainWindow.webContents)
ipcMain.handle(
'DESKTOP_CAPTURER_GET_SOURCES',
(event, opts) => desktopCapturer.getSources(opts)
)
let macroUrl;
ipcMain.on("openMacroWindow", (e, urlToLoad) => {
macroUrl = urlToLoad;
createMacroWindow(macroUrl);
})
ipcMain.on("closeApp", () => {
BrowserWindow.getAllWindows().forEach(window => window.close());
})
ipcMain.on('clearCache', () => {
mainWindow.webContents.session.clearStorageData([], function (data) {
console.log(data);
})
mainWindow.webContents.session.clearCache();
})
mainWindow.on("close", () => {
app.quit();
})
// Open the DevTools.
// mainWindow.webContents.openDevTools(); // Geliştirici konsolunu kapatmak için bu satırı silebilirsiniz
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// app.disableHardwareAcceleration()
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
splashScreen is the window that opens while the mainWindow loads, it closes when the main loads and is set to null afterwards just like the macroWindow.
macroWindow is opened when the ipcMain is told to openMacroWindow
Fix
I downgraded to electron-forge version 5.2.4 from 6.x version, this fixed my issue but this is not an ideal solution.
I'm trying to setup communication between my Vue browser app and the electron main process, but it is not working.
Before startBot() is even called, I get an error message that __Dirname is unknown. But this constant is nowhere in to be found in the code.
What am I doing wrong?
https://gist.github.com/Quenos/7d6dbe4f5410739499ea9e3b0b4f961a.js
This is the background.js where I open the browser window with a preload. This has the purpose of making window available to the browser process
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1300,
height: 1100,
title: "Hedgehog TRDR Bot",
icon: path.join(__static, "hedgehog.jpg"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// __static is set by webpack and will point to the public directory
preload: path.resolve(__static, "preload.js"),
},
});
This is said preload.js
const { contextBridge, ipcRenderer } = require("electron");
const validChannels = ["READ_FILE", "WRITE_FILE"];
contextBridge.exposeInMainWorld("ipc", {
send: (channel, data) => {
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on: (channel, func) => {
if (validChannels.includes(channel)) {
// Strip event as it includes `sender` and is a security risk
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
The main process which contains listeners that then will do file handling
const { ipcMain } = require("electron");
const fs = require("fs");
var file;
ipcMain.on("OPEN_FILE", (event, payload) => {
console.log("daaro");
file = fs.createWriteStream(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
ipcMain.on("TEST_FILE", (event, payload) => {
console.log("daaro");
file.write(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
And the browser process which send requests to do file handling:
async startBot() {
window.ipc.send("OPEN_FILE", { path: "./HH_trdr_bot.csv" });
}
In the meantime I've found this article that perfectly answers my question
https://medium.com/swlh/how-to-safely-set-up-an-electron-app-with-vue-and-webpack-556fb491b83
Docs of Vue CLI Plugin Electron Builder contain description of how to do it, but it's a little scattered.
First, see how to configure a preload script, i.e.:
vue.config.js
module.exports = {
// ...
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
}
}
}
and then, repeat the path in BrowserWindow's webPreferences.preload constructor option.
preload: path.join(__dirname, 'preload.js')
Finally, in src/preload.js expose the IPC Renderer as described in IPC Without Node Integration.
src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
// whitelist channels
let validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['fromMain']
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
It might be also a good idea to unsubscribe the event listener when the component that subscribed is about to be destroyed.
I made mine working by putting the preload.js file inside public folder, in the https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#preload-files docs, there is a little diagram under Folder Structure section that says ├── public/ # Files placed here will be available through __static or process.env.BASE_URL, so what I did is simply, use the __static variable as described in docs, and append it with \preload.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: __static + '/preload.js', // <-- simple solution
}
})
I've been experiencing some issues with an Electron + Create React App app I'm making. It's an offline app for cost calculation, I need to persist a couple user settings, for this I used https://github.com/sindresorhus/electron-store. Like with most electron's modules, I have to import it as:
const Store = window.require("electron-store");
To avoid webpack's conflicts. By searching I found that for most people setting nodeIntegration: true when creating electron's BrowserWindow would avoid the problem, but it's not my case, I keep getting the same error.
What I've already tried:
Using plain require: It results in TypeError: fs.existsSync is not a function, and in console: Can't resolve 'worker_threads' in '...\node_modules\write-file-atomic'
Use a module to override webpack config: I used craco to set target to electron-renderer. It results in a blank page when I launch the app, with an error in devtools telling ReferenceError: require is not defined
Additional info is that I'm not using typescript but plain js so using "declare global" and such won't work
My public/electron.js file:
const electron = require("electron");
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require("path");
const isDev = require("electron-is-dev");
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 680,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadURL(
isDev
? "http://localhost:3000"
: `file://${path.join(__dirname, "../build/index.html")}`
);
mainWindow.on("closed", () => (mainWindow = null));
if (!isDev) mainWindow.setMenu(null);
}
app.on("ready", createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
electron.app.allowRendererProcessReuse = true;
app.on("activate", () => {
if (mainWindow === null) {
createWindow();
}
});
You have tu create a preoload script on electron.
Create a file named preload.js on the directory where you have de electron main script, Put this code on it:
window.require = require;
Go to your electron main script and type this in the code where you create the window:
win = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
nodeIntegration: false,
preload: __dirname + '/preload.js'
},
})
With this you will preload the script before all, this fixed the problem for me, I hope also for you.
Electron 12 changed the default setting of contextIsolation for webPreferences, as documented in their breaking changes for Electron 12. Setting it to false will allow you access to require() again, as per their notes:
In Electron 12, contextIsolation will be enabled by default. To restore the previous behavior, contextIsolation: false must be specified in WebPreferences.
We recommend having contextIsolation enabled for the security of your application.
Another implication is that require() cannot be used in the renderer process unless nodeIntegration is true and contextIsolation is false.
mainWindow = new BrowserWindow({
width: 900,
height: 680,
webPreferences: {
contextIsolation: false,
nodeIntegration: true
}
}
I have all my html handlers in a second file (b.js). They look like this:
window.onload = function () {
let btn = window.getElementById('btn');
button.addEventListener('click', fn);
}
This works fine, but I want to make a button to open another window, so I tried adding an exported method in main.js. My full main.js is below:
const {app, BrowserWindow} = require('electron');
let mainWindow;
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 500,
height: 300,
frame: false,
transparent: true,
resizable: false,
webPreferences: {
nodeIntegration: true,
}
});
mainWindow.setResizable(false);
mainWindow.loadFile('index.html');
mainWindow.webContents.openDevTools();
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow.quit();
})
}
app.on('ready', createWindow);
module.exports = {
openMainScreen: function () {
mainWindow.loadFile("mainScreen.html");
mainWindow.resizeTo(1200, 800);
}
};
If I try to require(main.js)in b.js as I thought I should: I get this error:
Uncaught TypeError: Cannot read property 'on' of undefined
Pointing to app.on('ready'.... Looking at this post: Cannot read property 'on' of undefined in electron javascript It says the app is starting twice. How can I resolve this?
The way I fixed this was to use electrons built in IPCmain to communicate between javascript that was being rendered with the html and the main javascript that ran the app.
https://electronjs.org/docs/api/ipc-main
Could someone please help me to implement a loading animation for my Electron app ?
I am learning this new technology and it would be nice to understand the correct way to do that.
I am thinking about something like :
app.on('ready', () => {
// show main content
})
app.on('not-ready', () => {
// show loading animation
})
As far as I know there is no event emitted from app before ready (only exception is will-finish-launching available only on macOS).
Furthermore, you cannot open any BrowserWindow before app is ready, so you should really wait it.
However, if your main application window loading very slow, you can still open a "loading window" before that and switch them when your main window is ready.
const { app, BrowserWindow } = require('electron')
app.on('ready', () => {
let main = null
let loading = new BrowserWindow({show: false, frame: false})
loading.once('show', () => {
main = new BrowserWindow({show: false})
main.webContents.once('dom-ready', () => {
console.log('main loaded')
main.show()
loading.hide()
loading.close()
})
// long loading html
main.loadURL('http://spacecrafts3d.org')
})
loading.loadURL('loding.html')
loading.show()
})
You can use win.on('ready-to-show') instead of win.webContents.on('dom-ready') everywhere if you want to eliminate visual flash (but losing some speed)
window.open()
If you want to do the same for BrowserWindow opened in renderer process by window.open(), you can use new-window event of webContents if nativeWindowOpen is true
main = new BrowserWindow({
webPreferences: {
nativeWindowOpen: true
}
})
main.webContents.on('new-window', (event, url) => {
// there are more args not used here
event.preventDefault()
const win = new BrowserWindow({show: false})
win.webContents.once('dom-ready', () => {
win.show()
loading.hide() // don't destroy in this case
})
win.loadURL(url)
loading.show()
event.newGuest = win
})
It can be done by displaying a new BrowserWindow displaying the activity loader , until the main app fully loads .
Let's define a createWindow funtion (as given in docs) which is responsible for loading the main app for the user as :
var loadingwindow = null; // Responsible for creating activity loader
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
win.loadFile('index.html')
loadingwindow.hide() // Used to hide the loading screen when the contents in main app are loaded
}
Now , in order to display the loadingwindow screen , we need to place it in app.on('ready' , callback_fn) as shows here :
app.on("ready" , () => {
loadingwindow = new BrowserWindow({
frame : false,
movable : false,
})
loadingwindow.loadFile('activity.html') // To load the activity loader html file
loadingwindow.show();
})
That's it ! ,
To check if it is working properly , wrap the setTimeout function over the app.whenReady().then(createWindow)