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

Electron Forge+React+Socket.IO应用ipcRenderer导入报错求助

问题:Electron Forge + React 中导入ipcRenderer出现"Cannot find module 'fs'"错误

项目背景

我开发了一个基于Electron Forge、ReactJS和Socket.IO的应用,相关代码如下:

preload.js

import { contextBridge, ipcRenderer } from "electron";

window.addEventListener("DOMContentLoaded", () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector);
    if (element) element.innerText = text;
  };

  for (const type of ["chrome", "node", "electron"]) {
    replaceText(`${type}-version`, process.versions[type]);
  }
});

let state = {
  config: {},
  store: {},
};

// Function to update the state
const updater = (key, value) => {
  state[key] = value;

  ipcRenderer.send("updater", {
    key: key,
    value: value,
  });
};

const URL = `ws://localhost:3010`; // Test port

const io = require("socket.io-client");
const socket = io(URL);

socket.on("connect", () => {
  console.log("Connect...");
});

socket.on("upload", (msg) => {
  let json = JSON.parse(msg);
  let key = json.key;
  let value = json.value; 

  if (key == "config") updater("config", value);
  if (key == "store") updater("store", value);
});

ipcRenderer.on('command', (payload) => {
  socket.emit("command", JSON.stringify(payload));
});

contextBridge.exposeInMainWorld("stateAPI", {
  state: state,
  updater: updater, // Expose the update function
});

main.js

// Modules to control application life and create native browser window
const { app, BrowserWindow } = require("electron");
const path = require("path");

function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 640,
    height: 480,
    webPreferences: {
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
      webSecurity: false, // Need this to use Socket.io? Is there other way to do it?
    },
  });

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

  // if embedded Linux set fullscreen
  if (process.arch.includes("arm")) {
    mainWindow.setFullScreen(true);
  }

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

// 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.whenReady().then(() => {
  createWindow();

  app.on("activate", function () {
    console.log("-----> activate");
    // On macOS 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();
  });
});

// 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", function () {
  console.log("-----> window-all-closed");
  if (process.platform !== "darwin") app.quit();
});

// 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 require them here.

process.on("uncaughtException", (error) => {
  console.error("Uncaught Exception:", error);
  fs.writeFileSync(
    "/home/torizon/app/error.log",
    `Uncaught Exception: ${error.message}\n${error.stack}`
  );
});

React组件(index.js)

import React, { useEffect, useState } from "react";
import { ipcRenderer } from "electron"; // <=== 此处引发错误

const command = (payload) => {
  ipcRenderer.send("command", payload);
};

const Home = () => {
  const [state, setState] = useState(window.stateAPI.state);

  useEffect(() => {
    const handleStateUpdate = (key, value) => {
      let clone = Object.assign({}, state);
      clone[key] = value;
      setState(clone);
    };

    ipcRenderer.on("state_update", handleStateUpdate);

    // Clean up the listener on component unmount
    return () => {
      ipcRenderer.removeListener("state_update", handleStateUpdate);
    };
  }, []);

  return (
    <div>
      <h1>{state.config}</h1>
      <h1>{state.store}</h1>
      <button onClick={() => command("Test")}>Click me</button>
    </div>
  );
};

export default Home;

webpack配置文件

webpack.main.config.js

module.exports = {
  entry: './src/main.js',
  module: {
    rules: require('./webpack.rules'),
  },
};

webpack.renderer.config.js

const rules = require("./webpack.rules");

rules.push({
  test: /\.css$/,
  use: [
    {
      loader: "style-loader",
      options: {
        esModule: false,
      },
    },
    {
      loader: "css-loader",
      options: {
        esModule: false,
        modules: {},
      },
    },
  ],
});
rules.push({
  test: /\.(png|jpg|jpeg|gif)$/i, // Add support for image formats
  type: 'asset/resource', // This will emit a separate file for the image
});

module.exports = {
  // Put your normal webpack config below here
  module: {
    rules,
  },
};

