В 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)

Источники