Electron 应用有两种进程:

第 6 课:Electron 主进程


主进程是什么?

Electron 应用有两种进程: - 渲染进程:跑 React 界面的(上一课讲的) - 主进程:跑 Node.js 的,负责系统级操作

主进程就像一个"管家",负责: 1. 创建窗口 2. 接收前端消息,调用 SDK 3. 管理文件(保存上传、检测输出) 4. 处理下载请求

代码在 src/main/main.ts,大约 364 行。

创建应用窗口

code
const createWindow = () => {
  mainWindow = new BrowserWindow({
    width: 1024,
    height: 728,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      // ↑ 预加载脚本,定义前端能用的 API
    },
  });

  // 加载 React 界面
  mainWindow.loadURL(resolveHtmlPath('index.html'));
};

preload.ts 是安全桥梁——它定义了前端能调用的 IPC 方法,避免前端直接访问 Node.js 的危险 API。

code
// preload.ts —— 暴露给前端的安全接口
contextBridge.exposeInMainWorld('electron', {
  ipcRenderer: {
    sendMessage(channel: string, ...args: unknown[]) {
      ipcRenderer.send(channel, ...args);
    },
    on(channel: string, func: (...args: unknown[]) => void) {
      ipcRenderer.on(channel, (_event, ...args) => func(...args));
      // 返回清理函数
      return () => ipcRenderer.removeListener(channel, func);
    },
    invoke(channel: string, ...args: unknown[]) {
      return ipcRenderer.invoke(channel, ...args);
    },
  },
});

处理用户上传的文件

当用户拖一个 Excel 文件到聊天框,文件是怎么到达 AI 的?

code
ipcMain.on('claude-code:query', async (event, { content, files }) => {

  // 确保 problems 目录存在
  const problemsDir = path.join(process.cwd(), 'agent', 'problems');
  if (!fs.existsSync(problemsDir)) {
    fs.mkdirSync(problemsDir, { recursive: true });
  }

  let prompt = content;

  // 如果有上传文件
  if (files && files.length > 0) {
    for (const file of files) {
      // 生成唯一文件名:原名_时间戳_随机数.扩展名
      const timestamp = Date.now();
      const random = Math.random().toString(36).substring(2, 8);
      const ext = path.extname(file.name);
      const baseName = path.basename(file.name, ext);
      const uniqueName = `${baseName}_${timestamp}_${random}${ext}`;

      // 保存到 agent/problems/ 目录
      const filePath = path.join(problemsDir, uniqueName);
      fs.writeFileSync(filePath, Buffer.from(file.data));

      // 把文件路径加到提示词里
      prompt += `\n\nUploaded file: ${uniqueName}`;
      prompt += `\n(Saved to: ./problems/${uniqueName})`;
    }
  }

  // 然后调用 SDK...
});

文件名加了时间戳和随机数,这样多次上传同名文件不会冲突:

code
用户上传:Sales.xlsx

保存为:Sales_1704747600000_a3b5c2.xlsx
            │               │
            原名             时间戳 + 随机数

AI 能看到文件路径,就可以用 Read 工具去读取它。

输出文件检测

AI 工作完成后,主进程需要知道"AI 生成了什么新文件":

code
// 开始前:记录已有的文件
const outputDir = path.join(process.cwd(), 'agent');
const initialFiles = new Set(
  fs.readdirSync(outputDir)
    .filter(f => f.endsWith('.xlsx') || f.endsWith('.csv'))
);

// ... AI 工作 ...

// 结束后:找出新增的文件
const finalFiles = fs.readdirSync(outputDir)
  .filter(f => f.endsWith('.xlsx') || f.endsWith('.csv'));

const newFiles = finalFiles.filter(f => !initialFiles.has(f));

// 通知前端
if (newFiles.length > 0) {
  const outputFiles = newFiles.map(f => ({
    name: f,
    path: path.join(outputDir, f),
    size: fs.statSync(path.join(outputDir, f)).size,
  }));
  event.reply('claude-code:output-files', outputFiles);
}

逻辑很简单:开始前拍个"快照",结束后对比,多出来的就是新生成的。

文件下载处理

code
// 用户点击"下载"按钮时
ipcMain.handle('download-file', async (_event, filePath: string) => {
  // 弹出系统"另存为"对话框
  const { canceled, filePath: savePath } = await dialog.showSaveDialog({
    defaultPath: path.basename(filePath),
    filters: [
      { name: 'Excel', extensions: ['xlsx'] },
      { name: 'CSV', extensions: ['csv'] },
      { name: 'All Files', extensions: ['*'] },
    ],
  });

  if (!canceled && savePath) {
    // 复制文件到用户选择的位置
    fs.copyFileSync(filePath, savePath);
    return { success: true, path: savePath };
  }
  return { success: false };
});

还有一个"打开输出目录"的功能:

bash
ipcMain.handle('open-output-directory', async () => {
  const outputDir = path.join(process.cwd(), 'agent');
  // 在文件管理器中打开这个目录
  shell.openPath(outputDir);
});

错误处理

code
try {
  const queryIterator = query({ prompt, options: { ... } });

  for await (const message of queryIterator) {
    messages.push(message);
    event.reply('claude-code:response', message);
  }

} catch (error) {
  console.error('Claude Code SDK error:', error);

  // 通知前端出错了
  event.reply(
    'claude-code:error',
    error instanceof Error ? error.message : 'Unknown error'
  );
}

主进程的完整职责总结

graph TD subgraph Main["Electron 主进程"] W["1. 窗口管理<br/>createWindow() → 创建应用窗口"] M["2. 消息路由<br/>前端 → IPC → 主进程 → SDK<br/>SDK → 消息流 → 主进程 → IPC → 前端"] F["3. 文件管理<br/>上传文件 → 存到 agent/problems/<br/>AI 产出 → 检测新 .xlsx/.csv<br/>用户下载 → 弹出「另存为」"] E["4. 错误处理<br/>SDK 报错 → IPC → 前端显示错误"] T["5. 任务控制<br/>AbortController → 取消正在运行的任务"] end

本课小结

  • 主进程是 Electron 的"管家",负责窗口、IPC、文件、SDK 调用
  • 上传文件通过唯一命名保存到 agent/problems/
  • 输出文件通过前后快照对比来检测
  • 下载通过系统"另存为"对话框实现
  • 所有 SDK 错误都会通过 IPC 传给前端显示

课后练习

  1. 在 main.ts 里找到 createWindow 函数,把窗口大小改成 1280x800,看看效果
  2. 在文件检测逻辑里加入 .pdf 文件的检测
  3. 想一想:如果 AI 生成了一个很大的文件(100MB),当前的下载机制会不会有问题?

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录