EN · DE · RU · FR · ES

#2757: rest.js

projectforge-webapp/src/utilities/rest.js JavaScript / React утилита — центральный REST API клиент для React-фронтенда ProjectForge. Исходник: projectforge-webapp/src/utilities/rest.js 88 строк · 68 кода · 7 комментариев · 13 пустых
Каждый React-компонент, взаимодействующий с бэкендом ProjectForge, проходит через этот модуль. Он предоставляет тонкую обёртку над fetch API с построением URL с учётом окружения, аутентификацией через сессионные куки (credentials: 'include'), адаптивным дебаунсингом для медленных соединений и обработкой JSON-запросов/ответов. Это единая точка контакта между React SPA и REST-слоем Spring Boot — во фронтенде нет другого HTTP-клиента.

Архитектура

Конвейер построения URL

Модуль строит REST URL через трёхэтапный конвейер, каждый этап решает свою задачу:

Этап 1 — baseURL: Определяет источник сервера. В разработке (import.meta.env.DEV) указывает на http://localhost:8080 — dev-сервер Spring Boot. В продакшене возвращает пустую строку, делая все URL относительными к источнику страницы (CORS не требуется). Проверка MODE !== 'test' предотвращает утечку URL тестового сервера в Jest-тесты.

Этап 2 — getServiceURL / baseRestURL: Добавляет префикс REST. Если URL сервиса начинается с / (абсолютный от корня), используется baseURL напрямую. В противном случае используется baseRestURL, который добавляет /rs — стандартный префикс REST-эндпоинтов ProjectForge. Это позволяет использовать как getServiceURL('/some/path') (абсолютный), так и getServiceURL('user/list') (относительно /rs).

Этап 3 — evalServiceURL / createQueryParams: Добавляет параметры запроса. Фильтрует значения undefined, кодирует каждое значение и корректно использует ? или & в зависимости от того, содержит ли URL уже строку запроса.

Стратегия дебаунсинга — учёт Save-Data

Константа debouncedWaitTime адаптируется на основе клиентского сигнала Save-Data (MDN). Когда пользователь включил режим экономии данных (часто на мобильных устройствах), время ожидания увеличивается с 250 мс до 1000 мс — снижая частоту запросов на лимитированных соединениях. Это используется компонентами автодополнения (поиск адресов, пользователей, задач) для ограничения API-вызовов во время ввода.

Реализация защитная: navigator && navigator.connection && navigator.connection.saveData — три проверки перед доступом к свойству, так как navigator.connection доступен не во всех браузерах (Firefox, Safari).

Экспортируемый API

ЭкспортТипНазначение
debouncedWaitTimeconst (250 или 1000)Адаптивный дебаунс для полей автозаполнения на основе заголовка Save-Data
baseURLconst (строка)Источник сервера — localhost:8080 в dev, пусто в prod
baseRestURLconst (строка)baseURL + "/rs" — стандартный префикс REST-эндпоинтов
createQueryParams(obj)функция → строкаСтроит key=val&key=val из объекта, фильтрует undefined значения
evalServiceURL(url, params)функция → строкаДобавляет параметры запроса к URL, обрабатывает существующие ? и &
getServiceURL(url, params)функция → строкаПолный построитель URL — добавляет base + REST префикс, добавляет параметры
handleHTTPErrors(res)функция → ResponseВыбрасывает ошибку при не-2xx ответах, пропускает при успехе
fetchJsonGet(url, params, cb)функция → PromiseGET с JSON-ответом, парсит и передаёт в колбэк
fetchJsonPost(url, body, cb)функция → PromisePOST с JSON-сериализованным телом и JSON-ответом
fetchGet(url, params, cb)функция → PromiseПростой GET с опциональным колбэком (без JSON-парсинга)
getObjectFromQuery(str)функция → объектОбратная операция createQueryParams — парсит ?key=val&... в объект

Обработка ошибок

Все три fetch-функции используют одинаковый шаблон обработки ошибок: .catch((error) => alert('Внутренняя ошибка: ' + error)). Директива /* eslint-disable no-alert */ в начале файла подтверждает, что alert() обычно запрещён ESLint, но здесь используется намеренно как уведомление пользователя в крайнем случае.

Это осознанный компромисс: правильная система тостов (на Redux, с i18n) была бы лучше с точки зрения UX, но rest.js находится на дне графа зависимостей — он не может импортировать Redux или React-компоненты. Использование alert() означает, что обработчик ошибок не имеет зависимостей и работает в любом контексте (даже вне жизненного цикла React). Директива /* eslint-disable no-alert */ была добавлена в коммите ac30e55f7 для подавления предупреждения линтера об этом намеренном выборе.

Аутентификация

