
Driver.js 跨页面引导接入记录
给 Vue 项目接入新手引导时,我需要让用户从项目列表进入详情页,再返回列表继续操作。单页里的高亮和说明用 Driver.js 配置起来很方便,跨页面的步骤则需要配合路由跳转。
这套流程使用了预先准备的示例项目,新用户没有创建项目也能体验。接入过程中,我遇到了返回上一步时引导结束,以及列表尚未出现就开始高亮这两处问题。
点击上一步,引导却结束了
引导的第一步位于项目列表,点击“下一步”后会进入详情页,继续第二步。在详情页点击“上一步”,本来应该返回列表并重新高亮第一步的目标,实际却是页面回去了,引导也结束了。
我先检查了“上一步”的回调。它会等待路由返回列表,再调用当前引导实例的 movePrevious(),切回第一步:
onPrevClick: async () => {
await router.push('/demo/items')
nextTick(() => {
guide.movePrevious()
})
}
沿着这次页面跳转继续检查,我发现全局路由钩子 afterEach 也负责启动引导。只要处于演示模式,而且用户还没有看过,就调用 createGuide(router) 创建并配置一个 Driver.js 实例,再用 drive() 启动:
router.afterEach(() => {
nextTick(() => {
if (guideState.enabled && !guideState.completed) {
createGuide(router).drive()
}
})
})
从详情页返回列表时,用户仍在演示模式,guideState.completed 也还是 false,因此启动条件再次成立。一边是原实例准备执行 movePrevious(),另一边又创建了一个实例,从第一步开始引导。引导内部的一次页面切换,触发了重复启动。

针对这处重复创建,我调整了全局钩子的启动条件:从演示区外进入列表时才启动引导,演示区内部切换页面时继续使用原实例。
演示区内的页面都带有 meta.demo,可以通过目标路由 to 和来源路由 from 的标记区分这两种情况:
const startGuideAtEntry = () => {
if (router.currentRoute.value.path !== '/demo/items') return
if (!guideState.enabled || guideState.completed) return
createGuide(router).drive()
}
router.afterEach((to, from) => {
const toDemo = Boolean(to.meta.demo)
const fromDemo = Boolean(from.meta.demo)
if (!toDemo || fromDemo) return
nextTick(() => startGuideAtEntry())
})
这样,从详情页点击“上一步”返回列表时,fromDemo 为 true,全局钩子会跳过新建实例。返回列表后的步骤切换由原来的 guide.movePrevious() 继续处理。
首次直接打开演示页时,我则在应用的 onMounted 中等待 router.isReady(),再调用同一个启动检查。
等到列表项出现再启动
后来登录后的新用户也需要引导,同样从项目列表开始。列表数据是异步加载的,页面完成跳转时,第一步要高亮的项目条目可能还不存在。
原来的处理是固定等五秒,再创建登录后的引导实例:
setTimeout(() => {
createWelcomeGuide(router).drive()
}, 5000)
列表提前出现,用户仍要等满五秒;请求超过五秒,启动时又找不到目标。加一次 nextTick() 也不能保证列表出现,它等待的是 Vue 的 DOM 更新,接口请求未完成时还没有项目条目可以渲染。
我改成等待目标元素出现,最长十秒。示例项目被放在列表第一项,所以选择器指向对应列表的首个 li:
const initialUrl = router.currentRoute.value.fullPath
const element = await waitForTarget(
'.item-list > li:first-child',
{ timeoutMs: 10_000 },
)
if (!element) return
if (router.currentRoute.value.fullPath !== initialUrl) return
createWelcomeGuide(router).drive()
waitForTarget 是这次增加的 DOM 等待工具。它先用 querySelector() 查询,目标已经存在就返回;没有找到则用 MutationObserver 观察节点变化,直到目标出现或超时。找到后断开观察并清除计时器,十秒内始终没有找到就返回 null。
等待结束后,还要确认用户是否留在原页面。我曾把保存的地址与路由回调里的 to.fullPath 比较,但 to 属于那一次导航,用户后来离开也不会改变它。这里需要读取 router.currentRoute.value.fullPath,才能取得等待结束时的地址。
这个检查可以拦住已经去了其他地址的情况。快速离开后又回到同一地址,则需要额外取消旧等待,地址比较本身无法区分两次进入。
这两处调整分别限制了重复创建和过早启动:引导内部切页时继续使用原实例,首次加载列表时等到需要高亮的条目出现,再调用 drive()。
后记
Cover photo and top image by Jakub Zerdzicki on Pexels.
评论
评论加载中…