electron-userland / electron-webpack

Scripts and configurations to compile Electron applications using webpack
https://webpack.electron.build/
905 stars 171 forks source link

Multiple windows with routing not working #325

Open newicz opened 5 years ago

newicz commented 5 years ago

Hello guys, I have a problem I am struggling with for a couple of days now. I am trying to get multiple windows (2 exactly) to work in my electron app. I started from boilerplate and look trough multiple topics on that matter trough out the internet. The solution for multiple windows seems to be, to have ala routing in place. I tried it out and it works for the dev environment (yarn dev) as it is an HTTP, but when I package my app it seems not to work as expected. I have an error in the dev tools (of unpacked win):

chromewebdata/:1 Not allowed to load local resource: file:///C:/Users/grzes/Documents/projects/electron-webpack-quick-start/dist/win-unpacked/resources/app.asar/index.html%3Froute=settings

The problem seems to be this route part I've been adding when I remove it everything works fine (just unable to run multiwindow.

Ok, so let go trough the setup.

I created a class for app window that just takes care of building my window:

import { BrowserWindow } from "electron";
import { format as formatUrl } from 'url';
import * as path from 'path';

class AppWindow
{
    window: BrowserWindow|null;

    constructor(route: string, devMode: boolean = false) {

        this.window = new BrowserWindow({webPreferences: {nodeIntegration: true}});

        if (devMode) {
            this.window.webContents.openDevTools();
            this.window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?route=settings`);
        } else {
            this.window.loadURL(formatUrl({
                pathname: path.join(__dirname, 'index.html?route=settings'),
                protocol: 'file',
                slashes: true
            }))
        }

        this.window.on('closed', () => {
            this.window = null
        })

        this.window.webContents.on('devtools-opened', () => {
            if (this.window) {
                this.window.focus();
                setImmediate(() => {
                    if (this.window) {
                        this.window.focus()
                    }
                });
            }
        })
    }

    get() {
        return this.window;
    }
}

export default AppWindow;

Then in main process, I am creating one main window, and one settings window (on-demand, after clicking a button on main window trough IPC events).

'use strict'

import { app, ipcMain } from 'electron';
import AppWindow from './windows/AppWindow';

const isDevelopment = process.env.NODE_ENV !== 'production';

// global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow: any

function createMainWindow() {
    const window = new AppWindow('widget', isDevelopment).get();
    return window;
}

function createSettingsWindow() {
    const window = new AppWindow('settings', isDevelopment).get();
    return window;
}

// quit application when all windows are closed
app.on('window-all-closed', () => {
  // on macOS it is common for applications to stay open until the user explicitly quits
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // on macOS it is common to re-create a window even after all windows have been closed
  if (mainWindow === null) {
    mainWindow = createMainWindow()
  }
})

// create main BrowserWindow when electron is ready
app.on('ready', () => {
  mainWindow = createMainWindow();
})

ipcMain.on('settings', () => {
    createSettingsWindow();
})

In renderer, I tried to resolve the route parameter and decide which module to load (semi routing testing solution):

import Vue from 'vue';
import App from './App.vue';
import Settings from './Settings.vue';

const route = new URLSearchParams(window.location.search).get('route') || 'widget';

if (route === 'widget') {
    new Vue({
        components: { App },
        template: '<App/>'
    }).$mount('#app')
}

if (route === 'settings') {
    new Vue({
        components: { Settings },
        template: '<Settings/>'
    }).$mount('#app')
}

And there I stuck, it seems to be unable to pass the variable trough URL on windows packed build. I lost hope today, please help me out because I can't move forward with it at this point, I am out of ideas on how to do it. I'm gonna add that the idea is, to have a simple app window with a settings button, that will open another window with settings of this app.

loopmode commented 5 years ago

Hmm I've done something fairly similar with react and react-router, and it worked rather smoothly. However, I was using hashes in the URL. I'll try to dig out the project and put it on GitHub.

newicz commented 5 years ago

I have been trying to do it with # as well but with the same result. Here you have the repository with my test code: https://github.com/newicz/routing-electron-test

loopmode commented 5 years ago

Okay, I had it on GitHub all along: https://github.com/loopmode/react-desktop

So.. It's not quite the same. We have similar goals, but besides me using React and you Vue, we have a different approach.

Yours is to control the windows from inside the main process. Mine is to not even know about the windows inside the main process.

The repo above has an "AppLauncher" app that can launch apps either in the main window, or in their own window. In the end, I want to have something in the system tray to launch apps.

In my setup, the renderer boots up, and uses react-router to define different paths for different apps. Not sure whether you are familiar with reading react apps, but I suggest you check out my repo and get the app running and do some hands-on.

loopmode commented 5 years ago

BTW one guy's solution was to use file:: win.loadURL(file://${__dirname}/dist/index.html);

newicz commented 5 years ago

I cannot get how it works in your example (I mean I understand how this is set up more or less, I never used React). I have no idea how it allows you to run routing. I changed the code to not create a window from main process now and try out your approach to just open with target="_blank", this works like a charm opening a new window. The problem is that if my routing is pointing to some URL like /settings I got 404 in this new window.

Here you can see the changes I made in my renderer

import Vue from 'vue';
import App from './App.vue';
import Settings from './Settings.vue';

const NotFound = { template: '<p>Page not found</p>' }

const routes = {
    '/': App,
    '/settings': Settings
}

new Vue({
    el: '#app',
    data: {
      currentRoute: window.location.pathname
    },
    computed: {
      ViewComponent () {
          // @ts-ignore
        return routes[this.currentRoute] || NotFound
      }
    },
    // @ts-ignore
    render (h) { return h(this.ViewComponent) }
})

Then in App i have link to settings:

<template>
    <div>
        <p>{{name}}</p>
        <a href="/settings" target="_blank">settings (new)</a>
        <Test/>
    </div>
</template>

<script lang="ts">
    import Test from './Test.vue';
import { ipcRenderer } from 'electron';

    export default {
        components: {
            Test
        },
        data() {
            return { name: 'poel' };
        }
    }
</script>

This is strange for me, because it work when I run your example, it does not if I run mine...

When I try to run for example: http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}/settings I got this error in console:

VM110 hook.js:10 Refused to execute inline script because it violates the following Content Security Policy directive: "default-src 'none'". Either the 'unsafe-inline' keyword, a hash ('sha256-tNDGcuur+Xq1hWGosyhxNn/LdCiCH7CeKGodlT3JvPg='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback.
loopmode commented 5 years ago

Please note that I am using hash history, not browser history, because we cannot have something like URL rewriting on the server that translates /settings to index.html. while it looks like /path in my example, the hash history makes /#path of it. (In my current project, I even use Memory history in the first place, see https://github.com/ReactTraining/history/blob/master/docs/GettingStarted.md)

No clue about the error you received.. I hope someone with Vue experience can chime in here. And, if I find some time, I'll try out some Vue basics myself, but no promises there.

vcombey commented 3 years ago

I achieve what you wanted to do @newicz by changing this.window.loadURL(formatUrl({ pathname: path.join(__dirname, 'index.html?route=settings'), protocol: 'file', slashes: true })) by returnfile://${__dirname}/../renderer/index.html?route=settings`` as it doesn't escape the "?" any more. now looking to find a cross platform way to do that, I don't know yet what exactly formatUrl does

Nedi11 commented 1 year ago

I had to do this newWindow.loadURL(file://${__dirname}/../renderer/index.html#/route), ty @vcombey , I was doing path.join and it was getting escaped so it wasn't working