Опубліковано 16 травня 2025 р.

Розробка

Емуляція сервера для додатків Electron

Під час розробки Electron-додатків часто доводиться мати справу з міжпроцесовою взаємодією (inter-process communication, IPC). Але є альтернатива: можна використовувати знайому з веб-розробки архітектуру клієнт-сервер. Не запускаючи локальний сервер.

Цей підхід дозволить стати частково незалежним від інструментів. Наприклад, завжди можна буде портувати додаток для використання у звичайному браузері, який комунікуватиме з нормальним сервером.

Початковий код доступний у моєму репозиторії. Це рішення також доступне як модуль для Node.js на npm. У кінці статті є короткий приклад використання бібліотеки.

Структура

  • Сервер: обслуговує API-запити та, можливо, надсилає фронтенд-код;

  • Electron: запуск додатку, як зазвичай;

  • Проксі: перехоплює запити до “сервера” та емулює відповіді.

Залежності

Для створення сервера я скористаюсь Express.js, а для емуляції запитів — light-my-request:

npm install express light-my-request

Сервер

Задля простоти наш сервер матиме лише одну кінцеву точку API — /api/ping, що відповідатиме стрічкою "Pong".

// server.js
import express from "express"

export const server = express()
server.get("/api/ping", (request, response) => {
    response.setHeader("Content-Type", "text/html")
    response.send("Pong")
})

Можна також налаштувати сервер, аби він відправляв файли фронтенду або навіть відправляв запити до сервера вашого бандлеру (наприклад, Vite).

Electron

Поки в нас ще не реалізован проксі, але ми хочемо спочатку зрозуміти, як він буде використаний. Зараз буде шаблонний код, але зверніть увагу на виклик protocol.handle. Він дозволяє перехопити запити, що використовують конкретний протокол (у нашому випадку HTTP).

// app.js
import { app, protocol, BrowserWindow } from "electron"
import { createProxy } from "./proxy.js"
import { server } from "./server.js"

const baseURL = new URL("http://example-app.local")

function createMainWindow() {
    const window = new BrowserWindow()
    const url = new URL("./api/ping", baseURL)
    window.loadURL(url.toString())
}

function onAppReady() {
    // Тут ми реєструємо наш проксі
    protocol.handle(
        // Ми не хочемо, щоб у кінці була двокрапка
        baseURL.protocol.slice(0, -1),
        createProxy(baseURL, server)
    )

    createMainWindow()
}

function onAllWindowsClosed() {
    if (process.platform !== "darwin") {
        app.quit()
    }
}

function onActivated() {
    if (BrowserWindow.getAllWindows().length === 0) {
        createMainWindow()
    }
}

app.whenReady().then(onAppReady).catch(console.error)
app.on("window-all-closed", onAllWindowsClosed)
app.on("activate", onActivated)

Проксі

Задачі нашого проксі:

  • емуляція запиту до бекенду, якщо хост в URL відповідає URL нашого “сервера”;

  • виконати нормальний запит в іншому випадку.

Метод createProxy створює метод для обслуговування кожного HTTP-запита. Відповідь буде об’єктом типу Response. Ось загальний скелет:

// proxy.js

// Щоб виконати нормальний запит
import { net } from "electron"
// Для емуляції запиту до бекенду
import inject from "light-my-request"

export function createProxy(baseURL, server) {
    return request => {
        const url = new URL(request.url)

        // Якщо URL хосту не відповідає адресі нашого емульованого сервера
        if (url.hostname !== baseURL.hostname) {
            // Виконуємо нормальний запит.
            return net.fetch(request)
        }

        // Інакше ми хочему емулювати запит до нашого севрера.
        return emulateRequest({ server, baseURL, url, request })
    }
}

Тепер час визначити метод для емуляції самого запиту. Просто викликаємо inject із бібліотеки light-my-request, перетворюючи типи аргументів і відповіді для сумісності.

const cacheDisablingHeaders = {
    "Cache-Control": "no-cache, no-store, must-revalidate",
    "Pragma": "no-cache",
    "Expires": 0,
    "If-None-Match": null
}

async function emulateRequest({ server, baseURL, url, request }) {
    // Емулюємо запит.
    const response = await inject(server, {
        // Створюємо об’єкт з параметрами для бібліотеки
        // на базі об’єкта Response
        method: request.method,
        url: request.url,
        query: url.search,
        remoteAddress: new URL("/", baseURL).toString(),
        body: Buffer.from(await request.arrayBuffer()),
        // Нам треба перетворити заголовки запиту
        // в потрібний формат і вимкнути кешування
        // (дивитись нижче).
        headers: {
            ...Object.fromEntries([...request.headers]),
            ...cacheDisablingHeaders
        }
    }).end()

    // Перетворюємо відповідь в об’єкт типу Response.
    return new Response(response.body, {
        headers: response.headers,
        status: response.statusCode,
        statusText: response.statusMessage
    })
}

Як ви помітили, ми вимикаємо кешування для цих локальних запитів. Це тому що Node.js видає помилку, коли створюється об’єкт Response зі статусом 304 Not Modified. Не впевнений, чи є кращий метод обходу цієї помилки, але вказане рішення працює.

Результат

Якщо виконаєте npx electron app.js, ви побачите наступне вікно:

loading-ag-132

Бібліотека

Для перевикористання цього підходу я опублікував бібліотеку, що робить усі ці кроки замість вас. Для початку встановіть її у свій проект:

npm install electron-client-server electron

Потім імпортуйте в головний файл:

import { applyProxy } from "electron-client-server"

Визначте, за якою адресою буде доступний емульований сервер:

const baseURL = new URL("http://localhost:5173")

Тепер просто застосуйте проксі, коли додаток буде готовий для виконання:

app.whenReady().then(() => {
    applyProxy("http", baseURL, server)
    // ...
})

Я протестував це рішення за допомогою Express. Fastify теж має спрацювати. І, скоріш за все, інші бібліотеки для створення серверів.

Якщо знайшли баг або маєте пропозицію про покращення, прошу відкрити питання на сторінці репозиторію.