为 Electron React JS 应用程序创建安装程序 - 安装后运行时不加载 reactJS 组件

Boa*_*rdy 5 installation node.js reactjs electron

我正在使用 Electron 和 ReactJS 开发一个新项目。该项目在开发模式下运行良好,但我正在尝试为 Windows 创建一个安装程序,但无论我尝试什么以及在 Google 上找到什么,都没有任何效果。我只是得到一个空白的白色屏幕。

下面是我的 pacakge.json

{
  "name": "MyApp",
  "description": "My App Description",
  "version": "0.1.2",
  "private": true,
  "homepage": "./",
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "@types/jest": "^26.0.14",
    "@types/node": "^14.11.2",
    "@types/react": "^16.9.50",
    "@types/react-dom": "^16.9.8",
    "bootstrap": "^4.5.2",
    "electron-is-dev": "^1.2.0",
    "electron-settings": "^4.0.2",
    "electron-squirrel-startup": "^1.0.0",
    "react": "^16.13.1",
    "react-bootstrap": "^1.3.0",
    "react-dom": "^16.13.1",
    "react-icons": "^3.11.0",
    "react-json-pretty": "^2.2.0",
    "react-scripts": "3.4.3",
    "react-tooltip": "^4.2.10",
    "typescript": "^4.0.3"
  },
  "main": "src/electron-starter.js",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron-start": "set ELECTRON_START_URL=http://localhost:3000 && electron .",
    "package-win": "electron-packager . --asar --out=release-builds --platform=win32 --arch=x64 --no-prune --ignore=/e2e --overwrite",
    "create-installer-win": "node installers/windows/createInstaller.js"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {

    "electron": "^10.1.3",
    "electron-packager": "^12.0.1",
    "electron-winstaller": "^4.0.1",
    "react-router-dom": "^5.2.0",
    "react-toastify": "^6.0.8"
  }
}
Run Code Online (Sandbox Code Playgroud)

我的electron-start.js 脚本如下

const {electron, Menu, app, BrowserWindow} = require('electron');
// Module to control application life.
//const app = electron.app;
// Module to create native browser window.
//const BrowserWindow = electron.BrowserWindow;


const path = require('path');
const url = require('url');

if (require('electron-squirrel-startup')) app.quit()
// if first time install on windows, do not run application, rather
// let squirrel installer do its work
const setupEvents = require('../installers/setupEvents')
if (setupEvents.handleSquirrelEvent()) {
    console.log("Squirrel event returned true");
    process.exit()
    //return;
}

console.log("Starting main program");

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

/*const env = process.env.NODE_ENV;

let windowUrlBase = "";

if (env === "production")
{
    windowUrlBase = "/";
}
else
{
    windowUrlBase = 'http://localhost:3000';
}*/

let windowUrlBase = 'http://localhost:3000';

function returnMainWindow()
{
    const mainWindow =  new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true,
            //preload: __dirname + '/preload.tsx'
        }
    });

    //const env = process.env.NODE_ENV;
    //console.log("Environment: " + env);
    const isDev = require('electron-is-dev');

    windowUrlBase = "";
    console.log("Not electron dev");
    console.log("dir name: " + __dirname);

    const startUrl = process.env.ELECTRON_START_URL || url.format({
        //pathname: path.join(__dirname, '/../build/index.html'),
        pathname: path.join(__dirname, '../index.html'),
        protocol: 'file:',
        slashes: true,
        webSecurity: false
    });
    mainWindow.loadURL(startUrl);
    
    return mainWindow;
}


function createWindow() {
    // Create the browser window.

    mainWindow = returnMainWindow();



    mainWindow.maximize();

    // and load the index.html of the app.


    // Open the DevTools.
    //mainWindow.webContents.openDevTools();

    // Emitted when the window is closed.
    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 = null
    })
    setMainMenu();
}

function setMainMenu()
{
    const template = [
        {
            label: 'File',
            submenu: [
                {
                    label: 'Exit',
                    accelerator: "ctrl+f4",
                    click() {
                        app.quit();
                    }
                }
            ]
        },
        {
            label: 'Edit',
            submenu: [
                {
                    label: 'Settings',
                    click() {
                        mainWindow.loadURL(windowUrlBase + "/settings");
                    }
                }
            ]
        },
        {
            label: 'Help',
            submenu: [
                {
                    label: 'Show Dev Console',
                    accelerator: "f11",
                    click() {
                        mainWindow.webContents.openDevTools();
                    }
                }
            ]
        }
    ];

    Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}

// 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);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', function () {
    // 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 (mainWindow === null) {
        createWindow()
    }
});
Run Code Online (Sandbox Code Playgroud)

我的创建安装程序脚本如下

const createWindowsInstaller = require('electron-winstaller').createWindowsInstaller
const path = require('path')

getInstallerConfig()
    .then(createWindowsInstaller)
    .catch((error) => {
        console.error(error.message || error)
        process.exit(1)
    })

function getInstallerConfig () {
    console.log('creating windows installer')
    const rootPath = path.join('./')
    const outPath = path.join(rootPath, 'release-builds')

    return Promise.resolve({

        appDirectory: path.join(outPath, 'crash-catch-control-panel-win32-x64'),
        authors: 'Boardies IT Solutions',
        noMsi: true,
        outputDirectory: path.join(outPath, 'windows-installer'),
        exe: 'crash-catch-control-panel.exe',
        setupExe: 'crash-catch-control-panel-installer.exe'
        //setupIcon: path.join(rootPath, 'assets', 'images', 'logo.ico')
    })
}
Run Code Online (Sandbox Code Playgroud)

我的 setupEvents.js 在下面

