Создание Vue-компонента тура

Инкапсуляция логики тура в отдельный Vue-компонент позволяет централизовать управление шагами, состоянием и жизненным циклом. Такой компонент может быть переиспользуемым, конфигурируемым и изолированным от остального интерфейса.

Основные задачи компонента:

  • инициализация экземпляра Shepherd.Tour
  • декларативное описание шагов
  • синхронизация с жизненным циклом Vue
  • управление запуском и завершением тура
  • реакция на изменения DOM и маршрутов

Базовая структура компонента

Компонент можно реализовать как обычный .vue файл:

<template>
  <div>
    <slot />
  </div>
</template>

<script>
import Shepherd from 'shepherd.js';
import 'shepherd.js/dist/css/shepherd.css';

export default {
  name: 'AppTour',

  props: {
    steps: {
      type: Array,
      required: true
    },
    options: {
      type: Object,
      default: () => ({})
    },
    autoStart: {
      type: Boolean,
      default: false
    }
  },

  data() {
    return {
      tour: null
    };
  },

  mounted() {
    this.initTour();

    if (this.autoStart) {
      this.start();
    }
  },

  beforeUnmount() {
    this.destroyTour();
  },

  methods: {
    initTour() {
      this.tour = new Shepherd.Tour({
        useModalOverlay: true,
        defaultStepOptions: {
          cancelIcon: {
            enabled: true
          },
          scrollTo: true
        },
        ...this.options
      });

      this.steps.forEach(step => {
        this.tour.addStep(step);
      });
    },

    start() {
      if (this.tour) {
        this.tour.start();
      }
    },

    destroyTour() {
      if (this.tour) {
        this.tour.cancel();
        this.tour = null;
      }
    }
  }
};
</script>

Декларативное описание шагов

Передача шагов через props позволяет гибко управлять туром извне:

const steps = [
  {
    id: 'step-1',
    text: 'Это кнопка создания',
    attachTo: {
      element: '.create-btn',
      on: 'bottom'
    },
    buttons: [
      {
        text: 'Далее',
        action: function () {
          return this.next();
        }
      }
    ]
  },
  {
    id: 'step-2',
    text: 'Это список элементов',
    attachTo: {
      element: '.list',
      on: 'top'
    },
    buttons: [
      {
        text: 'Назад',
        action: function () {
          return this.back();
        }
      },
      {
        text: 'Завершить',
        action: function () {
          return this.complete();
        }
      }
    ]
  }
];

Использование компонента

<AppTour :steps="tourSteps" :autoStart="true">
  <MainPage />
</AppTour>

Реактивное управление туром

Для более глубокой интеграции полезно добавить реактивные методы управления:

methods: {
  next() {
    this.tour?.next();
  },
  back() {
    this.tour?.back();
  },
  cancel() {
    this.tour?.cancel();
  }
}

Также можно пробрасывать события наружу:

this.tour.on('complete', () => {
  this.$emit('completed');
});

this.tour.on('cancel', () => {
  this.$emit('cancelled');
});

Работа с асинхронным DOM

Во Vue элементы могут появляться не сразу. Shepherd требует, чтобы attachTo.element существовал в момент показа шага.

Решение — использовать beforeShowPromise:

{
  id: 'async-step',
  text: 'Асинхронный элемент',
  attachTo: {
    element: '.dynamic-element',
    on: 'right'
  },
  beforeShowPromise() {
    return new Promise(resolve => {
      const check = () => {
        const el = document.querySelector('.dynamic-element');
        if (el) {
          resolve();
        } else {
          requestAnimationFrame(check);
        }
      };
      check();
    });
  }
}

Интеграция с Vue Router

При использовании маршрутизации важно учитывать смену страниц:

watch: {
  $route() {
    if (this.tour) {
      this.tour.cancel();
    }
  }
}

Для шагов, требующих перехода:

{
  id: 'route-step',
  text: 'Переход на страницу настроек',
  buttons: [
    {
      text: 'Перейти',
      action: () => {
        this.$router.push('/settings');
        return this.tour.next();
      }
    }
  ]
}

Управление состоянием через composable (Vue 3)

