Загрузка по запросу и резервный интерфейс
Оглавление серии «Форма» · Урок 9 из 10.
Карточка уже умеет выбирать и передавать комплектацию. Но до сих пор Three.js импортировалась сразу при открытии страницы. Посетителю, который хочет только прочитать свойства или посмотреть артикул, библиотека и модель могут не понадобиться. В этом уроке разделим обычный интерфейс и 3D-модуль. Ожидаемый результат — форма появляется независимо от объёмного представления, а кнопка «Показать 3D» запускает его загрузку. Ошибка загрузки оставляет параметры, стоимость и ссылку доступными.
Ленивая загрузка означает перенос работы на конкретную границу действия. Она не делает библиотеку меньше автоматически и не обещает определённый выигрыш на любом устройстве. Если все посетители сразу нажимают кнопку, суммарные ресурсы останутся близкими к прежним. Но начальный сценарий чтения получит другой набор зависимостей. Сначала приходит HTML и небольшой модуль формы; тяжёлая часть становится необязательной для тех, кому она не нужна.
Два модуля и один источник состояния
Разделим код на main.js и viewer.js. Первый отвечает за форму, восстановление конфигурации, артикул, сумму и ссылку. Второй создаёт renderer, сцену, камеру, загрузчик модели и управление указателем. Viewer не читает самостоятельно localStorage и не вычисляет цену. Он получает конфигурацию от интерфейса и отображает её. Такой контракт уменьшает число независимых источников состояния: изображение не должно восстанавливать другой выбор из того же браузера.
main.js сохраняет актуальную конфигурацию независимо от наличия viewer. Пользователь может выбрать цвет до нажатия кнопки и продолжить менять параметры, пока библиотека загружается. На границе создания передаём последнее значение. После появления viewer функция update вызывает его apply. Если модель ещё не готова, apply запоминает состояние и применит его после загрузки. Два асинхронных этапа — импорт модуля и загрузка модели — не должны терять действие формы между ними.
Ниже полный модуль интерфейса девятого состояния. Тяжёлые импорты находятся внутри отдельного файла, а здесь используется динамический import. Пакетный сборщик может разделить зависимости на отдельную часть артефакта. При самостоятельной подготовке приложения нужно выпускать все созданные части вместе. Наличие одного HTML без соответствующих файлов не является полноценным результатом сборки: динамический запрос обратится к ним только после действия пользователя.
import { defaults, normalize, quote } from './config.js';
import {restore,remember,shareUrl} from './storage.js';
const restored=restore();let config=restored.config;
const host=document.querySelector('#viewport'),status=document.querySelector('#status');let active=null;
let generation=0;
const onSelect=role=>document.querySelector('#selected').textContent=`Выбрана деталь: ${role}`;
function update() {
for(const key of ['shade','base','size']) {const input=document.querySelector('#'+key);if(input)input.value=config[key];}
if(active)active.apply(config);
const {sku,price}=quote(config);document.querySelector('#summary').textContent=`Артикул: ${sku}\nУчебная стоимость: ${price} ₽. Оформление заказа не реализовано.`;
document.querySelector('#share').value=shareUrl(config);
}
for(const key of ['shade','base','size']) {
const input=document.querySelector('#'+key);if(!input)continue;
input.addEventListener('change',()=>{
const candidate=normalize({...config,[key]:input.value});if(!candidate)return;
config=candidate;update();
if(!remember(config))document.querySelector('#status').textContent='Выбор применён; локальное сохранение недоступно.';
});}
for(const button of document.querySelectorAll('[data-part]'))button.addEventListener('click',()=>onSelect(button.dataset.part));
update();
status.textContent=restored.message+' 3D загружается по кнопке.';
document.querySelector('#reset').addEventListener('click',()=>active?.reset());
async function start() {
if(active)return;const token=++generation;const button=document.querySelector('#start');button.disabled=true;
status.textContent='Загружаем 3D-модуль';
try {
const {createViewer}=await import('./viewer.js');if(token!==generation)return;
active=createViewer(host,{onStatus:value=>{if(token===generation)status.textContent=value;},onSelect},config);
await active.ready;
} catch(error) {
if(token===generation){active?.dispose();active=null;status.textContent=`3D недоступно: ${error.name}. Комплектация доступна в форме.`;}
} finally {if(token===generation)button.disabled=false;}
}
document.querySelector('#start').addEventListener('click',start);
Готовность состоит из нескольких состояний
После нажатия кнопка временно отключается, а сообщение говорит о загрузке 3D-модуля. Затем создаётся viewer, возвращающий объект с ready, apply, reset и dispose. ready описывает готовность модели. Он не равен готовности всего DOM: форма к этому моменту давно существует. Разделение позволяет сообщать точную стадию и не выдавать начало сетевого запроса за уже появившийся товар. Если Promise завершится ошибкой, интерфейс сможет очистить созданное представление.
Переменная active хранит текущее представление. Повторное нажатие после успешного создания не должно добавлять второй canvas и запускать второй кадровый цикл. В начале start проверяем наличие активного экземпляра. Пока идёт импорт, кнопку также блокируем. Это небольшая модель одного представления. Для галереи нескольких товаров потребуется отдельный жизненный цикл каждого экземпляра, а не общий глобальный признак, который случайно относится ко всем карточкам.
В коде введён номер поколения загрузки. Он сравнивается после ожидания импорта и в обработчиках результата. В девятом шаге интерфейс ещё не предлагает явное закрытие, но этот номер подготовит безопасное завершение следующего урока. Асинхронная функция может продолжить работу позже, когда пользователь уже сменил намерение. Проверка поколения отвечает на вопрос, относится ли её результат к текущему запросу. Она не отменяет сетевой запрос физически, но позволяет отказаться от устаревшего продолжения.
Обычная форма при отсутствии WebGL
Создание WebGLRenderer тоже может завершиться ошибкой, даже когда JavaScript-модуль загрузился. Причина может относиться к поддержке устройства, настройкам браузера или доступным ресурсам. Обработчик catch показывает сообщение и сохраняет HTML-форму. Не нужно скрывать артикул только потому, что посетитель не увидел 3D. Комплектация — предметные данные, а canvas — один из способов их представления. Это разделение было заложено ещё до динамического импорта.
В коде не добавлен обязательный таймер, который объявляет любую медленную загрузку ошибкой через короткий интервал. Для реального интерфейса полезны промежуточные сообщения и политика повторной попытки. Однако нельзя обещать процент загрузки, если у нас нет достоверного общего объёма ресурсов. Модель может ссылаться на дополнительные буферы и текстуры. В текущем небольшом ресурсе данных немного, но даже здесь текст «загружаем» честнее искусственной шкалы с выдуманным прогрессом.
Кнопка возврата ракурса вызывает метод только существующего viewer через безопасную проверку. До его создания она не меняет параметры формы. В более завершённом интерфейсе такую кнопку можно скрывать или отключать до готовности, чтобы назначение команды было яснее. В нашей лаборатории важно увидеть границу: команда представления не создаёт 3D автоматически и не подменяет действие «Показать 3D». У каждой кнопки остаётся конкретный понятный результат.
Доставка зависит от сборки
В режиме разработки Vite обслуживает модули отдельно, а производственная упаковка устроена иначе. Нельзя измерять эффект разделения только по числу запросов сервера разработки и переносить результат на опубликованный сайт. В этой серии не проводились сборка учебного приложения и замеры его загрузки. Мы показываем архитектуру запроса и зависимости. Для последующей публикации понадобится отдельно собрать приложение, изучить полученные части и измерить реальные пользовательские сценарии.
Динамический импорт не означает загрузку библиотеки из стороннего непроверенного адреса. Viewer остаётся собственным модулем проекта, а зависимости закреплены в package.json. Нет необходимости подставлять URL из параметров страницы в import. Такой механизм создал бы другую границу доверия, не связанную с задачей карточки. Известный локальный путь сохраняет понятную связь между исходниками, выпуском и запросом, который инициирует кнопка.
Ожидаемое наблюдение
Самостоятельно откройте страницу и сначала поменяйте плафон на зелёный, не включая 3D. Резюме и ссылка должны измениться сразу. После нажатия кнопки появившаяся лампа должна получить именно этот выбор. Если изменить поле во время загрузки модели, ожидается последний вариант, а не значение начала запроса. При временном неверном пути модели форма должна остаться доступной, сообщение — объяснить ошибку, а кнопка — снова позволить попытку.
Синтаксис загрузки описан в динамическом import, структура сборки — в документации Vite. Сейчас появление представления имеет явную границу и возвращает управляемый экземпляр. В следующем уроке сделаем обратную операцию полноценной: закрытие 3D должно остановить кадры, отключить события и освободить ресурсы, включая модель, которая могла завершить загрузку уже после закрытия.
Исходники этого шага
Архив серии содержит полную папку lesson-09. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.