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.

评论

评论加载中…

输入关键词开始搜索。

选择Enter 打开Esc 关闭CtrlK 唤起