Все fetch-вызовы используют credentials: 'include' — это указывает браузеру отправлять HTTP-only сессионные куки с каждым запросом. Это стандартный механизм аутентификации для SPA, обслуживаемых с того же источника, что и API. Сессионная кука устанавливается бэкендом Spring Boot при входе и автоматически включается браузером. Нет управления токенами, нет заголовка Authorization и нет клиентской логики сессии — браузер обрабатывает это прозрачно через флаг credentials.

Использование во фронтенде

Этот модуль импортируется практически каждым React-компонентом, который получает данные. Типичные шаблоны использования:
// В React-компоненте:
import { fetchJsonGet, fetchJsonPost } from '../utilities/rest';

// Загрузка списка
fetchJsonGet('user/list', { search: 'kai' }, (data) => setUsers(data));

// Сохранение сущности
fetchJsonPost('user/save', { id: 1, username: 'kai' }, (data) => console.log('сохранено', data));

// Простой GET для загрузки файла
fetchGet('export/excel', { id: 123 }, () => setDownloading(false));

Потребители

Модуль импортируется во всём React-фронтенде — адресная книга, календарь, табели учёта времени, управление пользователями, опросы, скрипты и все UI плагины. На момент написания около 40+ React-компонентов импортируют из этого файла. Любое изменение логики построения URL (например, изменение префикса REST с /rs на что-то другое) затронет каждый API-вызов в приложении.

История Git

КоммитЧто изменилось в этом файле
bf988bc6dМиграция с React-scripts на Vite. Определение окружения изменилось с process.env.NODE_ENV === 'development' (соглашение CRA) на import.meta.env.DEV && import.meta.env.MODE !== 'test' (соглашение Vite). Добавленная проверка MODE !== 'test' предотвращает указание Jest на localhost:8080 во время тестов — распространённая ошибка при миграции с CRA на Vite, где режим тестового окружения — 'test', а не 'development'.
823ef0992Защитная проверка колбэка в fetchGet. Изменено с .then(() => callback()) на .then(() => { if (typeof callback === 'function' && callback()); }). Функция fetchGet имеет опциональный колбэк — при вызове без него (например, запросы "забыл и забыл") старый код выбрасывал TypeError: callback is not a function. Проверка молча пропускает колбэк, если он не функция.
253b9f38bФорматирование кода. Многострочные цепочки fetch-вызовов преобразованы в однострочные. Было: fetch(\n getServiceURL(...), {\n method: 'GET',\n .... Стало: fetch(getServiceURL(...), { method: 'GET', .... Уменьшен визуальный шум, поведение не изменилось.
ac30e55f7Добавлено подавление ESLint. Вставлено /* eslint-disable no-alert */ в начале файла. Обработчики ошибок используют alert() для уведомления пользователя об ошибках, что запрещено рекомендуемой конфигурацией ESLint. Подавление подтверждает, что это намеренно — механизм уведомления об ошибках без зависимостей для модуля на дне графа зависимостей.
bbd81edc3Форматирование под ESLint. Стрелочные функции изменены с params => ... на (params) => ... (с круглыми скобками для одного параметра). Избыточное тело функции в getServiceURL удалено — функция ранее была определена с function getServiceURL(...) { ... }, и переписывание было только изменением форматирования.

Проектные решения

РешениеПочемуКомпромисс
Без Axios, чистый fetchНоль зависимостей. Модуль занимает ~80 строк и покрывает все нужды фронтенда. Добавление Axios (~14 КБ в gzip) ради перехватчиков и автоматического JSON-парсинга не оправдано для трёх HTTP-методов.Нет перехватчиков запросов/ответов — обновление токена аутентификации потребовало бы собственного кода. Сейчас не нужно, так как аутентификация основана на куках.
Колбэки вместо возврата промисовИсторически. Когда этот модуль был написан (2019), фронтенд использовал классовые компоненты с this.setState в колбэках. Возврат промисов потребовал бы .then(data => this.setState(...)) в каждом потребителе — шаблон с колбэками был проще в то время.Сложнее объединять в цепочки, сложнее использовать с async/await. Потребители должны передавать колбэки, даже если им просто нужны данные. Современная переработка возвращала бы промисы.
alert() для обработки ошибокНоль зависимостей, работает в любом контексте. Правильная система тостов потребовала бы импорта React/Redux — создавая риск циклических зависимостей.Некрасиво, не поддерживает i18n, блокирует поток UI. Лучше было бы использовать отложенный эмиттер событий ошибок: window.dispatchEvent(new CustomEvent('rest:error', {detail: error})) — обрабатываемый React-границей ошибок или компонентом тостов.
Дебаунс с учётом saveDataПрогрессивное улучшение. Пользователи с лимитированными соединениями (мобильные, развивающиеся страны) получают более медленный дебаунс — меньше API-вызовов, меньше трафика. Пользователи с безлимитными соединениями получают более быстрый дебаунс — более отзывчивый автокомплит.Поддержка браузеров ограничена — navigator.connection доступен только в Chromium. Пользователи Firefox/Safari всегда получают значение по умолчанию 250 мс. Отказоустойчивость работает корректно.