如何优雅地封装一个漫游式引导组件
背景
最近接到一个需求,要为用户提供产品功能引导,介绍产品特性和使用方法。起初考虑使用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组件则简化了引导元素的标记过程。这种设计既保证了引导逻辑与业务代码的解耦,又提供了良好的使用体验。
待优化问题
- 目前只支持点击关闭引导,后续可以考虑增加其他关闭方式
startDriver、openSimpleTour调用还是在业务组件中,没法完全解耦