不使用第三方庫(kù)怎么實(shí)現(xiàn)【前端引導(dǎo)頁(yè)】功能?
點(diǎn)擊上方 前端Q,關(guān)注公眾號(hào)
回復(fù)加群,加入前端Q技術(shù)交流群
前言
隨著應(yīng)用功能越來(lái)越多,繁多而詳細(xì)的功能使用和說(shuō)明文檔,已經(jīng)不能滿足時(shí)代追求 快速 的需求,而 引導(dǎo)頁(yè)(或分步引導(dǎo)) 本質(zhì)就是 化繁為簡(jiǎn),將核心功能以更簡(jiǎn)單、簡(jiǎn)短、明了的文字指引用戶去使用對(duì)應(yīng)的功能,特別是 ToB 的項(xiàng)目,各種新功能需求迭代非???,免不了需要 引導(dǎo)頁(yè) 的功能來(lái)快速幫助用戶引導(dǎo)。
下面我們通過(guò)兩個(gè)方面來(lái)圍繞著【前端引導(dǎo)頁(yè)】進(jìn)行展開(kāi):
哪些第三方庫(kù)可以直接使用快速實(shí)現(xiàn)功能? 如何自己實(shí)現(xiàn)前端引導(dǎo)頁(yè)的功能?
第三方庫(kù)的選擇
如果你不知道如何做技術(shù)選型,可以看看 山月大佬 的這一篇文章 在前端中,如何更好地做技術(shù)選型?[2],下面就簡(jiǎn)單列舉幾個(gè)相關(guān)的庫(kù)進(jìn)行簡(jiǎn)單介紹,具體需求具體分析選擇,其他和 API 使用、具體實(shí)現(xiàn)效果可以通過(guò)官方文檔或?qū)?yīng)的 README.md 進(jìn)行查看。
vue-tour
**`vue-tour`**[3] 是一個(gè)輕量級(jí)、簡(jiǎn)單且可自定義的 Tour 插件,配置也算比較簡(jiǎn)單清晰,但只適用于 Vue2 的項(xiàng)目,具體效果可以直接參考對(duì)應(yīng)的前面鏈接對(duì)應(yīng)的內(nèi)容。

driver.js
**`driver.js`**[4] 是一個(gè)強(qiáng)大而輕量級(jí)的普通 JavaScript 引擎,可在整個(gè)頁(yè)面上驅(qū)動(dòng)用戶的注意力,只有 4kb 左右的體積,并且沒(méi)有外部依賴,不僅高度可定制,還可以支持所有主流瀏覽器。

shepherd.js
**`shepherd.js`**[5] 包含的 API 眾多,大多場(chǎng)景都可以通過(guò)其對(duì)應(yīng)的配置得到,缺點(diǎn)就是整體的包體積較大,并且配置也比較復(fù)雜,配置復(fù)雜的內(nèi)容一般都需要進(jìn)行二次封裝,將可變和不可變的配置項(xiàng)進(jìn)行抽離,具體效果可見(jiàn)其 **官方文檔**[6]。

intro.js
**`intro.js`**[7] 是是一個(gè)開(kāi)源的 vanilla Javascript/CSS 庫(kù),用于添加分步介紹或提示,大小在 10kB左右,屬于輕量級(jí)的且無(wú)外部依賴,詳情可見(jiàn) **官方文檔**[8]。

實(shí)現(xiàn)引導(dǎo)頁(yè)功能
引導(dǎo)頁(yè)核心功能其實(shí)就兩點(diǎn):
一是 高亮部分 二是 引導(dǎo)部分
而這兩點(diǎn)其實(shí)真的不難實(shí)現(xiàn),無(wú)非就是 引導(dǎo)部分 跟著 高亮部分 移動(dòng),并且添加一些簡(jiǎn)單的動(dòng)畫或過(guò)渡效果即可,也分為 蒙層引導(dǎo) 和 無(wú)蒙層引導(dǎo),這里介紹相對(duì)比較復(fù)雜的 蒙層引導(dǎo),下面就簡(jiǎn)單介紹兩種簡(jiǎn)單的實(shí)現(xiàn)方案。
cloneNode + position + transition
核心實(shí)現(xiàn):
高亮部分 通過(guò) el.cloneNode(true)復(fù)制對(duì)應(yīng)目標(biāo)元素節(jié)點(diǎn),并將克隆節(jié)點(diǎn)添加到蒙層上通過(guò) margin(或tranlate、position等)實(shí)現(xiàn)克隆節(jié)點(diǎn)的位置與目標(biāo)節(jié)點(diǎn)重合引導(dǎo)部分 通過(guò) position: fixed實(shí)現(xiàn)定位效果,并通過(guò)動(dòng)態(tài)修改left、top屬性實(shí)現(xiàn)引導(dǎo)彈窗跟隨目標(biāo)移動(dòng)過(guò)渡動(dòng)畫 通過(guò) transition實(shí)現(xiàn)位置的平滑移動(dòng)頁(yè)面 位置/內(nèi)容 發(fā)生變化時(shí)(如: resize、scroll事件),需要重新計(jì)算位置信息
缺點(diǎn):
目標(biāo)節(jié)點(diǎn)需要被深度復(fù)制 不能實(shí)現(xiàn)邊引導(dǎo)邊操作
效果演示:

