You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Electron+React+Express应用开发正常但打包后请求挂起无响应

问题描述

使用React构建Electron应用,通过Webpack打包,后端基于Express.js开发。开发阶段执行npm start时一切正常,后端能正常启动并处理HTTP请求;但执行npm run make完成生产打包后,前端向后端发送的HTTP请求一直处于挂起(pending)状态,无法收到响应。

环境配置

  • 前端:React框架,Webpack打包
  • 后端:Express.js服务,运行在http://localhost:8000
  • 开发环境:请求处理正常,无异常
  • 生产构建:请求持续pending,无响应

主进程代码(index.ts)

import { app, BrowserWindow, screen } from "electron";
import { startServer, checkServerHealth } from "./backend/server";

// This allows TypeScript to pick up the magic constants auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
import("electron-squirrel-startup").then((module) => {
  if (module.default) {
    app.quit();
  }
});

const createWindow = (): void => {
  // Get the primary display's work area size (excluding the taskbar)
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;

  // Create the browser window to fit the full screen dimensions with the taskbar visible.
  const mainWindow = new BrowserWindow({
    width, // Set width to the screen's width
    height, // Set height to the screen's height
    webPreferences: {
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
      webSecurity: false, // This should only be used for development
    },
  });

  // Load the index.html of the app.
  mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);

  // Set the CSP directly in the HTML file (recommended approach):
  mainWindow.webContents.on("did-finish-load", () => {
    mainWindow.webContents.executeJavaScript(`
      const meta = document.createElement('meta');
      meta.httpEquiv = 'Content-Security-Policy';
      meta.content = "default-src 'self'; connect-src 'self' http://localhost:3000 http://localhost:8000; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';";
      document.getElementsByTagName('head')[0].appendChild(meta);
    `);
  });
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on("ready", async () => {
  try {
    console.log("Starting backend...");
    startServer();
    console.log("Backend started. Checking health...");
    await checkServerHealth();
    console.log("Backend health check passed. Creating window...");
    createWindow();
  } catch (error) {
    console.error("Failed to start backend or check health:", error.message);
  }
});

// Quit when all windows are closed, except on macOS.
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  // On macOS, 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();
  }
});
可能的原因及解决方案

1. 后端端口冲突或启动异常

生产环境下可能存在端口被占用,或后端打包后依赖/路径加载失败的情况:

  • 修改startServer函数,实现动态获取可用端口,避免固定端口冲突:
    import { createServer } from 'http';
    import express from 'express';
    
    const app = express();
    let server;
    
    export async function startServer() {
      return new Promise((resolve, reject) => {
        let port = 8000;
        const tryListen = () => {
          server = createServer(app).listen(port, () => {
            console.log(`Server running on port ${port}`);
            resolve(port);
          });
          server.on('error', (err: any) => {
            if (err.code === 'EADDRINUSE') {
              port++;
              tryListen();
            } else {
              reject(err);
            }
          });
        };
        tryListen();
      });
    }
    
  • 检查Electron Forge打包配置,确保后端文件被纳入打包范围,避免依赖缺失。

2. CSP配置失效

动态注入CSP在生产打包后可能不生效,导致请求被拦截:

  • 直接在前端index.html的<head>中添加CSP meta标签,替代动态注入:
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' http://localhost:8000 ws://localhost:8000; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';">
    
  • 生产环境建议开启webSecurity: true,通过正确配置CSP保障安全,避免关闭安全策略。

3. 前端请求地址硬编码

生产环境后端端口可能因冲突变更,但前端仍请求固定端口导致失败:

  • 通过预加载脚本传递后端实际运行端口给前端:
    1. 主进程中启动后端后传递端口:
      app.on("ready", async () => {
        try {
          console.log("Starting backend...");
          const port = await startServer();
          console.log("Backend started on port:", port);
          await checkServerHealth(port);
          console.log("Backend health check passed. Creating window...");
          const mainWindow = createWindow();
          // 向渲染进程发送端口信息
          mainWindow.webContents.on('did-finish-load', () => {
            mainWindow.webContents.send('server-port', port);
          });
        } catch (error) {
          console.error("Failed to start backend or check health:", error.message);
        }
      });
      
    2. 预加载脚本中暴露端口给前端:
      // preload.ts
      import { ipcRenderer, contextBridge } from 'electron';
      
      contextBridge.exposeInMainWorld('electronAPI', {
        getServerPort: () => new Promise(resolve => {
          ipcRenderer.once('server-port', (_, port) => resolve(port));
        })
      });
      
    3. 前端动态拼接请求地址:
      // React组件中
      const fetchData = async () => {
        const port = await window.electronAPI.getServerPort();
        const response = await fetch(`http://localhost:${port}/api/xxx`);
        // 处理响应逻辑
      };
      

4. 打包配置遗漏后端资源

确保Electron Forge正确打包后端文件:

  • 在forge.config.js中添加后端资源到打包列表:
    module.exports = {
      packagerConfig: {
        extraResource: ['./backend/**/*'],
      },
      // ...其他配置
    };
    
  • 若后端使用原生模块,执行npx electron-rebuild重新编译依赖,适配Electron版本。

内容的提问来源于stack exchange,提问作者Oussama

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 16:32:03