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

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

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

intro.js
**`intro.js`**[7] 是是一個開源的 vanilla Javascript/CSS 庫,用于添加分步介紹或提示,大小在 10kB左右,屬于輕量級的且無外部依賴,詳情可見 **官方文檔**[8]。
實現(xiàn)引導(dǎo)頁功能
引導(dǎo)頁核心功能其實就兩點:
- 一是 高亮部分
- 二是 引導(dǎo)部分
而這兩點其實真的不難實現(xiàn),無非就是 引導(dǎo)部分 跟著 高亮部分 移動,并且添加一些簡單的動畫或過渡效果即可,也分為 蒙層引導(dǎo) 和 無蒙層引導(dǎo),這里介紹相對比較復(fù)雜的 蒙層引導(dǎo),下面就簡單介紹兩種簡單的實現(xiàn)方案。
cloneNode + position + transition
核心實現(xiàn):
-
高亮部分 通過
el.cloneNode(true)復(fù)制對應(yīng)目標元素節(jié)點,并將克隆節(jié)點添加到蒙層上-
通過
margin(或tranlate、position等)實現(xiàn)克隆節(jié)點的位置與目標節(jié)點重合
-
通過
-
引導(dǎo)部分 通過
position: fixed實現(xiàn)定位效果,并通過動態(tài)修改left、top屬性實現(xiàn)引導(dǎo)彈窗跟隨目標移動 -
過渡動畫 通過
transition實現(xiàn)位置的平滑移動 -
頁面 位置/內(nèi)容 發(fā)生變化時(如:
resize、scroll事件),需要重新計算位置信息
缺點:
- 目標節(jié)點需要被深度復(fù)制
- 不能實現(xiàn)邊引導(dǎo)邊操作
效果演示:

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

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