0709 with changes and CollEditor
This commit is contained in:
611
src/api/README.md
Normal file
611
src/api/README.md
Normal file
@@ -0,0 +1,611 @@
|
||||
# API EEV_Proj
|
||||
|
||||
## Обзор
|
||||
|
||||
API функции для взаимодействия с серверной частью проекта EEV_Proj. Все функции используют современный JavaScript и обеспечивают единообразный интерфейс для работы с сервером.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
api/
|
||||
├── auth.js # Функции аутентификации
|
||||
└── README.md # Эта документация
|
||||
```
|
||||
|
||||
## auth.js
|
||||
|
||||
### Функции аутентификации
|
||||
|
||||
#### getUsersStatus(token)
|
||||
Получает статус всех пользователей в системе.
|
||||
|
||||
**Параметры:**
|
||||
- `token` (string) - JWT токен аутентификации
|
||||
|
||||
**Возвращает:**
|
||||
- Promise<Array> - Массив пользователей с их статусами
|
||||
|
||||
**Пример использования:**
|
||||
```javascript
|
||||
import { getUsersStatus } from './api/auth.js';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
try {
|
||||
const users = await getUsersStatus(token);
|
||||
console.log('Пользователи:', users);
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения статуса пользователей:', error);
|
||||
}
|
||||
```
|
||||
|
||||
#### loadUserInfo(userId, token)
|
||||
Загружает информацию о конкретном пользователе.
|
||||
|
||||
**Параметры:**
|
||||
- `userId` (string|number) - ID пользователя
|
||||
- `token` (string) - JWT токен аутентификации
|
||||
|
||||
**Возвращает:**
|
||||
- Promise<Object> - Объект с информацией о пользователе
|
||||
|
||||
**Пример использования:**
|
||||
```javascript
|
||||
import { loadUserInfo } from './api/auth.js';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
try {
|
||||
const userInfo = await loadUserInfo('123', token);
|
||||
console.log('Информация о пользователе:', userInfo);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки информации о пользователе:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## Создание новых API функций
|
||||
|
||||
### Шаблон API функции
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Описание функции
|
||||
* @param {string} param1 - Описание параметра 1
|
||||
* @param {number} param2 - Описание параметра 2
|
||||
* @returns {Promise<Object>} Описание возвращаемого значения
|
||||
*/
|
||||
export async function apiFunction(param1, param2) {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
throw new Error('Токен не найден');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/endpoint`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ param1, param2 })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Ошибка API функции:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Принципы
|
||||
|
||||
1. **Единообразие** - Все функции следуют одному паттерну
|
||||
2. **Обработка ошибок** - Всегда обрабатывайте ошибки
|
||||
3. **Валидация** - Проверяйте входные параметры
|
||||
4. **Логирование** - Логируйте ошибки для отладки
|
||||
5. **Типизация** - Используйте JSDoc для документирования типов
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Типы ошибок
|
||||
|
||||
```javascript
|
||||
// Сетевые ошибки
|
||||
class NetworkError extends Error {
|
||||
constructor(message, status) {
|
||||
super(message);
|
||||
this.name = 'NetworkError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
// Ошибки аутентификации
|
||||
class AuthError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'AuthError';
|
||||
}
|
||||
}
|
||||
|
||||
// Ошибки валидации
|
||||
class ValidationError extends Error {
|
||||
constructor(message, field) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Обработка в компонентах
|
||||
|
||||
```javascript
|
||||
import { apiFunction } from './api/api.js';
|
||||
|
||||
const MyComponent = () => {
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleApiCall = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await apiFunction('param1', 'param2');
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
if (error.name === 'AuthError') {
|
||||
// Перенаправление на страницу входа
|
||||
navigate('/login');
|
||||
} else if (error.name === 'ValidationError') {
|
||||
// Показать ошибку валидации
|
||||
setError(`Ошибка в поле ${error.field}: ${error.message}`);
|
||||
} else {
|
||||
// Общая ошибка
|
||||
setError(error.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <div>Загрузка...</div>}
|
||||
{error && <div className="error">{error}</div>}
|
||||
{data && <div>{/* Отображение данных */}</div>}
|
||||
<button onClick={handleApiCall}>Вызвать API</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Кэширование
|
||||
|
||||
### Простое кэширование
|
||||
|
||||
```javascript
|
||||
const cache = new Map();
|
||||
|
||||
export async function cachedApiCall(key, apiFunction) {
|
||||
if (cache.has(key)) {
|
||||
const { data, timestamp } = cache.get(key);
|
||||
const now = Date.now();
|
||||
|
||||
// Кэш действителен 5 минут
|
||||
if (now - timestamp < 5 * 60 * 1000) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiFunction();
|
||||
cache.set(key, { data, timestamp: Date.now() });
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Использование
|
||||
|
||||
```javascript
|
||||
import { cachedApiCall } from './api/cache.js';
|
||||
import { getUsersStatus } from './api/auth.js';
|
||||
|
||||
const loadUsers = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
return await cachedApiCall('users-status', () => getUsersStatus(token));
|
||||
};
|
||||
```
|
||||
|
||||
## Retry логика
|
||||
|
||||
### Автоматические повторы
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Выполняет API вызов с автоматическими повторами
|
||||
* @param {Function} apiFunction - Функция API
|
||||
* @param {number} maxRetries - Максимальное количество повторов
|
||||
* @param {number} delay - Задержка между повторами в мс
|
||||
*/
|
||||
export async function retryApiCall(apiFunction, maxRetries = 3, delay = 1000) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await apiFunction();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// Ждем перед следующим попыткой
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Использование
|
||||
|
||||
```javascript
|
||||
import { retryApiCall } from './api/retry.js';
|
||||
import { getUsersStatus } from './api/auth.js';
|
||||
|
||||
const loadUsersWithRetry = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
return await retryApiCall(() => getUsersStatus(token), 3, 1000);
|
||||
};
|
||||
```
|
||||
|
||||
## Batch запросы
|
||||
|
||||
### Группировка запросов
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Выполняет несколько API запросов параллельно
|
||||
* @param {Array<Function>} apiFunctions - Массив функций API
|
||||
* @returns {Promise<Array>} Массив результатов
|
||||
*/
|
||||
export async function batchApiCalls(apiFunctions) {
|
||||
try {
|
||||
const results = await Promise.allSettled(apiFunctions.map(fn => fn()));
|
||||
|
||||
return results.map((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
} else {
|
||||
console.error(`Ошибка в запросе ${index}:`, result.reason);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка batch запросов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Использование
|
||||
|
||||
```javascript
|
||||
import { batchApiCalls } from './api/batch.js';
|
||||
import { getUsersStatus, loadUserInfo } from './api/auth.js';
|
||||
|
||||
const loadAllData = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const results = await batchApiCalls([
|
||||
() => getUsersStatus(token),
|
||||
() => loadUserInfo('123', token),
|
||||
() => loadUserInfo('456', token)
|
||||
]);
|
||||
|
||||
const [users, user1, user2] = results;
|
||||
return { users, user1, user2 };
|
||||
};
|
||||
```
|
||||
|
||||
## WebSocket API
|
||||
|
||||
### Подключение
|
||||
|
||||
```javascript
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
class WebSocketAPI {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
connect(token) {
|
||||
const serverUrl = window.location.hostname === 'localhost'
|
||||
? 'http://localhost:4000'
|
||||
: window.location.origin;
|
||||
|
||||
this.socket = io(serverUrl, {
|
||||
transports: ['websocket', 'polling'],
|
||||
auth: { token },
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
console.log('WebSocket подключен');
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
this.isConnected = false;
|
||||
console.log('WebSocket отключен');
|
||||
});
|
||||
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit(event, data);
|
||||
} else {
|
||||
console.warn('WebSocket не подключен');
|
||||
}
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (this.socket) {
|
||||
this.socket.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.socket) {
|
||||
this.socket.off(event, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const wsAPI = new WebSocketAPI();
|
||||
```
|
||||
|
||||
### Использование
|
||||
|
||||
```javascript
|
||||
import { wsAPI } from './api/websocket.js';
|
||||
|
||||
// Подключение
|
||||
const token = localStorage.getItem('token');
|
||||
wsAPI.connect(token);
|
||||
|
||||
// Отправка события
|
||||
wsAPI.emit('playerMovement', { x: 100, y: 0, z: 200 });
|
||||
|
||||
// Подписка на события
|
||||
wsAPI.on('economy:balanceChanged', ({ userId, newBalance }) => {
|
||||
console.log('Баланс изменился:', newBalance);
|
||||
});
|
||||
|
||||
// Отписка
|
||||
wsAPI.off('economy:balanceChanged');
|
||||
```
|
||||
|
||||
## Тестирование API
|
||||
|
||||
### Mock функции
|
||||
|
||||
```javascript
|
||||
// __mocks__/api/auth.js
|
||||
export const getUsersStatus = jest.fn();
|
||||
export const loadUserInfo = jest.fn();
|
||||
|
||||
// Сброс моков
|
||||
beforeEach(() => {
|
||||
getUsersStatus.mockClear();
|
||||
loadUserInfo.mockClear();
|
||||
});
|
||||
```
|
||||
|
||||
### Тесты
|
||||
|
||||
```javascript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { getUsersStatus } from './api/auth.js';
|
||||
|
||||
// Мокаем модуль
|
||||
jest.mock('./api/auth.js');
|
||||
|
||||
describe('API Functions', () => {
|
||||
it('загружает статус пользователей', async () => {
|
||||
const mockUsers = [
|
||||
{ id: 1, name: 'User 1', status: 'online' },
|
||||
{ id: 2, name: 'User 2', status: 'offline' }
|
||||
];
|
||||
|
||||
getUsersStatus.mockResolvedValue(mockUsers);
|
||||
|
||||
const result = await getUsersStatus('token');
|
||||
|
||||
expect(result).toEqual(mockUsers);
|
||||
expect(getUsersStatus).toHaveBeenCalledWith('token');
|
||||
});
|
||||
|
||||
it('обрабатывает ошибки API', async () => {
|
||||
const errorMessage = 'Unauthorized';
|
||||
getUsersStatus.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await expect(getUsersStatus('invalid-token')).rejects.toThrow(errorMessage);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Мониторинг и метрики
|
||||
|
||||
### Логирование API вызовов
|
||||
|
||||
```javascript
|
||||
class APIMonitor {
|
||||
constructor() {
|
||||
this.calls = [];
|
||||
this.errors = [];
|
||||
}
|
||||
|
||||
logCall(endpoint, method, duration, success) {
|
||||
const call = {
|
||||
endpoint,
|
||||
method,
|
||||
duration,
|
||||
success,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.calls.push(call);
|
||||
|
||||
// Ограничиваем размер массива
|
||||
if (this.calls.length > 1000) {
|
||||
this.calls.shift();
|
||||
}
|
||||
}
|
||||
|
||||
logError(endpoint, method, error) {
|
||||
const errorLog = {
|
||||
endpoint,
|
||||
method,
|
||||
error: error.message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.errors.push(errorLog);
|
||||
|
||||
if (this.errors.length > 100) {
|
||||
this.errors.shift();
|
||||
}
|
||||
}
|
||||
|
||||
getStats() {
|
||||
const totalCalls = this.calls.length;
|
||||
const successfulCalls = this.calls.filter(call => call.success).length;
|
||||
const errorCalls = this.errors.length;
|
||||
const avgDuration = this.calls.reduce((sum, call) => sum + call.duration, 0) / totalCalls;
|
||||
|
||||
return {
|
||||
totalCalls,
|
||||
successfulCalls,
|
||||
errorCalls,
|
||||
successRate: (successfulCalls / totalCalls) * 100,
|
||||
avgDuration
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const apiMonitor = new APIMonitor();
|
||||
```
|
||||
|
||||
### Использование в API функциях
|
||||
|
||||
```javascript
|
||||
import { apiMonitor } from './api/monitor.js';
|
||||
|
||||
export async function monitoredApiCall(endpoint, options) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, options);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
apiMonitor.logCall(endpoint, options.method || 'GET', duration, response.ok);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
apiMonitor.logCall(endpoint, options.method || 'GET', duration, false);
|
||||
apiMonitor.logError(endpoint, options.method || 'GET', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
### Валидация токенов
|
||||
|
||||
```javascript
|
||||
export function validateToken(token) {
|
||||
if (!token) {
|
||||
throw new Error('Токен не предоставлен');
|
||||
}
|
||||
|
||||
// Проверяем формат JWT токена
|
||||
const tokenParts = token.split('.');
|
||||
if (tokenParts.length !== 3) {
|
||||
throw new Error('Неверный формат токена');
|
||||
}
|
||||
|
||||
try {
|
||||
// Декодируем payload
|
||||
const payload = JSON.parse(atob(tokenParts[1]));
|
||||
|
||||
// Проверяем срок действия
|
||||
if (payload.exp && Date.now() >= payload.exp * 1000) {
|
||||
throw new Error('Токен истек');
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throw new Error('Неверный токен');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Санитизация данных
|
||||
|
||||
```javascript
|
||||
export function sanitizeInput(input) {
|
||||
if (typeof input !== 'string') {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Удаляем потенциально опасные символы
|
||||
return input
|
||||
.replace(/[<>]/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function sanitizeObject(obj) {
|
||||
const sanitized = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === 'string') {
|
||||
sanitized[key] = sanitizeInput(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
sanitized[key] = sanitizeObject(value);
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user