错误信息

当在React组件中导入ipcRenderer时,出现如下错误:

index.js:1 Uncaught Error: Cannot find module 'fs'
    at webpackMissingModule (index.js:1:1)
    at eval (index.js:1:1)
    at ./node_modules/electron/index.js (index.js:62:1)
    at __webpack_require__ (index.js:8878:32)
    at fn (index.js:9073:21)
    at eval (index.js:7:66)
    at ./src/components/home/index.js (index.js:945:1)
    at __webpack_require__ (index.js:8878:32)
    at fn (index.js:9073:21)
    at eval (app.jsx:5:74)

注释掉导入语句则无报错,但无法在Electron与应用间同步state数据。


问题成因

  1. Electron进程隔离机制:React运行在渲染进程中,渲染进程默认没有Node.js环境的访问权限(比如fs是Node.js核心模块,渲染进程无法直接调用)。直接在渲染进程中导入electron模块,Webpack会尝试打包Node.js核心模块,导致找不到fs的错误。
  2. Electron安全规范要求:为了安全性,Electron禁止渲染进程直接访问Electron API,必须通过**预加载脚本(preload.js)**使用contextBridge将需要的API暴露给渲染进程。

解决步骤

1. 预加载脚本中暴露IPC相关API

修改preload.js,通过contextBridge把需要的IPC方法暴露给渲染进程:

import { contextBridge, ipcRenderer } from "electron";

// ... 保留原有代码 ...

// 新增:暴露IPC API给渲染进程
contextBridge.exposeInMainWorld("electronAPI", {
  // 发送command指令
  sendCommand: (payload) => ipcRenderer.send("command", payload),
  // 监听state更新事件,返回清理函数
  onStateUpdate: (callback) => {
    const handler = (event, key, value) => callback(key, value);
    ipcRenderer.on("state_update", handler);
    return () => ipcRenderer.removeListener("state_update", handler);
  }
});

contextBridge.exposeInMainWorld("stateAPI", {
  state: state,
  updater: updater,
});

2. 修改React组件,使用暴露的API

移除React组件中对ipcRenderer的直接导入,改用window上的暴露API:

import React, { useEffect, useState } from "react";

// 改用暴露的electronAPI发送指令
const command = (payload) => {
  window.electronAPI.sendCommand(payload);
};

const Home = () => {
  const [state, setState] = useState(window.stateAPI.state);

  useEffect(() => {
    // 使用暴露的onStateUpdate监听状态变化
    const cleanup = window.electronAPI.onStateUpdate((key, value) => {
      setState(prev => ({ ...prev, [key]: value }));
    });

    // 组件卸载时清理监听
    return cleanup;
  }, []);

  return (
    <div>
      {/* 注意:如果state.config/store是对象,需要转成字符串显示 */}
      <h1>{JSON.stringify(state.config)}</h1>
      <h1>{JSON.stringify(state.store)}</h1>
      <button onClick={() => command("Test")}>Click me</button>
    </div>
  );
};

export default Home;

3. 主进程补充state转发逻辑

在main.js中添加ipcMain监听,把预加载脚本发来的updater事件转发给渲染进程:

const { app, BrowserWindow, ipcMain } = require("electron"); // 新增ipcMain导入
const path = require("path");

// ... 保留原有代码 ...

// 新增:监听预加载脚本的updater事件,转发给所有渲染进程
ipcMain.on("updater", (event, data) => {
  BrowserWindow.getAllWindows().forEach(window => {
    window.webContents.send("state_update", data.key, data.value);
  });
});

4. 修复webSecurity设置(可选)

你之前设置了webSecurity: false,其实Socket.IO不需要关闭这个选项。只要你的WebSocket服务配置了正确的CORS(允许Electron应用的源访问),就可以去掉这个设置,提升应用安全性:

// main.js中修改BrowserWindow配置
webPreferences: {
  preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
  // 移除webSecurity: false
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 19:48:09