手撸一个引导页组件

引导页这个组件,相信大家或多或少都见过吧,就像下面这样:

想要实现这个功能,如果借助第三方库来做的话,其实很简单,比如driver.js就可以做到。就像下面这样:

javascript 复制代码
import React, { useEffect } from 'react';
import { driver } from 'driver.js';

function App(){

    useEffect(
        () => {
            let driverObj = driver({
                showProgress: true,
                steps: [
                  {
                      element: '.one',
                      popover: {
                          title: 'Title',
                          description: 'Description'
                      }
                  },
                  {
                      element: '.two',
                      popover: {
                          title: 'Title',
                          description: 'Description'
                      }
                  }
                ]
            });
            
            driverObj.drive();
        }
    );

    return <div>
      <div className='item one' onClick={click1}>第一个元素</div>
      <div className='item two'>第二个元素</div>
      <div className='item three'>第三个元素</div>
    </div>
}

export default App;

效果如下:

那接下来,我们就一起看看如何从0实现它。

一、这个组件真的是公共组件吗?

首先说一下这个组件的核心点,如下:

如何实现元素高亮效果?

那一定是衬托。就像上图一样,第一个元素为什么能看出来是高亮的,是因为它周围都是黑色的,颜色不同,效果自然不一样。

1.1、如何实现高亮效果?

2种方式,一种方式是"层级",一种方式是svg。

1.2、层级

层级嘛,很简单,我不写代码,相信大家也一定会。

代码如下:

html 复制代码
<style>
    .one {
        margin-left: 200px;
        margin-top: 100px;
        width: 200px;
        height: 200px;
        border-radius: 50%;
        box-sizing: border-box;
        display: flex;
        justify-content: center;
        align-items: center;
        background: cornflowerblue;
        z-index: 2000;
        position: relative;
    }
    .shadow-box {
        position: fixed;
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: 1000;
        pointer-events: none;
        background-color: #111;
        opacity: 0.7;
    }
</style>
<div class="one" onclick="clickElement()">第一个元素</div>
<div class="shadow-box"></div>

效果如下:

1.3、svg

这种方式也是driver.js使用的。代码如下:

html 复制代码
<style>
    svg {
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        position: fixed;
        left: 0px;
        bottom: 0px;
        top: 0px;
        right: 0px;
        z-index: 1000;
    }
</style>
<svg>
    <path
        d="
                M792,0
                L0,0
                L0,857
                L792,857
                L792,0
            Z
                M0,0 
                h210
                v110
                h-210
                v-110
            Z
        "
    ></path>
</svg>

效果如下:

svg这个标签,你想要画什么图案,它都有对应的标签能够满足你。比如<rect>用来画矩形<circle>用来画圆形<path>用来画路径

path标签里,d属性用来执行具体的绘画命令。它有很多个属性或者命令,这里不做赘述,想要弄清楚的可以自己去查阅MDN。

有一点我需要声明,我在MDN上没有查到这种写法,就是一个d属性里跟着多个Z。而且这种写法会触发多种怪异的行为,大家可以试一下(一个标签里,执行多个封闭的命令),它的行为并不总能符合预期,只是对于"引导页高亮"的效果来说,它能满足,因为这种效果是规则的,就是画矩形嘛。

1.4、它能写成公共组件吗?

如果是我,我不会将它封装成公共组件。因为我不具备对它兜底的能力。

首先第一点,层级这种写法我是一定不会采纳的,因为这种写法对用户的代码具有侵略性,随意修改用户代码的层级,这听起来就不靠谱,做起来更不靠谱。

第二点,svg的这种写法,第二次绘制的时候,为什么背景是透明?第二次绘制的时候,封闭路径的背景颜色可以修改吗?我在MDN上找不到答案。同时,为什么我在后面追加了多种不规则形状的路径,它的表现不符合预期,我在MDN上也没找到对应的答案。

基于以上2点,这个功能,我肯定是能做,但我一定不会将它封装成公共组件。

二、手撸代码

我们这里依旧是低开低走,只实现最简单的情况。

以2个元素之间的联动为例。

2.1、初始化

html 复制代码
<div class="item one">one</div>
<div class="item two">two</div>
<button onclick="clickStartTour()">开始引导</button>

样式代码如下:

css 复制代码
.item {
    width: 100px;
    height: 100px;
    box-sizing: border-box;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 100px;
    margin-left: 100px;
    border-radius: 50%;
}
.one {
    background-color: cornflowerblue;
}
.two {
    background-color: orange;
    margin-left: 150px;
}

现在的效果如下:

现在我们想着是点击"开始引导"按钮,然后就出现"one"元素高亮效果。

2.2、实现元素高亮

按钮点击事件如下:

javascript 复制代码
// 点击"开始引导"按钮触发
function clickStartTour(){
    let svgTag = document.createElementNS("http://www.w3.org/2000/svg", "svg");;
    let pathTag = document.createElementNS("http://www.w3.org/2000/svg", "path");
    svgTag.appendChild(pathTag);
    document.querySelector('body').appendChild(svgTag);
    pathTag.setAttribute(
        'd',
        `
            M0 0
            L0 ${window.innerHeight}
            L${window.innerWidth} ${window.innerHeight}
            L${window.innerWidth} 0
            Z
        `
    );
}

我们还需要给svg一个固定定位的样式,这样能够保证它是一个遮罩层。

css 复制代码
svg {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    position: fixed;
    z-index: 500;
    top: 0px;
    bottom: 0px;
    left: 0px;
    right: 0px;
}
path {
    opacity: 0.7;
}

现在当我们点击一下,就会有下面的这个效果:

接下来我们来获取到 one元素 的高亮区域块。

javascript 复制代码
function highLightElement(element){
    // 高亮区域块左上方的坐标
    let highLightAreaPointX = element.offsetLeft - 10;
    let hightLightAreaPointY = element.offsetTop - 10;
    // 获取到元素的width与height,我们就可以获取到高亮矩形区域的所有坐标
    let elementWidth = element.clientWidth + 20;
    let elementHeight = element.clientHeight + 20;
    let pathTag = document.querySelector('path');
    pathTag.setAttribute(
        'd',
        `
                M0 0
                L0 ${window.innerHeight}
                L${window.innerWidth} ${window.innerHeight}
                L${window.innerWidth} 0
            Z
                M${highLightAreaPointX} ${hightLightAreaPointY}
                L${highLightAreaPointX + elementWidth} ${hightLightAreaPointY}
                L${highLightAreaPointX + elementWidth} ${hightLightAreaPointY + elementHeight}
                L${highLightAreaPointX} ${hightLightAreaPointY + elementHeight}
            Z
        `
    )
}

然后,在clickStartTour函数的末尾,加上下面这句代码:

javascript 复制代码
highLightElement(document.querySelector('.one'));

我们便可以得到这样的效果:

接下来就是在高亮区域旁边加上类似tooltip的提示,用于触发下一个块的高亮。

2.3、tooltip提示

这个tooltip轻提示,它也是一种公共的基础组件。目前我的UI专栏里汇聚了16种组件,虽然没有tooltip组件的身影,但是以后肯定会实现,而且我相信这个专���也一定会是全网组件实现里面,组件种类最多,讲解最通俗易懂的专栏。

好啦,成功跑题了,我们回到tooltip的这个实现里。我们还是噢,不去实现特别细的点,因为细的点往往是由自身业务产生的,我们这里能够实现最简单的demo即可。

javascript 复制代码
function createTooltip(left, top){
    // tooltip组件的最外层盒子
    let divTag = document.createElement('div');
    divTag.classList.add('tooltip-content');
    // 头部
    let headerTag = document.createElement('div');
    let text = document.createTextNode('这是标题说明');
    // 尾部
    let footerTag = document.createElement('div');
    footerTag.classList.add('footer');
    // 按钮
    let buttonTag = document.createElement('button');
    let buttonName = document.createTextNode('下一步');
    
    // 尾部元素添加按钮
    footerTag.appendChild(buttonTag);
    buttonTag.appendChild(buttonName);
    headerTag.append(text);
    headerTag.classList.add('header');
    
    // tooltip最外层元素添加header
    divTag.appendChild(headerTag);
    
    // tooltip最外层元素添加footer
    divTag.appendChild(footerTag);
    
    // 设置tooltip组件的显示位置
    divTag.style.top = `${top}px`;
    divTag.style.left = `${left}px`;
    document.querySelector('body').appendChild(divTag);
}

我们还需要给这个tooltip组件添加一些基础的css样式:

css 复制代码
.tooltip-content {
    width: 200px;
    height: 100px;
    background: #fff;
    position: absolute;
    z-index: 600;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-start;
}
.tooltip-content::after {
    content: '';
    display: block;
    position: absolute;
    left: -10px;
    top: 40%;
    transform: rotate(-135deg);
    width: 10px;
    height: 10px;
    border-top: 3px solid #EFEFEF;
    border-right: 3px solid #EFEFEF;
}
.header {
    width: 100%;
    border-bottom: 1px solid cornflowerblue;
}
.footer {
    width: 100%;
    border-top: 1px solid cornflowerblue;
    display: flex;
    justify-content: flex-end;
    box-sizing: border-box;
    padding-right: 10px;
}

