Перейти к содержимому

Практика 12. Практическая работа 12. Сетевой слой: API-клиент, интерсепторы, JWT

Раздел 2. Привязка к лекции 12 («Сети, часть 1»). Работа практико-кодовая: строим переиспользуемый сетевой слой и проверяем его на реальных запросах.

Цели работы

  • Построить единый API-клиент — обёртку над fetch (или axios) с базовым URL и общими заголовками.
  • Реализовать интерсептор запроса: автоматическая подстановка JWT в заголовок Authorization.
  • Реализовать интерсептор ответа: парсинг JSON и единая обработка кодов ошибок.
  • Закрепить теорию: сравнить размер JSON-ответа, обсудить уместность Protobuf, обзорно посмотреть на заголовки HTTP/2.

Коротко о теории

Зачем нужен сетевой слой. Если разбросать fetch по экранам, то URL, заголовки и обработка ошибок дублируются повсюду — при смене API придётся править десятки файлов. Весь сетевой код выносят в один модуль — клиент API. Структура: компонент → сервис → клиент API (+интерсепторы) → сеть.

Интерсептор — перехватчик, встроенный в каждый запрос или ответ: в запросе подставляет токен, в ответе централизованно обрабатывает ошибки.

JWT — самодостаточный токен вида header.payload.signature. Передаётся в заголовке Authorization: Bearer <token>; сервер проверяет подпись и срок действия exp, не храня сессию (stateless).


Задание

Работаем в папке api/. Для тестовых запросов подойдёт публичный API https://jsonplaceholder.typicode.com или ваш учебный бэкенд с авторизацией.

Шаг 1. Хранилище токена

Вынесем доступ к токену в отдельный модуль, чтобы клиент не знал, где он лежит (в учебном примере — переменная в памяти, в реальном приложении — expo-secure-store или AsyncStorage).

api/tokenStore.js
let accessToken = null;
export const setToken = (t) => { accessToken = t; };
export const getToken = () => accessToken;
export const clearToken = () => { accessToken = null; };

Шаг 2. Базовый клиент на fetch

Клиент инкапсулирует базовый URL, таймаут и общие заголовки. Ядро — приватная функция request, на её основе строятся get и post.

api/client.js
import { getToken, clearToken } from './tokenStore';
const BASE_URL = 'https://jsonplaceholder.typicode.com';
const TIMEOUT = 10000;
// Класс ошибки, чтобы наверху различать сетевые сбои
export class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}

Шаг 3. Интерсептор запроса (подстановка JWT)

Перед отправкой собираем заголовки и, если токен есть, добавляем Authorization.

// api/client.js (продолжение)
function buildHeaders(custom = {}) {
const headers = { 'Content-Type': 'application/json', ...custom };
const token = getToken(); // интерсептор запроса
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
}

Шаг 4. Интерсептор ответа (парсинг JSON и обработка ошибок)

Единая точка, где разбираем тело и реагируем на коды состояния.

// api/client.js (продолжение)
async function handleResponse(response) {
const text = await response.text();
const data = text ? JSON.parse(text) : null; // парсинг JSON
if (response.ok) return data; // 2xx — успех
switch (response.status) { // обработка ошибок
case 401:
clearToken(); // токен истёк — сбрасываем
throw new ApiError('Не авторизован', 401, data);
case 403: throw new ApiError('Доступ запрещён', 403, data);
case 404: throw new ApiError('Ресурс не найден', 404, data);
default:
const msg = response.status >= 500 ? 'Ошибка сервера' : 'Ошибка запроса';
throw new ApiError(msg, response.status, data);
}
}

Шаг 5. Функция request и методы get/post

request собирает запрос, навешивает таймаут через AbortController и пропускает ответ через интерсептор.

// api/client.js (продолжение)
async function request(path, { method = 'GET', body, headers } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT);
try {
const response = await fetch(`${BASE_URL}${path}`, {
method,
headers: buildHeaders(headers),
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
return await handleResponse(response);
} catch (err) {
if (err.name === 'AbortError')
throw new ApiError('Превышено время ожидания', 0, null);
throw err;
} finally {
clearTimeout(timer);
}
}
export const apiClient = {
get: (path, headers) => request(path, { method: 'GET', headers }),
post: (path, body, headers) => request(path, { method: 'POST', body, headers }),
};

Шаг 6. Пример вызова

api/userService.js
import { apiClient } from './client';
export const userService = {
getUser: (id) => apiClient.get(`/users/${id}`),
createPost: (data) => apiClient.post('/posts', data),
};
// где-то в коде экрана
setToken('eyJhbGciOi...'); // обычно после логина
try {
const user = await userService.getUser(1);
console.log('Имя:', user.name);
} catch (e) {
if (e instanceof ApiError && e.status === 401) console.warn('Нужно заново войти');
else console.error('Сбой сети:', e.message);
}

Шаг 7 (по желанию). Типизация TypeScript

interface User { id: number; name: string; email: string; }
async function request<T>(path: string, opts?: RequestOptions): Promise<T> { /* ... */ }
const user = await apiClient.get<User>('/users/1'); // user выводится как User

Дополнительное задание: форматы и HTTP/2

  1. Размер JSON. Запросите GET /users/1, сохраните ответ в файл и посмотрите его размер. Прикиньте, какую долю занимают повторяющиеся имена полей ("name", "email"…). Сделайте вывод о читаемости против компактности.

  2. Где уместен Protobuf. Опишите 2–3 строками, когда бинарный формат оправдан (высокая нагрузка, gRPC, большие объёмы по медленной сети) и почему для обычного REST проще остаться на JSON.

  3. Заголовки HTTP/2 (обзорно). Выполните в терминале и сравните вывод:

    Окно терминала
    curl -I --http2 https://jsonplaceholder.typicode.com/users/1

    Обратите внимание: статус показан как HTTP/2 200, заголовки в нижнем регистре и сжимаются механизмом HPACK, а запросы по одному соединению идут параллельно (мультиплексирование) — в отличие от очереди в HTTP/1.1.


Критерии оценки

КритерийВес
Клиент с базовым URL, таймаутом и общими заголовками20%
Интерсептор запроса: подстановка JWT в Authorization20%
Интерсептор ответа: парсинг JSON и обработка кодов ошибок20%
Методы get/post и сервис поверх клиента15%
Рабочий пример вызова с обработкой ApiError15%
Доп. задание: сравнение JSON/Protobuf и заголовки HTTP/210%

Вопросы для самопроверки

  1. Почему вызовы fetch не размещают прямо в компонентах экранов? Какую проблему решает выделенный клиент API?
  2. Что делает интерсептор запроса и что — интерсептор ответа? Приведите по примеру задачи для каждого.
  3. В каком заголовке и в каком формате передаётся JWT? Что сервер с ним делает?
  4. Как ваш клиент реагирует на код 401? Почему именно так?
  5. Зачем нужен AbortController и таймаут в мобильной сети? Когда переход с JSON на Protocol Buffers оправдан, а когда избыточен?

Ресурсы