Загрузка JSON через fetch
В предыдущем уроке данные проходили через Promise, но источник оставался внутри программы. Теперь вынесем четыре записи каталога в JSON-файл. Программа будет отдельно получать ответ сервера, читать его содержимое и проверять, подходит ли оно нашей модели. После этого знакомая функция представления создаст карточки.
Продолжаем catalog-lab с модулями из урока 16. Потребуются validateCourses, selectCourses и countLessons из js/model.js, а также renderCourses из js/view.js. Они сохраняют назначение: модель проверяет и выбирает записи, представление создаёт DOM. Код этого урока добавляет источник данных, не меняя структуру карточки. Приведённые результаты ожидаемые; HTTP-сервер и учебное приложение при подготовке текста не запускались.
Файл данных и его адрес
Создайте папку data рядом с index.html, а в ней courses.json. Это весь файл; он содержит те же четыре записи и тот же порядок, что прежний js/data.js:
[
{"id":"javascript","title":"Современный JavaScript","topic":"frontend","lessons":20,"url":"./courses/javascript.html"},
{"id":"html-css","title":"HTML и CSS","topic":"frontend","lessons":16,"url":"./courses/html-css.html"},
{"id":"performance","title":"Производительность сайта","topic":"frontend","lessons":12,"url":"./courses/performance.html"},
{"id":"markdown","title":"Статический сайт из Markdown","topic":"publishing","lessons":12,"url":"./courses/markdown.html"}
]
У JSON другой синтаксический договор: имена свойств и строки заключены в двойные кавычки, комментариев и завершающей запятой после последнего элемента здесь нет. В файле не пишут export, потому что это данные, а не модуль JavaScript. Число уроков остаётся числом: кавычки вокруг 20 превратили бы его в строку и нарушили правило модели.
Покажите проект по HTTP. Из папки catalog-lab читатель может запустить локальный сервер следующей командой:
python3 -m http.server 8000 --bind 127.0.0.1
Адрес документа будет http://127.0.0.1:8000/. Запрос к ./data/courses.json в нашем коде вычисляется относительно адреса документа. Он не становится автоматически относительным к месту хранения js/api.js. Поэтому ожидаем путь /data/courses.json, а не /js/data/courses.json. Такая проверка помогает отличить ошибку размещения от ошибки содержимого.
Открытие модулей и запросов через file:// имеет браузерные ограничения и не представляет тот же сценарий. Loopback-сервер даёт понятный HTTP-обмен внутри вашего компьютера. Он служит учебному просмотру, а не инструкции размещения действующего сайта. В нашем примере файл и документ имеют одинаковый origin; настройка доступа к чужому API здесь не требуется.
Два этапа получения ответа
Создайте js/api.js. Следующий листинг — весь первоначальный модуль загрузки, написанный через цепочку из прошлого урока:
import { validateCourses } from './model.js';
export function loadCourses(url = './data/courses.json') {
return fetch(url, { cache: 'no-store' })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(value => validateCourses(value));
}
Вызов fetch возвращает Promise. При получении ответа обработчику передаётся объект Response, а не готовый массив. Сетевой сбой может отклонить обещание, но HTTP-ответ с кодом ошибки всё равно представляет полученный ответ. Поэтому отдельно проверяем response.ok; его значение истинно для диапазона 200–299. Назначение свойства описано в справочнике Response.ok.
Когда файл отсутствует и сервер возвращает 404, наша проверка создаёт исключение с кодом. Следующий этап разбора JSON не выполняется. Это существенно: многие серверы возвращают для ошибки HTML-страницу. Без проверки статуса программа попыталась бы прочитать её как JSON, и читатель увидел бы сообщение о синтаксисе вместо понятной причины отсутствия файла.
response.json() читает тело ответа и разбирает его, также возвращая Promise. Мы возвращаем его из обработчика then, поэтому следующий этап ждёт именно разобранное значение. Если забыть return, в validateCourses попадёт undefined, а операция чтения тела окажется отделена от основной цепочки. Контракт чтения тела описан в справочнике Response.json.
Тело ответа обычно потребляется один раз. Не добавляйте подряд response.text() для диагностики и response.json() для основной обработки к тому же объекту. Для знакомства с форматом можно открыть сам JSON в браузере или смотреть вкладку Network. В нашей программе требуется один разбор; дополнительные копии ответа пока не нужны.
Граница между JSON и курсами
Разобранный JSON ещё не гарантирует массив подходящих карточек. Успешно читается и объект {"message":"hello"}, и массив записей с неверными полями. Поэтому последний обработчик передаёт результат в validateCourses из урока 16. Проверка синтаксиса отвечает на вопрос о формате, модель — на вопрос о смысле данных.
Модель требует массив, объект каждой записи, непустое название, уникальный допустимый id, известную тему, положительное целое lessons и относительный путь к учебной странице. Она возвращает новые объекты с нужными полями. Именно эти проверенные записи должны попасть в представление. Проверка URL ограничивает наш учебный контракт; она не является универсальной политикой для любого внешнего каталога.
Временно замените весь js/app.js следующим вариантом. Форма уже существует в документе, но её группа полей на этом шаге отключается: сначала разбираем загрузку без смешивания с новым управлением состоянием.
import { loadCourses } from './api.js';
import { countLessons } from './model.js';
import { renderCourses } from './view.js';
const list = document.querySelector('#catalog-list');
const status = document.querySelector('#catalog-status');
const fieldset = document.querySelector('#catalog-controls fieldset');
if (!list || !status) throw new Error('Не найден список или статус');
if (fieldset) fieldset.disabled = true;
status.textContent = 'Загружаем каталог…';
loadCourses()
.then(courses => {
renderCourses(list, courses);
status.textContent = `Курсов: ${courses.length}. Уроков: ${countLessons(courses)}.`;
})
.catch(error => {
renderCourses(list, []);
status.textContent = 'Каталог не удалось загрузить.';
console.error('Ошибка загрузки:', error);
});
При успешной операции ожидаются четыре карточки и сообщение Курсов: 4. Уроков: 60.. Число 60 получается из наших исходных записей, а не из статистики ProfessorWeb. Сообщение загрузки может отображаться очень коротко на локальном компьютере; его отсутствие на глаз не доказывает, что этап пропущен.
Параметр cache: 'no-store' выбран для нашего учебного источника: запрос не использует прежний ответ из HTTP-кеша браузера и не сохраняет новый в этот кеш. Благодаря этому изменение JSON читателем относится к следующей попытке получения файла, а не скрывается за свежей сохранённой копией. Это настройка конкретного упражнения; в рабочем каталоге кеширование нужно проектировать вместе с обновлением данных. Режимы описаны в справочнике Request.cache.
Для ручного разбора читатель может по одному изменить условия: переименовать JSON-файл, испортить запятую или заменить число уроков строкой. В первом случае ожидается HTTP-ошибка, во втором — ошибка разбора, в третьем — ошибка модели. Точное сообщение браузера может различаться, но пользовательский статус остаётся понятным. После каждого изменения верните данные в исходное состояние, чтобы причины не смешивались.
Откуда берётся адрес источника
У нашего JSON нет собственного сервера приложений: обычный HTTP-сервер отдаёт сохранённый файл. Это достаточная модель для изучения получения данных. Если впоследствии файл начнёт формировать API, договор вызывающего кода может сохраниться: успешный ответ всё ещё должен содержать массив с согласованными полями. Изменится источник, а проверка данных останется границей между внешним ответом и каталогом.
Перенос страницы в подпапку способен изменить относительный адрес ./data/courses.json. Поэтому при разборе ошибки сначала посмотрите итоговый URL запроса в Network. Файл может существовать в проекте, но запрос уйти по другому пути. Не исправляйте такую проблему отключением проверки статуса: HTML-страница ошибки не станет JSON оттого, что программа проигнорировала response.ok. Аналогично, размещение файла на другом домене добавляет правила доступа между источниками. Для этого первого примера все файлы остаются в одном origin; решение для внешнего API потребует отдельно проверить его условия доступа.
Важное ограничение: здесь нет автоматического повтора, заданного таймаута и отмены запроса. Мы используем один известный источник. Общая модель работы Fetch API и сетевых отказов приведена в руководстве MDN. Следующий урок сохранит этот договор загрузки, но выразит зависимые шаги через async и await.