那这个tooltip提示产生的时机在哪里呢?是不是应该跟高亮区域一块显示的呀,因此我们还需要在highLightElement方法里添加createTooltip方法的调用。代码如下:

javascript 复制代码
function highLightElement(element){
    // 高亮区域块左上方的坐标
    let highLightAreaPointX = element.offsetLeft - 10;
    let hightLightAreaPointY = element.offsetTop - 10;
    // 获取到元素的width与height,我们就可以获取到高亮矩形区域的所有坐标
    let elementWidth = element.clientWidth + 20;
    let elementHeight = element.clientHeight + 20;
    let pathTag = document.querySelector('path');
    pathTag.setAttribute(
        'd',
        `
                M0 0
                L0 ${window.innerHeight}
                L${window.innerWidth} ${window.innerHeight}
                L${window.innerWidth} 0
            Z
                M${highLightAreaPointX} ${hightLightAreaPointY}
                L${highLightAreaPointX + elementWidth} ${hightLightAreaPointY}
                L${highLightAreaPointX + elementWidth} ${hightLightAreaPointY + elementHeight}
                L${highLightAreaPointX} ${hightLightAreaPointY + elementHeight}
            Z
        `
    )
    let tooltipTop = hightLightAreaPointY;
    let tooltipLeft = highLightAreaPointX + elementWidth + 30;
    createTooltip(tooltipLeft, tooltipTop);
}

现在当我们再次点击"开始引导"按钮的时候,就会出现下面这个效果:

很好哈,我们已经快成功了,接下来就是2个元素之间的高亮联动。

2.4、高亮元素联动

这个的触发时机没得说,就是点击"下一步"触发的。因此我们来补全下这块的代码。

按钮(下一步)这个元素是在创建tooltip组件的时候产生的,所以我们需要修改下createTooltip方法。

javascript 复制代码
// 声明一个全局变量,用于标识当前高亮元素是否是最后一个
let isFinal = false;

// 给button添加click事件
function createTooltip(left, top){

    // 其余代码不变 --------

    // 给button添加click事件
    buttonTag.onclick = function (){
        if (isFinal){
            // 因为是2个元素之间的联动,所以two元素高亮之后,再次点击"下一步"时,应该remove svg
            nextHightLightElement(null);
            if (document.querySelector('.tooltip-content')){
               document.querySelector('body').removeChild(document.querySelector('.tooltip-content'));
            }
            return
        }
        isFinal = true;
        // 注意这里,进行下个高亮元素的显示
        nextHightLightElement(document.querySelector('.two'));
    }
    
    // 其余代码不变 --------
}

接下来我们就来看看nextHightLightElement函数的实现。

javascript 复制代码
function nextHightLightElement(element){
    if (!element){
        removeSvg();
        return
    }
    highLightElement(element);
}

最后,我们修改下highLightElement函数的实现,只需在最后一步加上如下代码:

javascript 复制代码
let nextElement = document.querySelector('.two');
createTooltip(tooltipLeft, tooltipTop, nextElement);

至此,一个简单的引导页组件就实现了。效果如下:

三、最后

好啦,本期引导页组件的实现思路到这里就结束啦,在这个过程中,我也提了一些有关path标签绘制路径的一些疑惑,如果有大神知道这些问题的答案,欢迎评论区里指点一下,我们下期再见,拜拜~~

相关推荐
Moon里7 分钟前
【HTML】Html标签
前端·html
weixin_4427331114 分钟前
Cookie、Web Storage介绍
前端
懵魅的程序猿1 小时前
vue使用scope插槽实现dialog窗口
javascript·vue.js·elementui
GIS_JH1 小时前
vue3 + vite2 vue 打包后router-view空白
前端·javascript·vue.js
你不讲 wood2 小时前
组件通信——provide 和 inject 实现爷孙组件通信
前端·javascript·vue.js
我码玄黄2 小时前
高效Flutter应用开发:GetX状态管理实战技巧
前端·flutter·状态管理
codeMing_2 小时前
Vue使用query传参Boolean类型,刷新之后转换为String问题
前端·javascript·vue.js
神仙别闹2 小时前
基于Java+Mysql实现(WEB)宿舍管理系统
java·前端·mysql
有一个好名字2 小时前
后端Controller获取成功,但是前端报错404
前端·spring
stpzhf3 小时前
记录特别代码样式
前端·javascript·css