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

Angular+Electron环境中node fs模块无法使用的问题求助

Angular + Electron 使用 Node fs 模块报错问题

报错信息

"The package "fs" wasn't found on the file system but is built into node. Are you trying to bundle for node? You can use "platform: 'node'" to do that, which will remove this error."

问题场景

在基于Angular 18和Electron 30的应用中,尝试用Node的fs模块读取本地JSON文件时出现上述错误。应用在浏览器中运行正常,但Electron环境下报错。

相关配置与代码

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "password-manager": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "outputPath": "dist/password-manager",
            "index": "src/index.html",
            "browser": "src/main.ts",
            "polyfills": [
              "zone.js"
            ],
            "tsConfig": "tsconfig.app.json",
            "assets": [
              {
                "glob": "**/*",
                "input": "public"
              }
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": [],
            "server": "src/main.server.ts",
            "prerender": true,
            "ssr": {
              "entry": "server.ts"
            }
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kB",
                  "maximumError": "1MB"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kB",
                  "maximumError": "4kB"
                }
              ],
              "outputHashing": "all"
            },
            "development": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "buildTarget": "password-manager:build:production"
            },
            "development": {
              "buildTarget": "password-manager:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n"
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing"
            ],
            "tsConfig": "tsconfig.spec.json",
            "assets": [
              {
                "glob": "**/*",
                "input": "public"
              }
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          }
        }
      }
    }
  }
}

package.json

{
  "name": "password-manager",
  "version": "0.0.0",
  "main": "main.js",
  "scripts": {
    "ng": "ng",
    "start": "ng build --base-href ./ && set NODE_ENV=development && electron .",
    "serve": "ng serve --live-reload && set NODE_ENV=development && electron .",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "serve:ssr:password-manager": "node dist/password-manager/server/server.mjs"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^18.0.0",
    "@angular/common": "^18.0.0",
    "@angular/compiler": "^18.0.0",
    "@angular/core": "^18.0.0",
    "@angular/forms": "^18.0.0",
    "@angular/platform-browser": "^18.0.0",
    "@angular/platform-browser-dynamic": "^18.0.0",
    "@angular/platform-server": "^18.0.0",
    "@angular/router": "^18.0.0",
    "@angular/ssr": "^18.0.3",
    "dotenv": "^16.4.5",
    "esbuild": "^0.21.5",
    "express": "^4.18.2",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.14.3"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^18.0.3",
    "@angular/cli": "^18.0.3",
    "@angular/compiler-cli": "^18.0.0",
    "@types/express": "^4.17.17",
    "@types/jasmine": "~5.1.0",
    "@types/node": "^18.18.0",
    "electron": "^30.1.0",
    "fs-extended": "^0.2.1",
    "jasmine-core": "~5.1.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.2.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.1.0",
    "typescript": "~5.4.2"
  }
}

原数据处理代码

import { WebsiteAccount } from "./WebsiteAccount";
import * as fs from 'fs';
const Accounts = '../password-manager/src/data/accounts.json';

export class Data {

  accounts: WebsiteAccount[];

  constructor() {
    this.accounts = this.read();
    this.write();
  }
  
  private read() {
    var accounts: WebsiteAccount[] = [];


      var jsonString = fs.readFileSync(Accounts, 'utf-8');
      var data = JSON.parse(jsonString);
      var numAccounts = Object.keys(data).length;
      
      for (var i = 0; i < numAccounts; i++) {
          var newWebsite: WebsiteAccount = new WebsiteAccount(data[i].title, data[i].link, data[i].username, data[i].password, data[i].email, data[i].countryCode, data[i].areaCode, data[i].prefixCode, data[i].lineCode);
          accounts.push(newWebsite);
        }

      return accounts;
    
  }

  write() {
    const data = Object.assign({}, this.accounts);
    var dataString = JSON.stringify(data);
    fs.writeFileSync(Accounts, dataString);
  }

  
}

已尝试的无效方案

  • 使用fs-extended替代原生fs
  • 设置Angular为浏览器构建模式
  • 手动为esbuild配置参数

解决方案

核心原因

Angular默认以浏览器环境打包,不会识别Node.js内置模块;Electron中渲染进程默认隔离Node环境,直接在Angular代码中调用fs会导致打包报错或运行时无法访问。

步骤1:配置Electron主进程(main.js)

采用Electron最佳实践,通过IPC通信让主进程处理文件操作,避免直接在渲染进程暴露Node API:

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

// 定义JSON文件的绝对路径(避免相对路径混乱)
const accountsFilePath = path.join(app.getAppPath(), 'src/data/accounts.json');

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      contextIsolation: true, // 保持上下文隔离,提升安全性
      preload: path.join(__dirname, 'preload.js') // 加载预加载脚本
    }
  });

  // 加载Angular构建后的页面
  mainWindow.loadFile(path.join(__dirname, 'dist/password-manager/index.html'));
}

// IPC监听:处理文件读取请求
ipcMain.handle('read-accounts', async () => {
  try {
    if (!fs.existsSync(accountsFilePath)) {
      // 文件不存在时初始化空数组
      fs.writeFileSync(accountsFilePath, JSON.stringify([]));
      return [];
    }
    const jsonContent = fs.readFileSync(accountsFilePath, 'utf-8');
    return JSON.parse(jsonContent);
  } catch (err) {
    console.error('读取文件失败:', err);
    return [];
  }
});

// IPC监听:处理文件写入请求
ipcMain.handle('write-accounts', async (_, accounts) => {
  try {
    const jsonString = JSON.stringify(accounts, null, 2);
    fs.writeFileSync(accountsFilePath, jsonString);
    return true;
  } catch (err) {
    console.error('写入文件失败:', err);
    return false;
  }
});

app.whenReady().then(createWindow);

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

步骤2:编写预加载脚本(preload.js)

通过contextBridge向渲染进程暴露安全的文件操作API:

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronFileAPI', {
  readAccounts: () => ipcRenderer.invoke('read-accounts'),
  writeAccounts: (accounts) => ipcRenderer.invoke('write-accounts', accounts)
});

步骤3:修改Angular的Data类

通过暴露的API与主进程通信,不再直接调用fs:

import { WebsiteAccount } from "./WebsiteAccount";

// 声明全局对象类型,避免TypeScript报错
declare global {
  interface Window {
    electronFileAPI: {
      readAccounts: () => Promise<any[]>;
      writeAccounts: (accounts: WebsiteAccount[]) => Promise<boolean>;
    };
  }
}

export class Data {
  accounts: WebsiteAccount[] = [];

  constructor() {
    this.init();
  }

  private async init() {
    // 从主进程读取数据
    const rawData = await window.electronFileAPI.readAccounts();
    this.accounts = rawData.map(item => new WebsiteAccount(
      item.title, item.link, item.username, item.password,
      item.email, item.countryCode, item.areaCode,
      item.prefixCode, item.lineCode
    ));
    // 初始化后写入(可选)
    await this.write();
  }

  async write() {
    // 向主进程发送写入请求
    await window.electronFileAPI.writeAccounts(this.accounts);
  }
}

步骤4:调整Angular打包配置

在angular.json的build.options中添加外部依赖声明,告诉Angular不打包Node内置模块:

"build": {
  "builder": "@angular-devkit/build-angular:application",
  "options": {
    // 其他现有配置...
    "externalDependencies": ["fs", "path"]
  }
}

同时在tsconfig.app.json的compilerOptions中添加Node类型支持:

"compilerOptions": {
  // 其他现有配置...
  "types": ["node"],
  "allowSyntheticDefaultImports": true
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 13:20:53