Одностраничные приложения (Single Page Application, SPA) имеют принципиально иную архитектуру по сравнению с традиционными многостраничными сайтами. Контент в них динамически обновляется без полной перезагрузки страницы, что напрямую влияет на логику работы интерактивных туров в Intro.js.
Ключевые сложности:
В таких условиях стандартная инициализация тура становится недостаточной — требуется дополнительное управление жизненным циклом шагов.
В SPA переход между страницами реализуется через роутер (например, в React Router, Vue Router). Это означает, что:
Пример конфигурации:
const intro = introJs();
intro.setOptions({
steps: [
{
element: '#home-title',
intro: 'Главная страница'
},
{
element: '#profile-button',
intro: 'Переход к профилю'
},
{
element: '#profile-info',
intro: 'Информация о пользователе'
}
]
});
Проблема: элемент #profile-info отсутствует в DOM, пока
не выполнен переход на соответствующий маршрут.
Для решения используется обработчик события смены шага:
intro.onbeforechange(function(targetElement) {
const step = intro._currentStep;
if (step === 2) {
router.push('/profile');
}
});
Особенности:
В SPA элементы часто появляются с задержкой (например, после запроса к API). Intro.js не умеет автоматически ожидать DOM-элементы, поэтому требуется дополнительная логика.
function waitForElement(selector, callback) {
const interval = setInterval(() => {
const element = document.querySelector(selector);
if (element) {
clearInterval(interval);
callback(element);
}
}, 100);
}
Использование:
intro.onbeforechange(function() {
if (intro._currentStep === 2) {
router.push('/profile');
waitForElement('#profile-info', () => {
intro.refresh();
});
}
});
Более эффективный способ — отслеживание изменений DOM:
function observeElement(selector, callback) {
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
callback(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
После динамического изменения DOM необходимо пересчитать позицию подсказки:
intro.refresh();
Это критично в следующих случаях:
В SPA часто удобнее разбивать тур на несколько независимых:
Пример:
const homeTour = introJs().setOptions({
steps: [
{ element: '#home-title', intro: 'Главная' }
]
});
const profileTour = introJs().setOptions({
steps: [
{ element: '#profile-info', intro: 'Профиль' }
]
});
Запуск:
homeTour.start();
// позже
profileTour.start();
В SPA важна связь тура с состоянием (state management):
Пример с глобальным состоянием:
store.subscribe(() => {
const state = store.getState();
if (state.userLoaded) {
intro.refresh();
}
});
В SPA некоторые элементы могут быть:
Intro.js не пропускает шаги автоматически, поэтому требуется ручная проверка:
function isElementVisible(selector) {
const el = document.querySelector(selector);
return el && el.offsetParent !== null;
}
Фильтрация шагов:
const steps = [
{ element: '#a', intro: 'A' },
{ element: '#b', intro: 'B' }
];
const filteredSteps = steps.filter(step =>
isElementVisible(step.element)
);
intro.setOptions({ steps: filteredSteps });
Частая ситуация — шаг зависит от данных API.
Решение:
fetch('/api/user')
.then(res => res.json())
.then(data => {
renderUser(data);
intro.start();
});
В SPA важно корректно завершать тур:
intro.onexit(() => {
console.log('Тур завершён');
});
Также полезно сохранять прогресс:
intro.oncomplete(() => {
localStorage.setItem('tourCompleted', 'true');
});
Проверка перед запуском:
if (!localStorage.getItem('tourCompleted')) {
intro.start();
}
В SPA компоненты могут размонтироваться и монтироваться заново. Поэтому:
function createTour() {
return introJs().setOptions({
steps: [...]
});
}
let intro = createTour();
Особенности:
useEffect.useEffect(() => {
const intro = introJs();
intro.setOptions({
steps: [...]
});
intro.start();
return () => intro.exit();
}, []);
Использование в lifecycle:
mounted() {
this.$nextTick(() => {
const intro = introJs();
intro.start();
});
}
Использование в ngAfterViewInit:
ngAfterViewInit() {
const intro = introJs();
intro.start();
}
Типичные проблемы:
Защита:
intro.onbeforechange(function(targetElement) {
if (!targetElement) {
intro.nextStep();
}
});
В SPA с большим количеством компонентов:
refresh();Рекомендации:
Особенности взаимодействия:
Практики:
intro.setOptions({
disableInteraction: true
});
Полезные методы:
intro.goToStep(2);
intro.exit();
intro.refresh();
Логирование:
intro.onchange(function(element) {
console.log('Текущий шаг:', intro._currentStep);
});
Intro.js можно комбинировать с:
Пример интеграции с аналитикой:
intro.onchange(() => {
analytics.track('Tour Step', {
step: intro._currentStep
});
});
Сохранение текущего шага:
intro.onchange(() => {
localStorage.setItem('tourStep', intro._currentStep);
});
Восстановление:
const savedStep = localStorage.getItem('tourStep');
if (savedStep) {
intro.goToStepNumber(parseInt(savedStep) + 1).start();
}
Дополнительные настройки:
intro.setOptions({
scrollToElement: true,
scrollPadding: 50,
showStepNumbers: true,
exitOnOverlayClick: false
});
intro.onbeforechange(() => {
if (intro._currentStep === 1) {
openModal();
}
});
После открытия:
setTimeout(() => {
intro.refresh();
}, 300);
intro.onbeforechange(() => {
if (intro._currentStep === 2) {
activateTab('settings');
}
});
Для крупных SPA рекомендуется:
Пример структуры:
/tours
homeTour.js
profileTour.js
/services
tourService.js