#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
| Экспорт | Тип | Назначение |
debouncedWaitTime | const (250 или 1000) | Адаптивный дебаунс для полей автозаполнения на основе заголовка Save-Data |
baseURL | const (строка) | Источник сервера — localhost:8080 в dev, пусто в prod |
baseRestURL | const (строка) | 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) | функция → Promise | GET с JSON-ответом, парсит и передаёт в колбэк |
fetchJsonPost(url, body, cb) | функция → Promise | POST с 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 мс. Отказоустойчивость работает корректно. |