如何优雅地封装一个漫游式引导

613 阅读2分钟

如何优雅地封装一个漫游式引导组件

背景

最近接到一个需求,要为用户提供产品功能引导,介绍产品特性和使用方法。起初考虑使用el-tour,但发现它功能较为简单,主要适用于单页面场景,多页面使用时存在一些bug(不同分辨率定位不准确、tour框和dialog同时使用展示不正常)。此外,el-tour难以实现引导与系统的完全解耦。因此,我决定自己封装一个更灵活的引导组件。

技术选型

经过调研,我选择了driver.js作为基础库。它提供了丰富的功能和良好的可定制性,非常适合实现复杂的引导需求。

实现方案

1. useTour.ts

这个自定义hook封装了driver.js的核心功能,提供了以下主要特性:

  • 多步骤引导
  • 单步引导
import { driver, type Config, type Driver, type Side } from "driver.js";

export enum TourType {
    CreateTopic = 'CreateTopic',
    CreateReport = 'CreateReport',
}

export const useTour = () => {
    // 预定义引导步骤配置
    const defaultData = new Map([
        [TourType.CreateTopic, [
            {
                target: '#test-step-1',
                title: '创建主题 - 步骤1',
                description: '这是创建主题的第一个步骤。',
                placement: 'right'
            },
            {
                target: '#test-step-2',
                title: '创建主题 - 步骤2',
                description: '这是创建主题的第二个步骤。',
                placement: 'bottom'
            }
        ]],
        [TourType.CreateReport, [
            {
                target: '#test-step-1',
                title: '创建报告 - 步骤1',
                description: '这是创建报告的第一个步骤。',
                placement: 'right'
            },
            {
                target: '#test-step-2',
                title: '创建报告 - 步骤2',
                description: '这是创建报告的第二个步骤。',
                placement: 'bottom'
            }
        ]]
    ]);

    const toursMap = ref<Map<TourType, {
        target: string,
        title: string,
        description: string
        placement?: any
    }[]>>(defaultData);
    const driverInstance = shallowRef<Driver | null>(null);
    const simpleDriverInstance = shallowRef<Driver | null>(null);

    // 启动多步骤引导
    const startDriver = (type: TourType, config: Partial<Config> = {
        showProgress: true,
        nextBtnText: '下一步',
        prevBtnText: '上一步',
        doneBtnText: '完成',
    }) => {
        driverInstance.value = driver({
            ...config,
            steps: toursMap.value.get(type)!.map(tour => ({
                element: tour.target,
                popover: {
                    title: tour.title,
                    description: tour.description,
                    side: tour.placement
                }
            })) || []
        });
        driverInstance.value?.drive(0);
    };

    // 打开单步引导
    const openSimpleTour = (
        data: Partial<{
            id: string,
            title: string,
            description: string,
            placement: Side
        }>,
        config?: Partial<Config>
    ) => {
        const { id, title, description, placement } = data;
        simpleDriverInstance.value = driver(config);
        simpleDriverInstance.value?.highlight({
            element: id,
            popover: {
                title,
                description,
                side: placement || 'bottom'
            },
        });
    };

    // 关闭单步引导
    const closeSimpleTour = () => {
        simpleDriverInstance.value?.destroy();
    };

    return {
        startDriver,
        openSimpleTour,
        closeSimpleTour
    };
};

2. TourStep.vue

这个组件作为引导目标元素的包装器,具有以下特点:

  • 通过插槽机制为目标元素添加唯一标识,确保与业务逻辑低耦合
  • 灵活支持点击关闭引导等交互行为
<script lang="ts" setup>
import { useTour } from "@/hooks/useTour";

const { closeSimpleTour } = useTour();

const props = defineProps<{
  target: string;
  closeTrigger: "click";
}>();

const handleClick = () => {
  if (props.closeTrigger === "click") {
    closeSimpleTour();
  }
};
</script>

<template>
  <div :id="props.target" class="suwen-tour-step-container" @click="handleClick">
    <slot></slot>
  </div>
</template>

通过这种封装方式,我们可以轻松地在项目中实现灵活且可复用的引导功能。useTourhook提供了核心逻辑,而TourStep组件则简化了引导元素的标记过程。这种设计既保证了引导逻辑与业务代码的解耦,又提供了良好的使用体验。

待优化问题

  • 目前只支持点击关闭引导,后续可以考虑增加其他关闭方式
  • startDriveropenSimpleTour调用还是在业务组件中,没法完全解耦