Инкапсуляция логики тура в отдельный Vue-компонент позволяет централизовать управление шагами, состоянием и жизненным циклом. Такой компонент может быть переиспользуемым, конфигурируемым и изолированным от остального интерфейса.
Основные задачи компонента:
Shepherd.TourКомпонент можно реализовать как обычный .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');
});
Во 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();
});
}
}
При использовании маршрутизации важно учитывать смену страниц:
watch: {
$route() {
if (this.tour) {
this.tour.cancel();
}
}
}
Для шагов, требующих перехода:
{
id: 'route-step',
text: 'Переход на страницу настроек',
buttons: [
{
text: 'Перейти',
action: () => {
this.$router.push('/settings');
return this.tour.next();
}
}
]
}
Вынос логики в 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;
}
Если элемент не найден:
{
id: 'safe-step',
text: 'Попытка привязки',
attachTo: {
element: '.maybe-exists',
on: 'bottom'
},
when: {
show() {
const el = document.querySelector('.maybe-exists');
if (!el) {
this.next();
}
}
}
}
Изоляция логики
Повторное использование
Производительность
beforeShowPromiseUX
Компонент можно сделать более гибким:
<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 из простого инструмента подсказок в полноценную систему сценариев обучения внутри интерфейса с высокой степенью контроля, масштабируемости и повторного использования.