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

使用Promise未搭配async/await时Electron打开对话框卡顿

解决Electron Dialog与typescript-fsa异步工具结合时的卡顿冻结问题

我之前处理过好几起Electron dialog和Redux Observable(从你提到的epic来看,应该是用了这个库)结合时的卡顿冻结问题,结合你说的typescript-fsa异步工具在其他100个epic都正常的情况,几乎可以确定问题出在Electron dialog的同步阻塞特性和你的epic调度逻辑的冲突上。

问题根源分析

Electron的dialog.showOpenDialog这类API,虽然新版本支持Promise调用,但本质上还是会阻塞主线程——对话框的渲染和用户交互依赖主线程完成。而你的epic运行在Redux的调度流中,当你在epic里直接调用dialog时,主线程被dialog阻塞,没法处理后续的Redux动作(也就是typescript-fsa生成的done/failed动作),最终导致整个程序卡顿冻结。

你的typescript-fsa异步工具本身没有问题,只是Electron dialog的特性刚好和渲染进程的Redux调度流产生了冲突。

可行的解决方案

方案一:把Dialog调用移到主进程(最推荐)

Electron官方更推荐在主进程中调用dialog API,通过IPC和渲染进程通信,这样可以完全避免渲染进程主线程被阻塞。

渲染进程侧(Epic代码)

import { ipcRenderer } from 'electron';
import { actionCreatorFactory } from 'typescript-fsa';
import { from } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';

// 用typescript-fsa创建异步动作
const actionCreator = actionCreatorFactory();
export const openFileDialog = actionCreator.async<void, string[], Error>('OPEN_FILE_DIALOG');

// 对应的Epic
export const openFileDialogEpic = (action$) =>
  action$.ofType(openFileDialog.started)
    .pipe(
      mergeMap(() => 
        // 通过IPC调用主进程的dialog方法
        from(ipcRenderer.invoke('show-open-file-dialog'))
          .pipe(
            mergeMap(result => [openFileDialog.done({ result })]),
            catchError(err => [openFileDialog.failed({ error: err })])
          )
      )
    );

主进程侧代码

import { ipcMain, dialog, BrowserWindow } from 'electron';

// 注册IPC handler
ipcMain.handle('show-open-file-dialog', async () => {
  const focusedWindow = BrowserWindow.getFocusedWindow();
  if (!focusedWindow) throw new Error('No focused window');

  const dialogResult = await dialog.showOpenDialog(focusedWindow, {
    properties: ['openFile'],
    // 可根据需求添加其他配置
    filters: [{ name: 'Text Files', extensions: ['txt'] }]
  });

  if (dialogResult.canceled) {
    throw new Error('User canceled the dialog');
  }

  return dialogResult.filePaths;
});

方案二:在渲染进程中延迟调用Dialog(临时 workaround)

如果暂时不想动主进程代码,可以用setTimeout把dialog调用放到事件循环的下一轮,避免阻塞epic的调度流:

import { actionCreatorFactory } from 'typescript-fsa';
import { from } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';
import { dialog } from '@electron/remote'; // Electron 14+需单独安装并启用remote

const actionCreator = actionCreatorFactory();
export const openFileDialog = actionCreator.async<void, string[], Error>('OPEN_FILE_DIALOG');

export const openFileDialogEpic = (action$) =>
  action$.ofType(openFileDialog.started)
    .pipe(
      mergeMap(() => 
        new Promise<string[]>((resolve, reject) => {
          // 用setTimeout把dialog调用放到下一个事件循环
          setTimeout(async () => {
            try {
              const result = await dialog.showOpenDialog({ properties: ['openFile'] });
              if (result.canceled) reject(new Error('Canceled'));
              else resolve(result.filePaths);
            } catch (err) {
              reject(err);
            }
          }, 0);
        })
          .pipe(
            mergeMap(result => [openFileDialog.done({ result })]),
            catchError(err => [openFileDialog.failed({ error: err })])
          )
      )
    );

方案三:调整RxJS的调度器

借助RxJS的调度器,把dialog完成后的动作调度放到异步队列,避免和dialog的阻塞冲突:

import { actionCreatorFactory } from 'typescript-fsa';
import { from, asyncScheduler } from 'rxjs';
import { mergeMap, catchError, observeOn } from 'rxjs/operators';
import { dialog } from '@electron/remote';

const actionCreator = actionCreatorFactory();
export const openFileDialog = actionCreator.async<void, string[], Error>('OPEN_FILE_DIALOG');

export const openFileDialogEpic = (action$) =>
  action$.ofType(openFileDialog.started)
    .pipe(
      mergeMap(() => 
        from(dialog.showOpenDialog({ properties: ['openFile'] }))
          .pipe(
            // 用asyncScheduler把后续动作放到异步队列
            observeOn(asyncScheduler),
            mergeMap(result => {
              if (result.canceled) throw new Error('Canceled');
              return [openFileDialog.done({ result: result.filePaths })];
            }),
            catchError(err => [openFileDialog.failed({ error: err })])
          )
      )
    );

总结

最稳妥的还是方案一,符合Electron的设计规范,彻底避免渲染进程主线程阻塞的问题。你的typescript-fsa工具本身没有问题,只是Electron dialog的同步特性和渲染进程的Redux调度流产生了冲突,把dialog调用移到主进程就能解决这个问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 09:11:08