核心代碼:
// 核心配置參數(shù)
const selectors = [
{
selector: "#btn1",
message: "點(diǎn)此【新增】數(shù)據(jù)!",
},
{
selector: "#btn2",
message: "小心【刪除】數(shù)據(jù)!",
},
{
selector: "#btn3",
message: "可通過(guò)此按鈕【修改】數(shù)據(jù)!",
},
{
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;
}
// 獲取目標(biāo)節(jié)點(diǎn)信息
currNode =
currNode || document.querySelector(props.selectors[index.value].selector);
const { x, y, width, height } = currNode.getBoundingClientRect();
// 克隆節(jié)點(diǎn)
cloneNode = hasChange ? currNode.cloneNode(true) : cloneNode;
cloneNode.id = currNode.id + "_clone";
cloneNode.style = `
margin-left: ${x}px;
margin-top: ${y}px;
`;
// 指引相關(guān)
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);
}
};
// 頁(yè)面內(nèi)容發(fā)生變化時(shí),重新計(jì)算位置
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>
復(fù)制代碼
z-index + position + transition
核心實(shí)現(xiàn):
高亮部分 通過(guò)控制 z-index的值,讓目標(biāo)元素展示在蒙層之上引導(dǎo)部分 通過(guò) position: fixed實(shí)現(xiàn)定位效果,并通過(guò)動(dòng)態(tài)修改left、top屬性實(shí)現(xiàn)引導(dǎo)彈窗跟隨目標(biāo)移動(dòng)過(guò)渡動(dòng)畫 通過(guò) transition實(shí)現(xiàn)位置的平滑移動(dòng)頁(yè)面 位置/內(nèi)容 發(fā)生變化時(shí)(如: resize、scroll事件),需要重新計(jì)算位置信息
缺點(diǎn):
當(dāng)目標(biāo)元素的父元素 position: fixed | absolute | sticky時(shí),目標(biāo)元素的z-index無(wú)法超過(guò)蒙版層(可參考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;
}
// 修改上一個(gè)節(jié)點(diǎn)的 z-index
if (preNode) preNode.style = `z-index: 0;`;
// 獲取目標(biāo)節(jié)點(diǎn)信息
const target =
preNode = document.querySelector(props.selectors[index.value].selector);
target.style = `
position: relative;
z-index: 1000;
`;
const { x, y, width, height } = target.getBoundingClientRect();
// 指引相關(guān)
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;
`;
}
};
// 頁(yè)面內(nèi)容發(fā)生變化時(shí),重新計(jì)算位置
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>
復(fù)制代碼
【擴(kuò)展】SVG 如何完美解決 z-index 失效的問(wèn)題?
這里以 **`shepherd.js`**[9] 來(lái)舉例說(shuō)明,先來(lái)看起官方文檔展示的 demo 效果:

在上述展示的效果中進(jìn)行了一些驗(yàn)證:
正常點(diǎn)擊 NEXT進(jìn)入下一步指引,仔細(xì)觀察SVG相關(guān)數(shù)據(jù)發(fā)生了變化等到指引部分指向代碼塊的內(nèi)容區(qū)時(shí),復(fù)制了此時(shí) SVG中和path相關(guān)的參數(shù)返回到第一步很明顯此時(shí)的高亮部分高度較小,將上一步復(fù)制的參數(shù)直接替換當(dāng)前 SVG中和path相關(guān)的參數(shù),此時(shí)發(fā)現(xiàn)整體SVG高亮內(nèi)容寬高發(fā)生了變化
核心結(jié)論:通過(guò) SVG 可編碼的特點(diǎn),利用 SVG 來(lái)實(shí)現(xiàn)蒙版效果,并且在繪制蒙版時(shí),預(yù)留出目標(biāo)元素的高亮區(qū)間(即 SVG 不需要繪制這一部分),這樣就解決了使用 z-index 可能會(huì)失效的問(wèn)題。
最后
以上就是一些簡(jiǎn)單實(shí)現(xiàn),但還有很多細(xì)節(jié)需要考慮,比如:邊引導(dǎo)邊操作的實(shí)現(xiàn)、定位原因?qū)е碌膱D層展示問(wèn)題等仍需要優(yōu)化。
相信大部分人第一直覺(jué)是:直接使用第三方庫(kù)實(shí)現(xiàn)功能就好了呀,自己實(shí)現(xiàn)功能不全、也未必好用,屬實(shí)沒(méi)有必要。
對(duì)于這一點(diǎn)其實(shí)在早前看到的一句話說(shuō)的挺好:了解底層實(shí)現(xiàn)原理比使用庫(kù)本身更有意義,當(dāng)然每個(gè)人的想法不同,不過(guò)如果你想開(kāi)始了解原理又不能立馬挑戰(zhàn)一些高深的內(nèi)容,為什么不先從自己感興趣的又不是那么復(fù)雜的功能開(kāi)始呢?

關(guān)于本文
作者:熊的貓
https://juejin.cn/post/7142633594882621454

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...


