Архитектура интеграции
Браузер пользователя
│
├── GET index.html ──→ CloudFront ──→ S3 (HTML/CSS/JS)
│
└── GET /count ──→ API Gateway ──→ Lambda ──→ DynamoDB
│
└── Response: {"count": 42}
Фронтенд (HTML/CSS/JS) и бэкенд (API) -- это два отдельных компонента, связанных через HTTP-запросы.
JavaScript Fetch API
Базовая реализация счётчика
// counter.js
const API_URL = 'https://api.ivanpetrov.com/count';
async function updateVisitorCount() {
const counterElement = document.getElementById('visitor-count');
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
counterElement.textContent = data.count;
} catch (error) {
console.error('Failed to fetch visitor count:', error);
counterElement.textContent = 'unavailable';
}
}
// Call when page loads
document.addEventListener('DOMContentLoaded', updateVisitorCount);
Улучшенная версия с retry
// counter.js - Production-ready version
const API_URL = 'https://api.ivanpetrov.com/count';
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // ms
async function fetchWithRetry(url, retries = MAX_RETRIES) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt === retries) throw error;
await new Promise(r => setTimeout(r, RETRY_DELAY * attempt));
}
}
}
async function updateVisitorCount() {
const el = document.getElementById('visitor-count');
try {
const data = await fetchWithRetry(API_URL);
el.textContent = new Intl.NumberFormat('ru-RU').format(data.count);
el.classList.add('loaded');
} catch (error) {
console.error('Visitor counter error:', error);
el.textContent = '—';
el.title = 'Не удалось загрузить счётчик';
}
}
document.addEventListener('DOMContentLoaded', updateVisitorCount);
Подключение к HTML
<!-- В footer секции index.html -->
<footer>
<p>
Посетителей:
<span id="visitor-count" aria-live="polite">загрузка...</span>
</p>
</footer>
<!-- Перед закрывающим </body> -->
<script src="counter.js"></script>
Обработка ошибок
Типичные ошибки и решения
| Ошибка | Причина | Решение |
|---|---|---|
TypeError: Failed to fetch |
CORS блокирует запрос | Настроить CORS на API Gateway |
403 Forbidden |
Нет доступа к API | Проверить IAM permissions |
500 Internal Server Error |
Ошибка в Lambda | Проверить CloudWatch Logs |
net::ERR_CONNECTION_TIMED_OUT |
API недоступен | Проверить API Gateway stage |
Debugging в браузере
// Откройте DevTools (F12) → Console → Network
// 1. Проверьте, что запрос уходит
// Network tab: должен быть запрос к вашему API URL
// 2. Проверьте CORS
// Console: если видите "CORS policy" ошибку:
// "Access to fetch at 'https://api...' from origin 'https://resume...'
// has been blocked by CORS policy"
// 3. Проверьте ответ
// Network tab → click request → Response tab
// Должен быть JSON: {"count": 42}
// 4. Проверьте заголовки ответа
// Network tab → click request → Headers tab
// Должен быть: access-control-allow-origin: https://resume.ivanpetrov.com
Smoke Testing
Что такое smoke test
Smoke test -- это минимальная проверка, что всё работает. Для CRC:
- Фронтенд загружается
- API отвечает
- Счётчик обновляется
- HTTPS работает
Автоматический smoke test
#!/bin/bash
# smoke-test.sh
FRONTEND_URL="https://resume.ivanpetrov.com"
API_URL="https://api.ivanpetrov.com/count"
echo "=== CRC Smoke Test ==="
# Test 1: Frontend loads
echo -n "Frontend... "
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$FRONTEND_URL")
if [ "$STATUS" -eq 200 ]; then
echo "OK (HTTP $STATUS)"
else
echo "FAIL (HTTP $STATUS)"
exit 1
fi
# Test 2: API responds
echo -n "API... "
RESPONSE=$(curl -s -w "\n%{http_code}" "$API_URL")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -1)
if [ "$HTTP_CODE" -eq 200 ]; then
echo "OK (HTTP $HTTP_CODE)"
echo " Response: $BODY"
else
echo "FAIL (HTTP $HTTP_CODE)"
exit 1
fi
# Test 3: Response has count
echo -n "Count field... "
COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['count'])" 2>/dev/null)
if [ -n "$COUNT" ]; then
echo "OK (count=$COUNT)"
else
echo "FAIL (no count in response)"
exit 1
fi
# Test 4: HTTPS
echo -n "HTTPS... "
CERT=$(curl -s -o /dev/null -w "%{ssl_verify_result}" "$FRONTEND_URL")
if [ "$CERT" -eq 0 ]; then
echo "OK (valid certificate)"
else
echo "FAIL (certificate issue)"
exit 1
fi
# Test 5: CORS headers
echo -n "CORS... "
CORS=$(curl -s -H "Origin: $FRONTEND_URL" -I "$API_URL" | grep -i "access-control-allow-origin")
if [ -n "$CORS" ]; then
echo "OK"
else
echo "WARN (no CORS headers visible)"
fi
echo "=== All checks passed ==="
Порядок деплоя
Правильный порядок
- Бэкенд первый -- деплоите Lambda, DynamoDB, API Gateway
- Тестируете API -- curl к эндпоинту, убедитесь что работает
- Фронтенд второй -- деплоите HTML/CSS/JS с правильным API URL
- Smoke test -- проверяете всё вместе
Частая ошибка: захардкоженный URL
// ПЛОХО: хардкод URL для разработки
const API_URL = 'http://localhost:3000/count';
// ХОРОШО: URL зависит от окружения
const API_URL = window.location.hostname === 'localhost'
? 'http://localhost:3000/count'
: 'https://api.ivanpetrov.com/count';
Ещё лучше -- вынести URL в конфигурацию, которая подставляется при деплое.