В Electron, если вы хотите обмениваться данными между процессом Node, в котором запущено ваше приложение (основное), и процессом Chromium, в котором запущен ваш интерфейс JS (рендерер), вам нужно будет использовать IPC, Inter Process Communication. IPC очень полезен для многих вещей, таких как выполнение вызовов сервера и избегание CORS и доступ к собственным API, таким как хранилище файлов и другие API ОС. Лучше всего то, что он одновременно асинхронный и синхронный, что упрощает его использование, но есть одна проблема.
IPC и пользовательские ошибки
Отклоненные промисы IPC не обрабатывают пользовательские объекты ошибок. IPC отправляет и получает сообщения в формате JSON. Сериализатор ошибок прерывается, когда он сталкивается с любым объектом ошибки, который имеет настраиваемые поля. Для простых случаев использования это не проблема. Но если вы используете Axios, например, для вызовов API в основном процессе, а Axios выдает ошибку 500 из-за неправильного ответа сервера, IPC не передаст эту ошибку. Это связано с тем, как V8 сериализует ошибочные объекты — V8 — это движок, который запускает JS в Chromium. V8 выдает ошибку, и ничто не перехватывает эту ошибку, поэтому весь IPC обещает быть отклоненным. Это боль, потому что вы не можете просто отклонить вызов IPC и передать нужную ошибку. Это заставляет вас использовать стандартные ошибки или переводить пользовательские ошибки в стандартные.
Отклонение вызова IPC отправит получателю общую ошибку IPC UnhandledPromiseRejectionWarning
, что сбивает с толку, поскольку реальная ошибка не имеет ничего общего с IPC, а связана с Axios. Эта ошибка возникает, когда промис отклоняется, и нет предложения catch для обработки отклонения. Таким образом, когда вызов Axios терпит неудачу, вы вынуждены перехватывать ошибку и отправлять ее в разрешенном вызове, чтобы сохранить данные в ошибке. Это создает впечатление, что вызов Axios был успешным, потому что обещание было разрешено, а не отклонено. Вам понадобится отдельный путь кода для обработки этих ложных разрешений. Это беспорядок.
Моя работа вокруг
Обходной путь, который я придумал, состоял в том, чтобы создать тонкую абстракцию вокруг вызовов IPC во внешнем интерфейсе и незначительных изменений в бэкэнде. Я делаю вызовы основному процессу, используя эту абстракцию, и эта абстракция получает ответы от основного процесса.
В основном процессе я перехватываю все ошибки и упаковываю их в разрешенное сообщение, в котором я добавляю флаг к handle_as_rejected_promise
, для которого установлено значение true
. Этот флаг указывает уровню абстракции отклонить промис, как только он получит сообщение. Поскольку обещание разрешено, я могу передать ему любой объект ошибки, не сталкиваясь с проблемами синтаксического анализа. Уровень абстракции во внешнем интерфейсе преобразует разрешенное обещание в отклоненное и передает ему ошибку. Мой интерфейс получает отклоненное обещание с правильной ошибкой. Это сработало очень хорошо, и я применил его ко всем своим вызовам IPC.
Преимущества
- все обещания разрешаются, поэтому вы можете передавать настраиваемые объекты ошибок средству визуализации и избегать путаницы ошибок IPC.
- простой и удобный интерфейс для понимания и обслуживания
- позволяет обрабатывать IPC как обычные асинхронные вызовы, не нужно создавать отдельные пути кода для ошибок IPC
- быстрое и полное, это решение не требует априорных знаний о том, какие ошибки произойдут — оно обрабатывает ожидаемые и непредвиденные ошибки без каких-либо изменений кода
- БОНУС, упрощает тестирование, уровень абстракции легче имитировать, чем модуль IPC.
Недостатки
- еще один фрагмент кода, который нужно написать, протестировать и поддерживать
- разрешенное обещание перегрузки может вызвать небольшую путаницу, если оно не задокументировано
Другие обходные пути
Есть и другие попытки решить эту проблему, большинство из них задокументировано в выпуске GitHub. Некоторые из этих решений включают преобразование пользовательских ошибок в дескриптор формата IPC (строку или существующий объект ошибка) перед отправкой ошибки. Это требует знания всех ошибок времени выполнения, с которыми вы столкнетесь заранее, что за десять лет работы с JS не стоит вашего времени. В конечном счете, эта проблема должна быть решена в первопричине, V8 нуждается в исправлении.
Код рендерера — абстракция
Вот JS-версия абстракции, а TypeScript-версию можно найти здесь.
/** * This exists because `ipcMain.handle` does not allow * you to return rejected Promises with custom data. * You can throw an error in `handle` but it can only * be a string or existing error object. This means all * the error processing logic must live in main process * in order to figure what string or error type to throw * in `handle`. * * This abstraction allows us to send messages to `handle`. * If `handle` resolves to message with `rejected` equal * to true then this method throws an error with the object * that is contained in the resolved promise. Everything else * is the same. * * This allows us to get custom error objects and use catch * in `ipc.invoke` calls. It also frees us to remove all the * error catching from `then` - since all failures will be * caught in ipcMain and re-thrown here. * * https://github.com/electron/electron/issues/24427 * https://github.com/electron/electron/issues/25196 */ import { ipcRenderer } from 'electron' const invoke = async (channel, args) => { try { const response = await ipcRenderer.invoke(channel, args) if (isRejectedPromise(response)) throw response else return response } catch (error) { console.warn( '[IpcRendererService.invoke] threw an error', { error }, ) return new Promise((_, reject) => reject(error)) } } const isRejectedPromise = response_from_ipc_main => { if (response_from_ipc_main === undefined) return false else if (response_from_ipc_main.handle_as_rejected_promise) return true else return false } export default { invoke }
Код рендерера — использование абстракции
import IpcRendererService from '../services/ipc_renderer_service' IpcRendererService.invoke('check-for-app-updates').then( this.handleCheckForAppUpdateResponse ).catch( this.handleCheckForAppUpdateError ).finally( () => progressbar.hide() )
Код Main/Background.js
const checkForUpdates = async () => { try { const result = await autoUpdater.checkForUpdatesAndNotify() return result.updateInfo } catch (error) { console.log('checkForUpdates, failed', error) return { error, handle_as_rejected_promise: true, } } } ipcMain.handle('check-for-updates', checkForUpdates)