electron中, 子进程的加载全解

stdio 选项设置为 inheritpipe 主要影响的是子进程的输入和输出行为:

  1. inherit: 子进程将继承父进程的标准输入、输出和错误流。这意味着子进程的输出会直接显示在父进程的终端中。这种方式不会对子进程的执行产生其他影响,只是改变了输出的显示方式。

  2. pipe: 子进程的标准输入、输出和错误流会被重定向到父进程中。你可以通过监听这些流来捕获子进程的输出。这种方式也不会对子进程的执行产生其他影响,但需要你在父进程中处理这些流。

  3. ignore: 忽略子进程的标准输入。

总结来说,这些设置主要影响的是子进程的输出行为,不会对子进程的执行逻辑产生其他影响。

下面我们来重点说下pipe

通过设置 stdio 选项为 pipe,你可以与子进程进行通信。具体来说,你可以通过监听子进程的 stdout 和 stderr 事件来接收子进程的输出,并且可以通过 stdin 向子进程发送输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { spawn } = require('child_process');

const sdkProcessParameter = ['-configPath=' + configDir, '-logPathAndName=' + logsDirPath];
const sdkProcess = spawn(key_tone_sdk_path, sdkProcessParameter, {
detached: false,
stdio: ['pipe', 'pipe', 'pipe'], // 将 stdio 设置为 'pipe'
});

// 监听子进程的 stdout
sdkProcess.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});

// 监听子进程的 stderr
sdkProcess.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});

// 监听子进程的关闭事件
sdkProcess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});

// 向子进程发送输入
sdkProcess.stdin.write('some input\n');
sdkProcess.stdin.end();

设置一个数组,三个元素都是 'pipe' 是为了分别重定向子进程的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)流。具体来说:

  • 第一个 'pipe':重定向子进程的标准输入(stdin),这样你可以通过父进程向子进程发送输入。
  • 第二个 'pipe':重定向子进程的标准输出(stdout),这样你可以在父进程中捕获子进程的输出。
  • 第三个 'pipe':重定向子进程的标准错误(stderr),这样你可以在父进程中捕获子进程的错误信息。

这样,你就可以在父进程中与子进程进行通信,并捕获子进程的输出和错误信息。

使用pipe代替inherit

使用 inherit 方式时,子进程的输出会直接显示在 Electron 主进程的终端中,这样确实会混合在一起,难以区分。

如果你需要区分子进程和主进程的输出,可以考虑以下几种方法:

  1. 添加前缀:在子进程的输出前添加一个前缀,以便区分。例如,可以在子进程的输出中添加 [Child Process] 前缀。(但这样需要改动子进程自身的日志格式, 如果调用的是第三方的子进程模块, 或是子进程项目是可以单独运行等情况下, 这种方式难免有些南辕北辙了, 而且工作量太大, 因此不建议。)

  2. 使用 pipe 并手动处理输出:将 stdio 设置为 pipe,然后在主进程中手动处理子进程的输出,并添加前缀或其他标识。

以下是使用 pipe 并手动处理输出的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { spawn } = require('child_process');

const sdkProcessParameter = ['-configPath=' + configDir, '-logPathAndName=' + logsDirPath];
const sdkProcess = spawn(key_tone_sdk_path, sdkProcessParameter, {
detached: false,
stdio: ['pipe', 'pipe', 'pipe'], // 将 stdio 设置为 'pipe'
});

// 监听子进程的 stdout
sdkProcess.stdout.on('data', (data) => {
console.log(`[Child Process] stdout: ${data}`);
});

// 监听子进程的 stderr
sdkProcess.stderr.on('data', (data) => {
console.error(`[Child Process] stderr: ${data}`);
});

// 监听子进程的关闭事件
sdkProcess.on('close', (code) => {
console.log(`[Child Process] exited with code ${code}`);
});

通过这种方式,你可以在主进程中捕获子进程的输出,并添加前缀以便区分。

electron中, 如何调试主进程

由于electron的主进程实际上是一个nodejs程序, 因此我们可以借助浏览器的开发者工具或是vscode的运行和调试模块来调试electron的主进程。

如何通过 浏览器的开发者工具 调试electron主进程

这里是electron官网关于此部分的描述, 调试主进程

但是, 如果在quasar中, 是无法使用此参数的, quasar中关于此调试的配置, 被集成在了quasar.config.js这个配置文件中。

1
2
3
4
5
6
7
8
9
10
11
// quasar.config.js
module.exports = function (ctx) {
return {
// ...
electron: {
// ...
inspectPort: 5858, // 添加这个配置(实际上此字段在quasar中的默认值就是5858)
// ...
}
}
}

然后, 就和electron文档中的一样, 安装原有的后续步骤进行即可。以下是后续步骤:

确保调试端口配置正确:

在 Chrome 的 chrome://inspect 页面:

  1. 点击 “Configure…”
  2. 添加以下地址:
    • localhost:5858
    • 127.0.0.1:5858
  3. 在 “Remote Target” 下可以看到你的 Electron 主进程
  4. 点击 “inspect” 打开调试工具(此步骤需要在更改并保存代码后, 关闭旧调试工具窗口并重新执行)

关于主进程调试过程中如何热重载的问题

目前, 基于vite-electron部分似乎已经默认的启动了热重载, 参考