const electron = require('electron')
const app = electron.app

module.exports = {
    handleSquirrelEvent: function() {
        if (process.argv.length === 1) {
            return false;
        }

        const ChildProcess = require('child_process');
        const path = require('path');

        const appFolder = path.resolve(process.execPath, '..');
        const rootAtomFolder = path.resolve(appFolder, '..');
        const updateDotExe = path.resolve(path.join(rootAtomFolder, 'Update.exe'));
        const exeName = path.basename(process.execPath);
        const spawn = function(command, args) {
            let spawnedProcess, error;

            try {
                spawnedProcess = ChildProcess.spawn(command, args, {detached: true});
            } catch (error) {}

            return spawnedProcess;
        };

        const spawnUpdate = function(args) {
            return spawn(updateDotExe, args);
        };

        const squirrelEvent = process.argv[1];
        switch (squirrelEvent) {
            case '--squirrel-install':
            case '--squirrel-updated':
                // Optionally do things such as:
                // - Add your .exe to the PATH
                // - Write to the registry for things like file associations and
                // explorer context menus

                // Install desktop and start menu shortcuts
                spawnUpdate(['--createShortcut', exeName]);

                setTimeout(app.quit, 1000);
                return true;

            case '--squirrel-uninstall':
                // Undo anything you did in the --squirrel-install and
                // --squirrel-updated handlers

                // Remove desktop and start menu shortcuts
                spawnUpdate(['--removeShortcut', exeName]);

                setTimeout(app.quit, 1000);
                return true;

            case '--squirrel-obsolete':
                // This is called on the outgoing version of your app before
                // we update to the new version - it's the opposite of
                // --squirrel-updated

                app.quit();
                return true;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我的 App.js 在下面

import * as React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom'
import './Stylesheet.css'
import Dashboard from "./Components/Dashboard";
import Settings from "./Components/Settings";
import './ComponentStyles/BreadcrumbNav.css'
import 'react-toastify/dist/ReactToastify.min.css';
import { ToastContainer, toast } from 'react-toastify';
import CustomerDetails from "./Components/CustomerDetails";

toast.configure({
    position: 'top-center',
    hideProgressBar: true
});
function App() {
  return (

      <BrowserRouter>
          <div>
              <Switch>
                  <Route path="/" render={() => <Dashboard  /> } exact />
                  <Route path="/customer-information/:customer_id" render={(props) => <CustomerDetails {...props} />  } exact />
                  <Route path="/settings" render={() => <Settings /> } exact />
              </Switch>
          </div>
      </BrowserRouter>
  );
}

export default App;
Run Code Online (Sandbox Code Playgroud)

当我在应用程序加载时查看 chrome 控制台时,我看到以下错误:

不允许加载本地资源:file:///C:/Users/Chris/AppData/Local/MyApp/app-0.1.2/resources/app.asar/index.html

如上所述,只有当我安装了电子应用程序时才会启动该问题。如果我将它作为 Node 开发服务器的一部分启动,那么它工作得很好。

更新

以下目录结构按要求

项目目录结构

components 目录包含实际的 ReactJS 组件,目录 ComponentStyles 是所有单独的组件样式表。组件是打字稿,因此采用 tsx 格式。

Boa*_*rdy 5

经过大量的尝试和错误,我终于让它工作了。

第一件事是更改我的 App.js,这样我现在不再使用 BrowserRouter,而是使用 HashRouter,如下所示,并使用react-router-dom 中的历史记录

import {HashRouter, useHistory} from "react-router-dom";
const history = createBrowserHistory();

<HashRouter basename="/" history={history} >
          <Switch>
              <Route path="/" exact component={Dashboard} />
              <Route path="/customer-information/:customer_id" component={CustomerDetails} />
              <Route path="/settings" component={Settings} />
              <Route path="*" component={NotFound} />
          </Switch>
      </HashRouter>
Run Code Online (Sandbox Code Playgroud)

然后在每个组件中,当我想更改位置时,我会执行以下操作:

const history = useHistory();
history.push('/newlocation');
Run Code Online (Sandbox Code Playgroud)

当我加载浏览器窗口时,我现在有以下内容:

mainWindow.loadURL(isDev ? windowUrlBase : `file://${__dirname}/../build/index.html`);
Run Code Online (Sandbox Code Playgroud)

并删除了以下块

const startUrl = process.env.ELECTRON_START_URL || url.format({
        //pathname: path.join(__dirname, '/../build/index.html'),
        pathname: path.join(__dirname, '../index.html'),
        protocol: 'file:',
        slashes: true,
        webSecurity: false
    });
Run Code Online (Sandbox Code Playgroud)

isDev 使用以下内容设置:

const isDev = require('electron-is-dev');
Run Code Online (Sandbox Code Playgroud)

对于设置菜单更改使用 IPC 的位置,使用以下命令:

在 Electron-start.js 中

const { ipcMain } = require("electron");

    submenu: [
                    {
                        label: 'Settings',
                        click() {
                            //mainWindow.loadURL(windowUrlBase + "/settings");
                            //history.push('/settings')
                            //location.pathname = "/settings";
                            //ipcMain.send("change-location", "/settings");
                            mainWindow.webContents.send('change-location', '/settings')
                            //mainWindow.location.pathname = "/settings";
                        }
                    }
                ]
Run Code Online (Sandbox Code Playgroud)

然后在整个项目共享的组件中,我有以下内容来接收 IPC 事件

const {ipcRenderer} = window.require('electron')
const history = useHistory();

useEffect(() => {
        ipcRenderer.on('change-location', (event, arg) => {
            history.push(arg);
        });
    })
Run Code Online (Sandbox Code Playgroud)