前言
随着应用功能越来越多,繁多而详细的功能使用和说明文档,已经不能满足时代追求 快速 的需求,而 引导页(或分步引导) 本质就是 化繁为简,将核心功能以更简单、简短、明了的文字指引用户去使用对应的功能,特别是 ToB
的项目,各种新功能需求迭代非常快,免不了需要 引导页 的功能来快速帮助用户引导。
下面我们通过两个方面来围绕着【前端引导页】进行展开:
- 哪些第三方库可以直接使用快速实现功能?
- 如何自己实现前端引导页的功能?
第三方库的选择
如果你不知道如何做技术选型,可以看看 山月大佬 的这一篇文章 在前端中,如何更好地做技术选型?,下面就简单列举几个相关的库进行简单介绍,具体需求具体分析选择,其他和 API
使用、具体实现效果可以通过官方文档或对应的 README.md
进行查看。
vue-tour
vue-tour
是一个轻量级、简单且可自定义的 Tour
插件,配置也算比较简单清晰,但只适用于 Vue2
的项目,具体效果可以直接参考对应的前面链接对应的内容。
driver.js
driver.js
是一个强大而轻量级的普通 JavaScript
引擎,可在整个页面上驱动用户的注意力,只有 4kb
左右的体积,并且没有外部依赖,不仅高度可定制,还可以支持所有主流浏览器。
shepherd.js
shepherd.js
包含的 API
众多,大多场景都可以通过其对应的配置得到,缺点就是整体的包体积较大,并且配置也比较复杂,配置复杂的内容一般都需要进行二次封装,将可变和不可变的配置项进行抽离,具体效果可见其 官方文档。
intro.js
intro.js
是是一个开源的 vanilla Javascript/CSS
库,用于添加分步介绍或提示,大小在 10kB
左右,属于轻量级的且无外部依赖,详情可见 官方文档。
实现引导页功能
引导页核心功能其实就两点:
- 一是 高亮部分
- 二是 引导部分
而这两点其实真的不难实现,无非就是 引导部分 跟着 高亮部分 移动,并且添加一些简单的动画或过渡效果即可,也分为 蒙层引导 和 无蒙层引导,这里介绍相对比较复杂的 蒙层引导,下面就简单介绍两种简单的实现方案。
cloneNode + position + transition
核心实现:
- 高亮部分 通过
el.cloneNode(true)
复制对应目标元素节点,并将克隆节点添加到蒙层上
- 通过
margin
(或tranlate
、position
等)实现克隆节点的位置与目标节点重合
- 引导部分 通过
position: fixed
实现定位效果,并通过动态修改left、top
属性实现引导弹窗跟随目标移动 - 过渡动画 通过
transition
实现位置的平滑移动 - 页面 位置/内容 发生变化时(如:
resize、scroll
事件),需要重新计算位置信息
缺点:
- 目标节点需要被深度复制
- 不能实现边引导边操作
效果演示:
核心代码:
// 核心配置参数 const selectors = [ { selector: "#btn1", message: "点此【新增】数据!", }, { selector: "#btn2", message: "小心【删除】数据!", }, { selector: "#btn3", message: "可通过此按钮【修改】数据!", }, { selector: "#btn4", message: "一键【完成】所有操作!", }, ]; // Guide.vue <script setup> import { computed, onMounted, ref } from "vue"; const props = defineProps({ selectors: Array, }); const guideModalRef = ref(null); const guideBoxRef = ref(null); const index = ref(0); const show = ref(true); let cloneNode = null; let currNode = null; let message = computed(() => { return props.selectors[index.value]?.message; }); const genGuide = (hasChange = true) => { // 前置操作 cloneNode && guideModalRef.value?.removeChild(cloneNode); // 所有指引完毕 if (index.value > props.selectors.length - 1) { show.value = false; return; } // 获取目标节点信息 currNode = currNode || document.querySelector(props.selectors[index.value].selector); const { x, y, width, height } = currNode.getBoundingClientRect(); // 克隆节点 cloneNode = hasChange ? currNode.cloneNode(true) : cloneNode; cloneNode.id = currNode.id + "_clone"; cloneNode.style = ` margin-left: ${x}px; margin-top: ${y}px; `; // 指引相关 if (guideBoxRef.value) { const halfClientHeight = guideBoxRef.value.clientHeight / 2; guideBoxRef.value.style = ` left:${x + width + 10}px; top:${y <= halfClientHeight ? y : y - halfClientHeight + height / 2}px; `; guideModalRef.value?.appendChild(cloneNode); } }; // 页面内容发生变化时,重新计算位置 window.addEventListener("resize", () => genGuide(false)); window.addEventListener("scroll", () => genGuide(false)); // 上一步/下一步 const changeStep = (isPre) => { isPre ? index.value-- : index.value++; currNode = null; genGuide(); }; onMounted(() => { genGuide(); }); </script> <template> <teleport to="body"> <div v-if="show" ref="guideModalRef" class="guide-modal"> <div ref="guideBoxRef" class="guide-box"> <div>{{ message }}</div> <button class="btn" :disabled="index === 0" @click="changeStep(true)"> 上一步 </button> <button class="btn" @click="changeStep(false)">下一步</button> </div> </div> </teleport> </template> <style scoped> .guide-modal { position: fixed; z-index: 999; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.3); } .guide-box { width: 150px; min-height: 10px; border-radius: 5px; background-color: #fff; position: absolute; transition: 0.5s; padding: 10px; text-align: center; } .btn { margin: 20px 5px 5px 5px; } </style> 复制代码
z-index + position + transition
核心实现:
- 高亮部分 通过控制
z-index
的值,让目标元素展示在蒙层之上 - 引导部分 通过
position: fixed
实现定位效果,并通过动态修改left、top
属性实现引导弹窗跟随目标移动 - 过渡动画 通过
transition
实现位置的平滑移动 - 页面 位置/内容 发生变化时(如:
resize、scroll
事件),需要重新计算位置信息
缺点:
- 当目标元素的父元素
position: fixed | absolute | sticky
时,目标元素的z-index
无法超过蒙版层(可参考shepherd.js
的svg
解决方案)
效果演示:
核心代码:
<script setup> import { computed, onMounted, ref } from "vue"; const props = defineProps({ selectors: Array, }); const guideModalRef = ref(null); const guideBoxRef = ref(null); const index = ref(0); const show = ref(true); let preNode = null; let message = computed(() => { return props.selectors[index.value]?.message; }); const genGuide = (hasChange = true) => { // 所有指引完毕 if (index.value > props.selectors.length - 1) { show.value = false; return; } // 修改上一个节点的 z-index if (preNode) preNode.style = `z-index: 0;`; // 获取目标节点信息 const target = preNode = document.querySelector(props.selectors[index.value].selector); target.style = ` position: relative; z-index: 1000; `; const { x, y, width, height } = target.getBoundingClientRect(); // 指引相关 if (guideBoxRef.value) { const halfClientHeight = guideBoxRef.value.clientHeight / 2; guideBoxRef.value.style = ` left:${x + width + 10}px; top:${y <= halfClientHeight ? y : y - halfClientHeight + height / 2}px; `; } }; // 页面内容发生变化时,重新计算位置 window.addEventListener("resize", () => genGuide(false)); window.addEventListener("scroll", () => genGuide(false)); const changeStep = (isPre) => { isPre ? index.value-- : index.value++; genGuide(); }; onMounted(() => { genGuide(); }); </script> <template> <teleport to="body"> <div v-if="show" ref="guideModalRef" class="guide-modal"> <div ref="guideBoxRef" class="guide-box"> <div>{{ message }}</div> <button class="btn" :disabled="index === 0" @click="changeStep(true)"> 上一步 </button> <button class="btn" @click="changeStep(false)">下一步</button> </div> </div> </teleport> </template> <style scoped> .guide-modal { position: fixed; z-index: 999; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.3); } .guide-box { width: 150px; min-height: 10px; border-radius: 5px; background-color: #fff; position: absolute; transition: 0.5s; padding: 10px; text-align: center; } .btn { margin: 20px 5px 5px 5px; } </style> 复制代码
【扩展】SVG
如何完美解决 z-index
失效的问题?
这里以 shepherd.js
来举例说明,先来看起官方文档展示的 demo
效果:
在上述展示的效果中进行了一些验证:
- 正常点击
NEXT
进入下一步指引,仔细观察SVG
相关数据发生了变化 - 等到指引部分指向代码块的内容区时,复制了此时
SVG
中和path
相关的参数 - 返回到第一步很明显此时的高亮部分高度较小,将上一步复制的参数直接替换当前
SVG
中和path
相关的参数,此时发现整体SVG
高亮内容宽高发生了变化
核心结论:通过 SVG
可编码的特点,利用 SVG
来实现蒙版效果,并且在绘制蒙版时,预留出目标元素的高亮区间(即 SVG
不需要绘制这一部分),这样就解决了使用 z-index
可能会失效的问题。
最后
欢迎关注同名公众号《熊的猫》,文章会同步更新!
以上就是一些简单实现,但还有很多细节需要考虑,比如:边引导边操作的实现、定位原因导致的图层展示问题等仍需要优化。
相信大部分人第一直觉是:直接使用第三方库实现功能就好了呀,自己实现功能不全、也未必好用,属实没有必要。
对于这一点其实在早前看到的一句话说的挺好:了解底层实现原理比使用库本身更有意义,当然每个人的想法不同,不过如果你想开始了解原理又不能立马挑战一些高深的内容,为什么不先从自己感兴趣的又不是那么复杂的功能开始呢?