Вынос логики в composable делает компонент легче:

import { ref, onMounted, onBeforeUnmount } from 'vue';
import Shepherd from 'shepherd.js';

export function useTour(steps, options = {}) {
  const tour = ref(null);

  const init = () => {
    tour.value = new Shepherd.Tour(options);
    steps.forEach(step => tour.value.addStep(step));
  };

  const start = () => tour.value?.start();
  const cancel = () => tour.value?.cancel();

  onMounted(init);
  onBeforeUnmount(cancel);

  return { tour, start, cancel };
}

Глобальный сервис тура

Для сложных приложений удобно создать единый сервис:

class TourService {
  constructor() {
    this.tour = null;
  }

  create(steps, options = {}) {
    this.tour = new Shepherd.Tour(options);
    steps.forEach(step => this.tour.addStep(step));
  }

  start() {
    this.tour?.start();
  }

  cancel() {
    this.tour?.cancel();
  }
}

export default new TourService();

Использование:

import TourService from '@/services/tour';

TourService.create(steps);
TourService.start();

Динамическая генерация шагов

Шаги можно строить на основе состояния приложения:

computed: {
  tourSteps() {
    const steps = [];

    if (this.user.isAdmin) {
      steps.push({
        id: 'admin-panel',
        text: 'Панель администратора',
        attachTo: {
          element: '.admin',
          on: 'left'
        }
      });
    }

    return steps;
  }
}

Кастомизация внешнего вида

Через классы:

defaultStepOptions: {
  classes: 'custom-shepherd-theme'
}

CSS:

.custom-shepherd-theme {
  background: #1e1e2f;
  color: #fff;
  border-radius: 8px;
}

Обработка ошибок и fallback

Если элемент не найден:

{
  id: 'safe-step',
  text: 'Попытка привязки',
  attachTo: {
    element: '.maybe-exists',
    on: 'bottom'
  },
  when: {
    show() {
      const el = document.querySelector('.maybe-exists');
      if (!el) {
        this.next();
      }
    }
  }
}

Оптимизация и best practices

Изоляция логики

  • не смешивать бизнес-логику и шаги тура
  • использовать отдельные конфигурационные файлы

Повторное использование

  • группировать шаги по сценариям
  • создавать фабрики шагов

Производительность

  • избегать тяжелых beforeShowPromise
  • минимизировать DOM-поиск

UX

  • ограничивать длину текста
  • использовать последовательные шаги
  • учитывать мобильные устройства

Расширение через слоты

Компонент можно сделать более гибким:

<template>
  <div>
    <slot :start="start" :cancel="cancel"></slot>
  </div>
</template>

Использование:

<AppTour :steps="steps">
  <template #default="{ start }">
    <button @click="start">Начать тур</button>
  </template>
</AppTour>

Ленивая инициализация

Иногда тур не должен создаваться сразу:

methods: {
  ensureTour() {
    if (!this.tour) {
      this.initTour();
    }
  },
  start() {
    this.ensureTour();
    this.tour.start();
  }
}

Хранение состояния прохождения

Пример с localStorage:

mounted() {
  const completed = localStorage.getItem('tour-completed');

  if (!completed) {
    this.start();
  }

  this.tour.on('complete', () => {
    localStorage.setItem('tour-completed', 'true');
  });
}

Поддержка нескольких туров

Компонент может принимать идентификатор:

props: {
  tourId: {
    type: String,
    required: true
  }
}

И хранить состояние отдельно:

localStorage.setItem(`tour-${this.tourId}`, 'done');

Глубокая кастомизация шагов

Использование функций внутри шагов:

{
  id: 'dynamic-text',
  text: () => {
    return `Текущий пользователь: ${store.user.name}`;
  }
}

Контроль прокрутки и позиционирования

scrollTo: {
  beh * avior: 'smooth',
  block: 'center'
}

Отладка

Полезно включать логирование:

this.tour.on('show', step => {
  console.log('Показ шага:', step.id);
});

Такая организация Vue-компонента позволяет превратить Shepherd.js из простого инструмента подсказок в полноценную систему сценариев обучения внутри интерфейса с высокой степенью контроля, масштабируемости и повторного использования.