但在对主进程调试用的 浏览器的开发者工具 中却无法热重载, 每次都需要重新的点击在 “Remote Target” 下的 Electron 主进程的 “inspect” 来打开调试工具, 以手动更新。(目前仍未找到如何实现此部分的热重载的方案)

使用 Chrome 的 chrome://inspect 调试 Electron 主进程时,每次代码更改后都需要重新点击 “inspect” 是一个常见的限制。

这是因为:

  • 当主进程重启时,调试连接会断开
  • Chrome DevTools Protocol 没有提供自动重连的机制
  • electron-reloader 只负责重启主进程,不会处理调试连接
    目前没有完美的解决方案来实现 Chrome DevTools 的自动重连。这也是为什么大多数开发者推荐使用 VS Code 进行主进程调试的原因,因为 VS Code 能够自动处理重连。

如何通过 vscode的运行调试模块 调试electron主进程

未作调研, 可以参考前面部分中提到的electron官网的调试主进程的部分做了解, 或是通过ai工具来帮忙适配。

electron中, 如何在主进程中, 调用渲染进程中的方法

可以通过以下几种方式在主进程中调用渲染进程的函数:

  1. 使用 webContents.executeJavaScript() - 最直接的方式(不推荐):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // typescript:frontend/src-electron/electron-main.ts
    // ... existing code ...

    // 在主进程中执行渲染进程的函数
    mainWindow?.webContents.executeJavaScript(`
    window.yourFunctionName('参数1', '参数2')
    `).then((result) => {
    console.log('函数执行结果:', result);
    }).catch((err) => {
    console.error('执行失败:', err);
    });
  2. 使用 IPC 通信 - 更规范的方式(目前出于安全限制, 在electron的渲染进程中, ipcRenderer已被禁止使用了):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // typescript:frontend/src-electron/electron-main.ts
    // 主进程中发送事件
    mainWindow?.webContents.send('execute-renderer-function', arg1, arg2);

    // 如果需要获取返回值,可以使用 invoke
    ipcMain.handle('get-renderer-result', async (event, ...args) => {
    // 发送请求到渲染进程
    mainWindow?.webContents.send('execute-renderer-function', ...args);

    // 等待渲染进程的响应
    return new Promise((resolve) => {
    ipcMain.once('renderer-result', (event, result) => {
    resolve(result);
    });
    });
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // typescript:frontend/src/your-component.ts
    // 渲染进程中监听事件
    import { ipcRenderer } from 'electron';

    ipcRenderer.on('execute-renderer-function', (event, ...args) => {
    // 执行你的函数
    const result = yourFunction(...args);

    // 如果需要返回结果
    ipcRenderer.send('renderer-result', result);
    });
  3. 使用预加载脚本作为桥接 - 最推荐的方式(也是目前最可靠和唯一的方式):

    以下是一个完整的示例,展示如何在主进程中触发渲染进程的状态更新:

    1. 首先在预加载脚本中设置通信桥接:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // typescript:frontend/src-electron/preload.ts
      import { contextBridge, ipcRenderer } from 'electron';

      contextBridge.exposeInMainWorld('electronAPI', {
      // 监听主进程的更新请求
      onUpdateState: (callback: (data: any) => void) => {
      const listener = (_event: any, data: any) => callback(data);
      ipcRenderer.on('update-state', listener);
      return () => ipcRenderer.removeListener('update-state', listener);
      },

      // 向主进程发送更新结果
      sendUpdateResult: (result: any) => {
      ipcRenderer.send('update-result', result);
      }
      });
    2. 在渲染进程(Vue组件)中处理更新:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      // typescript:frontend/src/components/YourComponent.vue
      <template>
      <div>
      <p>状态: {{ state.status }}</p>
      <p>端口: {{ state.port }}</p>
      <p>消息: {{ state.message }}</p>
      </div>
      </template>

      <script setup lang="ts">
      import { reactive, onMounted, onUnmounted } from 'vue';

      // 定义响应式状态
      const state = reactive({
      status: '',
      port: 0,
      message: ''
      });

      // 处理来自主进程的更新
      function handleStateUpdate(data: any) {
      // 更新状态
      if (data.status) state.status = data.status;
      if (data.port) state.port = data.port;
      if (data.message) state.message = data.message;

      // 通知主进程更新完成
      window.electronAPI.sendUpdateResult({
      success: true,
      updatedFields: Object.keys(data)
      });
      }

      // 设置监听器
      let cleanup: (() => void) | null = null;

      onMounted(() => {
      // 监听主进程的更新请求
      cleanup = window.electronAPI.onUpdateState(handleStateUpdate);
      });

      onUnmounted(() => {
      // 清理监听器
      cleanup?.();
      });
      </script>
    3. 在主进程中触发更新:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      // typescript:frontend/src-electron/electron-main.ts
      import { app, BrowserWindow, ipcMain } from 'electron';

      // ... existing code ...

      // 更新渲染进程状态的函数
      function updateRendererState(data: any) {
      return new Promise((resolve, reject) => {
      // 发送更新请求到渲染进程
      mainWindow?.webContents.send('update-state', data);

      // 设置超时
      const timeout = setTimeout(() => {
      cleanup();
      reject(new Error('更新超时'));
      }, 5000);

      // 等待渲染进程的响应
      const cleanup = () => {
      clearTimeout(timeout);
      ipcMain.removeListener('update-result', handleResult);
      };

      const handleResult = (_event: any, result: any) => {
      cleanup();
      resolve(result);
      };

      ipcMain.once('update-result', handleResult);
      });
      }

      // 使用示例:
      // 1. 单个字段更新
      async function updateStatus() {
      try {
      const result = await updateRendererState({
      status: '正在运行'
      });
      console.log('状态更新成功:', result);
      } catch (error) {
      console.error('状态更新失败:', error);
      }
      }

      // 2. 多个字段同时更新
      async function updateMultipleFields() {
      try {
      const result = await updateRendererState({
      port: backendPort,
      message: '服务已启动',
      status: 'active'
      });
      console.log('多字段更新成功:', result);
      } catch (error) {
      console.error('多字段更新失败:', error);
      }
      }

      // 3. 定时更新示例
      setInterval(async () => {
      try {
      await updateRendererState({
      port: backendPort,
      status: '运行中'
      });
      } catch (error) {
      console.error('定时更新失败:', error);
      }
      }, 1000);

      // 4. 条件更新示例
      let lastPort = backendPort;
      setInterval(async () => {
      if (backendPort !== lastPort) {
      try {
      await updateRendererState({
      port: backendPort,
      message: '端口已更新'
      });
      lastPort = backendPort;
      } catch (error) {
      console.error('端口更新失败:', error);
      }
      }
      }, 1000);
    4. 添加类型定义:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // typescript:frontend/src/types/electron.d.ts
      interface UpdateData {
      status?: string;
      port?: number;
      message?: string;
      }

      interface UpdateResult {
      success: boolean;
      updatedFields: string[];
      }

      declare global {
      interface Window {
      electronAPI: {
      onUpdateState: (callback: (data: UpdateData) => void) => () => void;
      sendUpdateResult: (result: UpdateResult) => void;
      }
      }
      }
    5. 如果使用 Pinia 状态管理:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      // typescript:frontend/src/stores/app.ts
      import { defineStore } from 'pinia';

      export const useAppStore = defineStore('app', {
      state: () => ({
      status: '',
      port: 0,
      message: ''
      }),

      actions: {
      updateState(data: UpdateData) {
      if (data.status) this.status = data.status;
      if (data.port) this.port = data.port;
      if (data.message) this.message = data.message;
      },

      setupElectronListeners() {
      return window.electronAPI.onUpdateState((data) => {
      this.updateState(data);
      window.electronAPI.sendUpdateResult({
      success: true,
      updatedFields: Object.keys(data)
      });
      });
      }
      }
      });

    这种实现方式的优点:

    1. 类型安全:完整的类型定义
    2. 可靠性:使用 IPC 通信而不是 executeJavaScript
    3. 双向通信:可以确认更新是否成功
    4. 灵活性:可以更新单个或多个字段
    5. 错误处理:包含超时和错误处理机制
    6. 状态管理:可以与 Vue 的响应式系统或 Pinia 集成

    使用这种方式,你可以从主进程安全可靠地控制渲染进程的状态更新,同时保持代码的可维护性和类型安全性。

建议:

  1. 避免直接使用 executeJavaScript,因为(极为不推荐):

    • 可能存在安全风险
    • 代码难以维护
    • 不利于类型检查
  2. 如果只是简单的数据传递,使用 IPC 通信就足够了(但目前已无法使用)。

  3. 优先使用预加载脚本的方式,因为它(推荐的方式):

    • 提供了更好的类型安全
    • 维护了上下文隔离
    • 更容易管理和维护
    • 提供了更好的安全性
  4. 记住要处理错误情况和异步操作。

  5. 确保在调用渲染进程函数时,窗口已经完全加载:

    1
    2
    3
    4
    // typescript:frontend/src-electron/electron-main.ts
    mainWindow?.webContents.on('did-finish-load', () => {
    // 这里是安全的时机调用渲染进程函数
    });

注意:无论使用哪种方式,都要注意安全性,避免直接执行不可信的代码。同时,建议将这些通信逻辑封装成可复用的模块,以便更好地管理和维护。

electron中, 如果在渲染进程中, 调用主进程中的方法, 或是其它nodejs进程中的方法

在渲染进程中调用主进程或其他 Node.js 进程的函数主要有以下几种方式:

  1. 通过预加载脚本(Preload)和 IPC 通信 - 最推荐的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // typescript:frontend/src-electron/preload.ts
    import { contextBridge, ipcRenderer } from 'electron';

    // 在预加载脚本中暴露接口
    contextBridge.exposeInMainWorld('electronAPI', {
    // 异步函数调用
    callMainFunction: async (...args) => {
    return await ipcRenderer.invoke('main-function', ...args);
    },

    // 同步函数调用
    callMainFunctionSync: (...args) => {
    return ipcRenderer.sendSync('main-function-sync', ...args);
    },

    // 订阅主进程事件
    onMainEvent: (callback) => {
    ipcRenderer.on('main-event', (event, ...args) => callback(...args));
    }
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // typescript:frontend/src-electron/electron-main.ts
    import { ipcMain } from 'electron';

    // 在主进程中处理调用
    ipcMain.handle('main-function', async (event, ...args) => {
    // 异步函数实现
    return await yourMainFunction(...args);
    });

    // 处理同步调用
    ipcMain.on('main-function-sync', (event, ...args) => {
    // 同步函数实现
    event.returnValue = yourMainFunctionSync(...args);
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // typescript:frontend/src/components/YourComponent.vue
    // 在渲染进程中使用
    async function callMainProcess() {
    try {
    // 异步调用
    const result = await window.electronAPI.callMainFunction('arg1', 'arg2');
    console.log('异步调用结果:', result);

    // 同步调用
    const syncResult = window.electronAPI.callMainFunctionSync('arg1', 'arg2');
    console.log('同步调用结果:', syncResult);

    // 监听主进程事件
    window.electronAPI.onMainEvent((data) => {
    console.log('收到主进程事件:', data);
    });
    } catch (error) {
    console.error('调用失败:', error);
    }
    }
  2. 调用其他 Node.js 进程的函数(通过主进程作为中介):

    1
    2
    3
    4
    5
    6
    // typescript:frontend/src-electron/preload.ts
    contextBridge.exposeInMainWorld('nodeAPI', {
    callNodeProcess: async (...args) => {
    return await ipcRenderer.invoke('node-process', ...args);
    }
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // typescript:frontend/src-electron/electron-main.ts
    import { fork } from 'child_process';

    // 创建子进程
    const nodeProcess = fork('path/to/your/node/script.js');

    // 处理渲染进程的调用
    ipcMain.handle('node-process', async (event, ...args) => {
    return new Promise((resolve, reject) => {
    // 向子进程发送消息
    nodeProcess.send({ type: 'function-call', args });

    // 接收子进程的响应
    nodeProcess.once('message', (response) => {
    if (response.error) {
    reject(response.error);
    } else {
    resolve(response.result);
    }
    });
    });
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // typescript:path/to/your/node/script.js
    // 子进程中的处理
    process.on('message', async (message) => {
    if (message.type === 'function-call') {
    try {
    const result = await yourNodeFunction(...message.args);
    process.send({ result });
    } catch (error) {
    process.send({ error: error.message });
    }
    }
    });
  3. 使用 @electron/remote 模块(不推荐,但在某些场景可能有用):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // typescript:frontend/src-electron/electron-main.ts
    import { initialize } from '@electron/remote/main';

    // 初始化remote模块
    initialize();

    // 在创建窗口时启用remote
    mainWindow = new BrowserWindow({
    webPreferences: {
    nodeIntegration: true,
    contextIsolation: false,
    enableRemoteModule: true
    }
    });
    1
    2
    3
    4
    5
    // typescript:frontend/src/components/YourComponent.vue
    import { remote } from '@electron/remote';

    // 直接调用主进程模块
    const result = remote.app.getPath('userData');

建议和注意事项:

  1. 优先使用预加载脚本和 IPC 通信方式,因为:

    • 保持了上下文隔离
    • 提供了更好的安全性
    • 代码结构更清晰
    • 更容易维护和调试
  2. 对于性能敏感的操作:

    • 考虑使用同步 IPC 调用(但要注意可能阻塞UI)
    • 可以实现批处理机制
    • 考虑使用 Worker 进程处理计算密集型任务
  3. 错误处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // typescript:frontend/src/components/YourComponent.vue
    try {
    const result = await window.electronAPI.callMainFunction();
    } catch (error) {
    // 处理错误
    console.error('调用主进程函数失败:', error);
    // 可以显示用户友好的错误提示
    showErrorNotification(error);
    }
  4. 类型安全:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // typescript:frontend/src/types/electron.d.ts
    declare global {
    interface Window {
    electronAPI: {
    callMainFunction: (arg1: string, arg2: number) => Promise<string>;
    callMainFunctionSync: (arg1: string, arg2: number) => string;
    onMainEvent: (callback: (data: any) => void) => void;
    };
    }
    }
  5. 性能优化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // typescript:frontend/src-electron/electron-main.ts
    // 批处理示例
    let batchQueue: any[] = [];
    let batchTimeout: NodeJS.Timeout | null = null;

    ipcMain.handle('batch-process', async (event, item) => {
    batchQueue.push(item);

    if (!batchTimeout) {
    batchTimeout = setTimeout(async () => {
    const items = [...batchQueue];
    batchQueue = [];
    batchTimeout = null;

    // 批量处理
    const results = await processBatch(items);
    event.sender.send('batch-results', results);
    }, 100);
    }
    });

这些方法可以根据你的具体需求选择使用。通常建议从最简单的 IPC 通信开始,然后根据需要逐步添加更复杂的功能。

electron中, ipcRenderer都有哪些方法

ipcRenderer 的主要方法可以分为以下几类:

  1. 发送消息的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // typescript:frontend/src-electron/preload.ts
    import { ipcRenderer } from 'electron';

    // 异步发送(不等待响应)
    ipcRenderer.send('channel-name', ...args);

    // 同步发送(会阻塞渲染进程直到收到响应)
    const result = ipcRenderer.sendSync('channel-name', ...args);

    // 发送消息到特定窗口
    ipcRenderer.sendTo(windowId, 'channel-name', ...args);

    // 发送消息给主窗口
    ipcRenderer.sendToHost('channel-name', ...args);

    // 调用主进程方法并等待结果(Promise方式)
    const result = await ipcRenderer.invoke('channel-name', ...args);
  2. 监听消息的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // typescript:frontend/src-electron/preload.ts
    // 监听消息(可以多次触发)
    ipcRenderer.on('channel-name', (event, ...args) => {
    console.log('收到消息:', ...args);
    });

    // 监听消息(只触发一次)
    ipcRenderer.once('channel-name', (event, ...args) => {
    console.log('收到一次性消息:', ...args);
    });

    // 移除特定监听器
    ipcRenderer.removeListener('channel-name', listener);

    // 移除所有监听器
    ipcRenderer.removeAllListeners('channel-name');
  3. 实际应用示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // typescript:frontend/src-electron/preload.ts
    contextBridge.exposeInMainWorld('ipcAPI', {
    // 基础消息发送
    sendMessage: (channel: string, ...args: any[]) => {
    ipcRenderer.send(channel, ...args);
    },

    // 同步消息
    sendSyncMessage: (channel: string, ...args: any[]) => {
    return ipcRenderer.sendSync(channel, ...args);
    },

    // 异步调用(Promise)
    invokeMain: async (channel: string, ...args: any[]) => {
    return await ipcRenderer.invoke(channel, ...args);
    },

    // 监听主进程消息
    onMessage: (channel: string, callback: (...args: any[]) => void) => {
    const listener = (event: any, ...args: any[]) => callback(...args);
    ipcRenderer.on(channel, listener);

    // 返回清理函数
    return () => {
    ipcRenderer.removeListener(channel, listener);
    };
    },

    // 一次性监听
    onceMessage: (channel: string, callback: (...args: any[]) => void) => {
    ipcRenderer.once(channel, (event, ...args) => callback(...args));
    }
    });
  4. 常见使用场景:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // typescript:frontend/src/components/YourComponent.vue
    // 1. 简单的消息发送
    window.ipcAPI.sendMessage('update-something', { data: 'value' });

    // 2. 需要等待响应的调用
    const result = await window.ipcAPI.invokeMain('get-user-data');

    // 3. 监听主进程事件
    const cleanup = window.ipcAPI.onMessage('status-update', (status) => {
    console.log('状态更新:', status);
    });

    // 4. 同步调用(谨慎使用,可能阻塞UI)
    const immediate = window.ipcAPI.sendSyncMessage('get-config');

    // 5. 一次性事件监听
    window.ipcAPI.onceMessage('app-ready', () => {
    console.log('应用就绪');
    });
  5. 错误处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // typescript:frontend/src/components/YourComponent.vue
    try {
    const result = await window.ipcAPI.invokeMain('risky-operation');
    } catch (error) {
    console.error('操作失败:', error);
    }

    // 使用超时处理
    async function invokeWithTimeout(channel: string, timeout = 5000) {
    const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('操作超时')), timeout);
    });

    try {
    return await Promise.race([
    window.ipcAPI.invokeMain(channel),
    timeoutPromise
    ]);
    } catch (error) {
    console.error('调用失败:', error);
    throw error;
    }
    }
  6. 类型定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // typescript:frontend/src/types/electron.d.ts
    declare global {
    interface Window {
    ipcAPI: {
    sendMessage: (channel: string, ...args: any[]) => void;
    sendSyncMessage: (channel: string, ...args: any[]) => any;
    invokeMain: (channel: string, ...args: any[]) => Promise<any>;
    onMessage: (channel: string, callback: (...args: any[]) => void) => () => void;
    onceMessage: (channel: string, callback: (...args: any[]) => void) => void;
    };
    }
    }

注意事项:

  1. 安全性:

    • 始终通过 contextBridge 暴露 API
    • 避免直接暴露 ipcRenderer 对象
    • 限制可用的通道名称
  2. 性能:

    • 避免频繁使用 sendSync
    • 大数据传输考虑分批处理
    • 使用 invoke 代替 send + on 的模式
  3. 可维护性:

    • 统一管理通道名称
    • 做好错误处理
    • 及时清理不需要的监听器
  4. 调试:

    • 可以添加日志记录
    • 使用开发者工具监控 IPC 通信

electron中, ipcMain都有哪些方法

介绍了ipcRenderer后, 就不得不介绍下ipcMain了。

ipcMain 是主进程中用于处理 IPC 通信的模块,以下是其主要方法和使用方式:

当然, 从这些介绍中不难看出其使用时和ipcRenderer中某些方法的对应关系。当然, 我也会在下一小节中介绍这些对应关系。

  1. 基本监听方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // typescript:frontend/src-electron/electron-main.ts
    import { ipcMain } from 'electron';

    // 处理异步消息
    ipcMain.on('channel-name', (event, ...args) => {
    // 处理消息
    console.log('收到消息:', ...args);

    // 回复消息
    event.reply('reply-channel', '处理结果');
    // 或
    event.sender.send('reply-channel', '处理结果');
    });

    // 处理同步消息
    ipcMain.on('sync-channel', (event, ...args) => {
    // 处理消息
    console.log('收到同步消息:', ...args);

    // 必须设置 returnValue 来响应同步消息
    event.returnValue = '同步处理结果';
    });

    // 处理 invoke 调用
    ipcMain.handle('invoke-channel', async (event, ...args) => {
    // 可以返回 Promise
    return await someAsyncOperation(...args);
    });
  2. 高级监听方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // typescript:frontend/src-electron/electron-main.ts
    // 一次性监听
    ipcMain.once('one-time-channel', (event, ...args) => {
    console.log('这个监听器只会触发一次');
    });

    // 移除监听器
    const listener = (event, ...args) => {
    console.log('处理消息');
    };
    ipcMain.on('channel-name', listener);
    ipcMain.removeListener('channel-name', listener);

    // 移除所有监听器
    ipcMain.removeAllListeners('channel-name');

    // 移除特定 handle
    ipcMain.removeHandler('invoke-channel');
  3. 实际应用示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // typescript:frontend/src-electron/electron-main.ts
    // 配置相关操作
    ipcMain.handle('get-config', async (event, key) => {
    try {
    return await readConfigFile(key);
    } catch (error) {
    console.error('读取配置失败:', error);
    throw error; // 错误会传递给渲染进程
    }
    });

    // 文件操作
    ipcMain.handle('save-file', async (event, { path, content }) => {
    try {
    await fs.promises.writeFile(path, content);
    return { success: true };
    } catch (error) {
    console.error('保存文件失败:', error);
    throw error;
    }
    });

    // 窗口操作
    ipcMain.on('window-control', (event, action) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    switch (action) {
    case 'minimize':
    window?.minimize();
    break;
    case 'maximize':
    window?.isMaximized() ? window.unmaximize() : window.maximize();
    break;
    case 'close':
    window?.close();
    break;
    }
    });
  4. 处理大量数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // typescript:frontend/src-electron/electron-main.ts
    // 分批处理大数据
    ipcMain.handle('process-large-data', async (event, data) => {
    const BATCH_SIZE = 1000;
    const results = [];

    for (let i = 0; i < data.length; i += BATCH_SIZE) {
    const batch = data.slice(i, i + BATCH_SIZE);
    const batchResult = await processBatch(batch);
    results.push(...batchResult);

    // 发送进度更新
    event.sender.send('process-progress', {
    processed: i + batch.length,
    total: data.length
    });
    }

    return results;
    });
  5. 错误处理和安全检查:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // typescript:frontend/src-electron/electron-main.ts
    // 带验证的处理器
    ipcMain.handle('secure-operation', async (event, ...args) => {
    // 检查发送者
    if (!isValidSender(event.sender)) {
    throw new Error('未授权的请求');
    }

    try {
    // 验证参数
    validateArgs(args);

    // 执行操作
    const result = await performSecureOperation(...args);

    // 记录操作
    await logOperation(event.sender.id, 'secure-operation', args);

    return result;
    } catch (error) {
    console.error('安全操作失败:', error);
    throw error;
    }
    });
  6. 组织和管理 IPC 处理器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // typescript:frontend/src-electron/ipc-handlers/index.ts
    // 模块化 IPC 处理器
    import { configHandlers } from './config-handlers';
    import { fileHandlers } from './file-handlers';
    import { windowHandlers } from './window-handlers';

    export function setupIpcHandlers() {
    // 配置相关
    ipcMain.handle('get-config', configHandlers.getConfig);
    ipcMain.handle('save-config', configHandlers.saveConfig);

    // 文件相关
    ipcMain.handle('read-file', fileHandlers.readFile);
    ipcMain.handle('write-file', fileHandlers.writeFile);

    // 窗口相关
    ipcMain.on('window-control', windowHandlers.handleWindowControl);
    }
  7. 调试和日志:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // typescript:frontend/src-electron/electron-main.ts
    // 添加调试日志
    function createIpcLogger(channel: string) {
    return (event: Electron.IpcMainEvent, ...args: any[]) => {
    console.log(`[IPC] ${channel}:`, {
    windowId: event.sender.id,
    args: args,
    timestamp: new Date().toISOString()
    });
    };
    }

    // 使用日志装饰器
    ipcMain.on('important-channel', createIpcLogger('important-channel'));

注意事项:

  1. 安全性:

    • 始终验证输入数据
    • 限制可访问的系统资源
    • 注意跨域和权限问题
  2. 性能:

    • 避免在主进程中进行密集计算
    • 合理使用异步操作
    • 考虑使用 Worker 进程
  3. 可维护性:

    • 模块化处理器
    • 统一错误处理
    • 添加适当的日志
  4. 资源管理:

    • 及时清理不用的监听器
    • 正确处理窗口关闭事件
    • 管理好内存使用

electron中, ipcRenderer 和 ipcMain 在使用过程中必须保持的对应关系

是的,ipcRenderer 和 ipcMain 在使用过程中需要保持一定的对应关系。以下是主要的对应模式:

  1. 基本对应关系:

    1
    2
    3
    4
    5
    6
    7
    // 主进程 (electron-main.ts)
    ipcMain.on('channel-name', (event, ...args) => {
    // 处理来自渲染进程的消息
    });

    // 渲染进程 (通过preload.ts暴露给前端)
    ipcRenderer.send('channel-name', ...args);
  2. 同步通信对应:

    1
    2
    3
    4
    5
    6
    7
    8
    // 主进程
    ipcMain.on('sync-channel', (event, ...args) => {
    // 处理同步请求
    event.returnValue = '结果'; // 必须设置returnValue
    });

    // 渲染进程(TIPS: 在渲染进程中直接调用是不可取的, 需要通过preload.ts结合contextBridge来暴露给前端渲染进程使用。 下一小节会介绍完整的使用流程)
    const result = ipcRenderer.sendSync('sync-channel', ...args); // 比如可以在electron-preload.ts中的contextBridge.exposeInMainWorld回调暴露给前端(渲染进程)的方法中, 作为某个方法的返回值return给前端(渲染进程)。
  3. 异步请求-响应模式(推荐):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // typescript:frontend/src-electron/preload.ts
    // 预加载脚本中暴露接口
    contextBridge.exposeInMainWorld('electronAPI', {
    // Promise方式调用
    invokeMain: async (...args) => {
    return await ipcRenderer.invoke('async-channel', ...args);
    },

    // 回调方式发送
    sendToMain: (callback) => {
    ipcRenderer.send('message-channel', 'some-data');
    ipcRenderer.once('message-reply', (_event, result) => {
    callback(result);
    });
    }
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // typescript:frontend/src-electron/electron-main.ts
    // 主进程
    // 处理 invoke 调用
    ipcMain.handle('async-channel', async (event, ...args) => {
    const result = await someAsyncOperation(...args);
    return result;
    });

    // 处理普通消息
    ipcMain.on('message-channel', (event, ...args) => {
    // 处理消息
    event.reply('message-reply', '处理结果');
    // 或
    event.sender.send('message-reply', '处理结果');
    });
  4. 双向通信模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // typescript:frontend/src-electron/preload.ts
    contextBridge.exposeInMainWorld('electronAPI', {
    // 监听主进程消息
    onMainEvent: (callback) => {
    const listener = (_event, ...args) => callback(...args);
    ipcRenderer.on('main-event', listener);
    return () => ipcRenderer.removeListener('main-event', listener);
    },

    // 发送消息到主进程
    sendToMain: (data) => {
    ipcRenderer.send('renderer-event', data);
    }
    });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // typescript:frontend/src-electron/electron-main.ts
    // 主进程
    // 监听渲染进程消息
    ipcMain.on('renderer-event', (event, data) => {
    // 处理消息
    console.log('收到渲染进程消息:', data);

    // 可以发送回复
    event.reply('main-event', '已收到消息');
    });

    // 主动发送消息给渲染进程
    mainWindow?.webContents.send('main-event', '来自主进程的消息');

注意事项:

  1. 通道名称(channel)必须完全匹配:

    1
    2
    3
    4
    5
    6
    7
    // ❌ 错误示例:通道名不匹配
    ipcMain.on('channel-1', () => {});
    ipcRenderer.send('channel-2', data); // 消息永远不会被接收

    // ✅ 正确示例:通道名匹配
    ipcMain.on('channel-name', () => {});
    ipcRenderer.send('channel-name', data);
  2. 处理方式要对应:

    1
    2
    3
    4
    5
    6
    7
    // ❌ 错误示例:处理方式不匹配
    ipcMain.on('channel', () => {}); // on 用于处理 send
    ipcRenderer.invoke('channel'); // invoke 需要用 handle 处理

    // ✅ 正确示例:处理方式匹配
    ipcMain.handle('channel', () => {}); // handle 用于处理 invoke
    ipcRenderer.invoke('channel');
  3. 返回值处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ❌ 错误示例:同步消息没有设置返回值
    ipcMain.on('sync-msg', (event) => {
    // 没有设置 returnValue,渲染进程会一直等待
    });

    // ✅ 正确示例:同步消息设置返回值
    ipcMain.on('sync-msg', (event) => {
    event.returnValue = '结果'; // 同步消息必须设置 returnValue
    });
  4. 清理监听器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // ✅ 推荐做法:返回清理函数
    function setupIPC() {
    const cleanup = window.electronAPI.onMainEvent((data) => {
    console.log(data);
    });

    // 组件卸载时清理
    onUnmounted(() => {
    cleanup();
    });
    }

electron中, 如何完整前正确的使用ipcRenderer和ipcMain

不能直接在渲染进程中使用 ipcRenderer,这是因为 Electron 的安全限制。正确的做法是通过预加载脚本(preload)和 contextBridge 来暴露必要的 API。

以下是完整的设置流程:

  1. 首先在主进程中配置 webPreferences

    1
    2
    3
    4
    5
    6
    7
    8
    // typescript:frontend/src-electron/electron-main.ts
    const mainWindow = new BrowserWindow({
    webPreferences: {
    contextIsolation: true, // 开启上下文隔离
    nodeIntegration: false, // 关闭 Node 集成
    preload: path.join(__dirname, 'preload.js') // 指定预加载脚本
    }
    });
  2. 在预加载脚本中暴露 API:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // typescript:frontend/src-electron/preload.ts
    import { contextBridge, ipcRenderer } from 'electron';

    // 通过 contextBridge 暴露安全的 API
    contextBridge.exposeInMainWorld('electronAPI', {
    // 发送消息
    send: (channel: string, ...args: any[]) => {
    // 可以添加通道白名单检查
    const validChannels = ['save-data', 'load-data', 'update-config'];
    if (validChannels.includes(channel)) {
    ipcRenderer.send(channel, ...args);
    }
    },

    // 调用主进程方法
    invoke: async (channel: string, ...args: any[]) => {
    const validChannels = ['get-user-data', 'process-file'];
    if (validChannels.includes(channel)) {
    return await ipcRenderer.invoke(channel, ...args);
    }
    throw new Error(`Invalid channel: ${channel}`);
    },

    // 监听主进程消息
    on: (channel: string, callback: (...args: any[]) => void) => {
    const validChannels = ['update-status', 'new-message'];
    if (validChannels.includes(channel)) {
    const subscription = (_event: any, ...args: any[]) => callback(...args);
    ipcRenderer.on(channel, subscription);

    // 返回清理函数
    return () => {
    ipcRenderer.removeListener(channel, subscription);
    };
    }
    return () => {}; // 返回空清理函数
    }
    });
  3. 添加类型定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // typescript:frontend/src/types/electron.d.ts
    declare global {
    interface Window {
    electronAPI: {
    send: (channel: string, ...args: any[]) => void;
    invoke: (channel: string, ...args: any[]) => Promise<any>;
    on: (channel: string, callback: (...args: any[]) => void) => () => void;
    }
    }
    }
  4. 在渲染进程(前端代码)中使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // typescript:frontend/src/components/YourComponent.vue
    <script setup lang="ts">
    import { onUnmounted } from 'vue';

    // 发送消息
    function handleSave() {
    window.electronAPI.send('save-data', { key: 'value' });
    }

    // 异步调用
    async function loadData() {
    try {
    const data = await window.electronAPI.invoke('get-user-data');
    console.log('加载的数据:', data);
    } catch (error) {
    console.error('加载失败:', error);
    }
    }

    // 监听主进程消息
    const cleanup = window.electronAPI.on('update-status', (status) => {
    console.log('状态更新:', status);
    });

    // 组件卸载时清理监听器
    onUnmounted(() => {
    cleanup();
    });
    </script>
  5. 使用 Vuex/Pinia 状态管理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // typescript:frontend/src/stores/electron.ts
    import { defineStore } from 'pinia';

    export const useElectronStore = defineStore('electron', {
    state: () => ({
    status: '',
    messages: [] as string[]
    }),

    actions: {
    async initialize() {
    // 设置监听器
    window.electronAPI.on('update-status', (status) => {
    this.status = status;
    });

    window.electronAPI.on('new-message', (message) => {
    this.messages.push(message);
    });
    },

    async sendMessage(message: string) {
    await window.electronAPI.invoke('send-message', message);
    }
    }
    });
  6. 错误处理和类型安全:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // typescript:frontend/src/utils/electron.ts
    // 封装 electron API 调用
    export async function invokeMain<T>(
    channel: string,
    ...args: any[]
    ): Promise<T> {
    try {
    return await window.electronAPI.invoke(channel, ...args);
    } catch (error) {
    console.error(`调用 ${channel} 失败:`, error);
    throw error;
    }
    }

    // 使用
    const userData = await invokeMain<UserData>('get-user-data');

注意事项:

  1. 安全性:

    • 始终使用 contextIsolation: true
    • 在预加载脚本中验证通道名称
    • 限制暴露的 API 范围
  2. 类型安全:

    • 定义完整的类型声明
    • 使用泛型约束返回值类型
    • 处理所有可能的错误情况
  3. 性能:

    • 避免频繁的 IPC 通信
    • 合理使用缓存
    • 批量处理数据
  4. 可维护性:

    • 集中管理 IPC 通道名称
    • 使用状态管理统一处理数据
    • 做好错误处理和日志记录

这样的设置可以确保:

  • 安全性:通过上下文隔离保护主进程
  • 类型安全:提供完整的类型支持
  • 可维护性:集中管理 IPC 通信
  • 性能:避免不必要的通信开销

electron启动过程中, 相关的生命周期的基本常识

渲染进程是随着 BrowserWindow 的创建和加载而启动的。

让我们看看具体的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// typescript:frontend/src-electron/electron-main.ts
function createWindow() {
// 1. 创建窗口实例
mainWindow = new BrowserWindow({
// ... 窗口配置
webPreferences: {
preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
}
});

// 2. 加载页面 - 这一步会启动渲染进程
mainWindow.loadURL(process.env.APP_URL);
// 或
mainWindow.loadFile('index.html');
}

// 应用程序生命周期
app.whenReady().then(() => {
// 在这里之前,渲染进程还未启动

// 调用 createWindow() 后,渲染进程才会启动
createWindow();
});

渲染进程的启动顺序:

  1. 主进程启动
  2. 等待 app ready
  3. 创建 BrowserWindow 实例
  4. 加载页面(loadURL/loadFile)
  5. 创建渲染进程
  6. 执行预加载脚本(preload)
  7. 加载并执行页面的 JavaScript

因此,如果你需要在渲染进程启动前做一些初始化工作,可以在 createWindow 调用前进行:

1
2
3
4
5
6
7
8
9
10
11
// typescript:frontend/src-electron/electron-main.ts
app.whenReady().then(async () => {
// 在渲染进程启动前的初始化工作
await initializeServices();
await loadConfigurations();
await setupDatabases();

// 然后创建窗口,启动渲染进程
createWindow();
createTray();
});