关于前端:新手引导功能的四种姿势

3次阅读

共计 8673 个字符,预计需要花费 22 分钟才能阅读完成。

  • 原文地址:老手疏导性能的四种姿态
  • 原文作者:清夜
  • 舒适提醒:如果你对文章有歧义或者倡议,欢送来 Github 里提 PR 或者 Issue 一起保护文章~
  • 文章著作权归原文作者所有,转载请注明出处哦~

产品新性能的开发,个别须要遵循的其中两条准则:

  1. 不要信赖用户
    这个意思并不是说要把用户当成敌人,而是站在系统安全的角度,更具体点说,是不要信赖用户的任何输出,尽可能地不要在零碎中留下任何破绽,根本的比方 csrf、xss 的防备,这条准则个别由后端来保障
  2. 将用户当成傻子
    同样不是友好用户的意思,而是要求产品能从用户的角度去适应用户的行为,升高用户的应用门槛,比方表单填写的各种提醒、显眼的提交按钮等,因为波及到与用户的间接接触,所以这一条次要从前端层面进行保障

可能对于常常跟互联网产品打交道的你我来说,对于一个新性能甚至是新产品能够做到边应用边摸索边学会,但你不能保障所有人都具备这个能力,很多人基本不晓得 Ctrl + F 是啥,更不可能晓得浏览器的兼容模式是什么意思,如果产品的流量够大,那么哪怕只有 1% 的用户在应用产品时遇到困难,都是不小的损失

针对这种状况,早就有了很多成熟的解决方案,比方,老手疏导

如上图,其实就是一个遮罩层加一个弹窗,只不过比个别弹窗略微简单一点的是,遮罩层之上除了弹窗之外,还有须要高亮疏导的元素区域,并且弹窗的地位也并不是固定的,而是追随高亮疏导区域进行定位

解释性弹窗这块没什么可说的,次要是遮罩层这块略微有点意思,最近我刚好遇到了这个需要,所以就多思考了一下,发现这个这遮罩层的实现思路倒是不少~

第一种姿态 – mask 拼接

看图谈话


原谅我的灵魂绘图b(~▽~)d

上图整个大矩形看成是浏览器页面,页面区域被五个区块瓜分:toprightbottomleft 以及 新性能区域

其中 新性能区域 元素就是咱们所要展现的新性能区域,而 toprightbottomleft 则都是遮罩元素,它们一起将除了 新性能区域 这块区域之外的残余区域填满,这样看起来可能像是页面上有一个充斥整个页面区域的遮罩层,而后 新性能区域 悬浮在这个遮罩层之上,其实并不是

起因呢也很简略,新性能区域 元素并不是悬浮在页面之上的弹窗,而是位于页面的主体文档流中,一般而言不会是悬浮的,也不会被动设置一个比拟大的 z-index 属性的,那么如果间接一个大遮罩层遮在整个页面之上,必定也就将 新性能区域 给遮罩住了,而后你又想高亮显示 新性能区域 ,这就矛盾了,所以不能局部三七二十一间接一整个遮罩层盖下来,而是要专门避开 新性能区域 才能够

实际上,除了这五块之外,还有第六块元素,它遮罩在 新性能区域 元素的下层,尺寸和地位都和 新性能区域 完全相同,只不过这个元素是通明的(即 background-color: transparent;),你看不到它,然而却能透过它看到位于它底层的 新性能区域

这块区域是干啥用的呢?如果没有做过这个性能可能一下子想不到,这个隐形的遮罩层其实是为了避免用户真的触达到 新性能区域

因为 新性能区域 在理论的场景中,可能是一个菜单,可能是一个按钮,悬浮下来或者点了之后会触发某些操作,比方页面跳转等,咱们必定不心愿在给用户介绍新性能的时候页面元素忽然被更改了,甚至是间接打断了对用户的疏导流程,所以必须要屏蔽掉,那么间接给 新性能区域 加个通明的 盖子,就是一个很简略的解决伎俩了

看代码可能更分明点,次要是 DOM 计算

function Guide() {const [maskTop, setMaskTop] = useState({})
  const [maskRight, setMaskRight] = useState({})
  const [maskBottom, setMaskBottom] = useState({})
  const [maskLeft, setMaskLeft] = useState({})
  const [maskTarget, setMaskTarget] = useState({})
  const targetRef = React.createRef()

  const computeEle = () => {const rect = targetRef.current.getBoundingClientRect()
    const clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    const clientWidth = document.documentElement.clientWidth || document.body.clientWidth
    setMaskTop({height: rect.top + 'px'})
    setMaskRight({
      top: rect.top + 'px',
      height: rect.height + 'px',
      width: clientWidth - rect.right + 'px'
    })
    setMaskBottom({height: clientHeight - rect.bottom + 'px'})
    setMaskLeft({
      top: rect.top + 'px',
      height: rect.height + 'px',
      width: rect.left + 'px'
    })
    setMaskTarget({
      top: rect.top + 'px',
      left: rect.left + 'px',
      width: rect.width + 'px',
      height: rect.height + 'px'
    })
  }

  useEffect(() => {computeEle()
  }, [])
  
  return (
    <div className="box">
      <div className="target_box" ref={targetRef}> 我就是新性能 </div>
      <div className="mask mask_top" style={maskTop}></div>
      <div className="mask mask_right" style={maskRight}></div>
      <div className="mask mask_bottom" style={maskBottom}></div>
      <div className="mask mask_left" style={maskLeft}></div>
      <div className="mask mask_target" style={maskTarget}></div>
    </div>
  )
}

第二种姿态 – border

还是间接上图

这种实现办法只用到了一个额定元素,脑洞也略微大一点,第一种办法中的 toprightbottomleft 四个元素别离应用一个元素的四条边 (border) 来代替,相比于失常元素的 border 来说,这个元素的 border-width 会比拟大,以至于能够填充斥除了 新性能区域 之外页面残余的区域,再将 border-color 设置为失常遮罩层的背景色,border就假装好了~

function Guide() {const [maskBox, setMaskBox] = useState({})
  const targetRef = React.createRef()

  const computeEle = () => {const rect = targetRef.current.getBoundingClientRect()
    const clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    const clientWidth = document.documentElement.clientWidth || document.body.clientWidth
    setMaskBox({
      'borderTopWidth': rect.top + 'px',
      'borderRightWidth': clientWidth - rect.right + 'px',
      'borderBottomWidth': clientHeight - rect.bottom + 'px',
      'borderLeftWidth': rect.left + 'px',
      width: rect.width + 'px',
      height: rect.height + 'px'
    })
  }

  React.useEffect(() => {computeEle()
  }, [])

  return (
    <div className="box">
      <div className="target_box" ref={targetRef}> 我就是新性能 </div>
      <div className="mask_box" style={maskBox}></div>
    </div>
  )
}

不仅如此,这种办法还能够实现一个第一种姿态无奈实现的成果,比方,当 新性能区域 是圆形的时候

如果采取第一种办法,因为拼接遮罩层的四个元素都是矩形,无奈凑出一个圆形的镂空,然而如果采取以后办法,实际上只有给 border所在的元素一个 border-radius: 50%的属性就能够了,不仅限于圆,椭圆甚至是能够应用 border-radius实现的任何成果的形态也是同样的情理

当然啦,如果是这种思路的话,border的计算以及元素的定位就不是上述的逻辑了,给 border所在的元素加了 border-radius: 50%之后,border同样也会受到这个属性的影响,所以须要适当调整 border 的尺寸,防止遮罩层露馅

function Guide() {const [maskBox, setMaskBox] = useState({})
  const targetRef = React.createRef()
  const computeEle = () => {const rect = targetRef.current.getBoundingClientRect()
    const clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    const clientWidth = document.documentElement.clientWidth || document.body.clientWidth
    const rX = rect.left + rect.width / 2
    const rY = rect.top + rect.height / 2
    // 须要高亮的 圆形的新性能区域 圆心间隔四个拐角的最大长度作为遮罩层圆的半径
    const lt = Math.sqrt(Math.pow(rX, 2) + Math.pow(rY, 2))
    const lb = Math.sqrt(Math.pow(rX, 2) + Math.pow(clientHeight - rY, 2))
    const rt = Math.sqrt(Math.pow(clientWidth - rX, 2) + Math.pow(rY, 2))
    const rb = Math.sqrt(Math.pow(clientWidth - rX, 2) + Math.pow(clientHeight - rY, 2))
    const r = Math.ceil(Math.max(lt, lb, rt, rb))
    setMaskBox({
      borderWidth: r + 'px',
      width: rect.width + 'px',
      height: rect.height + 'px',
      left: rX - r - rect.width / 2 + 'px',
      top: rY - r - rect.height / 2 + 'px'
    })
  }

  React.useEffect(() => {computeEle()
  }, [])

  return (
    <div className="box">
      <div className="target_box" ref={targetRef}> 我就是新性能 </div>
      <div className="mask_box" style={maskBox}></div>
    </div>
  )
}

第三种姿态 – box-shadow

这种姿态是从下面的 border启发而来的,既然 border的尺寸在足够大的状况下,能够模仿遮罩层,那么 box-shadow在尺寸足够大、数量足够多的状况下,也能够模仿

只不过呢,应用 box-shadow实现这个成果,比应用 border麻烦了很多,因为一个元素在不设置 box-shadow 暗影的大小 (spread) 属性的状况下,其 box-shadow 尺寸最大也只能和元素自身雷同,也就是元素的 box-shadow 齐全从元素自身偏移进去的成果

<style>
  .box {
    width: 50px;
    height: 50px;
    background-color: green;
    box-shadow: 50px 0px indianred;
  }
</style>

而如果设置了 box-shadow的暗影的大小 (spread) 属性,那么因为这个属性会以元素为核心同时向四个方向扩大,就不好管制 box-shadow 占据的区域地位了,没方法通过对一个 box-shadow扩大来达到放大暗影的成果,好在 box-shadow 的值能够设置不止一个,通过多个 box-shadow 的组合,最终也能够实现一个覆盖住全屏幕的遮罩层

我轻易写了一个 Demo,而后为了实现这个成果,box-shadow 一共设置了 1316 个值!而 box-shadow 这个属性对于浏览器的耗费是比拟大的,同时渲染那么多 box-shadow显然是不可能用于理论生产环境的,并且计算这些值相对而言也比拟伤脑筋

const clientHeight = document.documentElement.clientHeight || document.body.clientHeight
const clientWidth = document.documentElement.clientWidth || document.body.clientWidth
const shadowPositionMap = {
  top: {
    index: 1,
    flag: -1
  },
  right: {
    index: 0,
    flag: 1
  },
  bottom: {
    index: 1,
    flag: 1
  },
  left: {
    index: 0,
    flag: -1
  },
}
const maskColor = 'rgba(0, 0, 0, 0.45)'
function getOneSideBoxShadow(totalSize, targetSize, direction) {const shadowArr = []
  let i = 0
  let shadowItem = null
  while (targetSize * i < totalSize) {shadowItem = [0, 0]
    shadowItem[direction.index] = direction.flag * targetSize * (i + 1)
    shadowArr.push(shadowItem)
    i++
  }
  return shadowArr
}
function genBoxShadow(shadowArr) {return shadowArr.reduce((t, c) => {return t + `${c[0]}px ${c[1]}px ${maskColor},`
  }, '').slice(0, -1)
}
function repeatBoxShadow(arr, targetWidth, leftW, rightW) {const leftCount = Math.ceil(leftW / targetWidth)
  const rightCount = Math.ceil(rightW / targetWidth)
  const repeatArr = []
  ;[leftCount, rightCount].forEach((count, index) => {
    const flag = index === 0 ? -1 : 1
    for (let i = 1; i <= count; i++) {
      repeatArr.push(...arr.map(item => {return [flag * i * targetWidth, item[1]]
      }))
    }
  })
  return repeatArr
}

function Guide() {const [maskBox, setMaskBox] = useState({})
  const targetRef = React.createRef()
  const computeEle = () => {const rect = targetRef.current.getBoundingClientRect()
    const rightGap = clientWidth - rect.right
    const color = 'rgba(0,0,0,0.45)'
    const topSingle = getOneSideBoxShadow(rect.top, rect.height, shadowPositionMap.top)
    const bottomSingle = getOneSideBoxShadow(clientHeight - rect.bottom, rect.height, shadowPositionMap.bottom)
    setMaskBox({
      boxShadow: `
        ${genBoxShadow(topSingle)},
        ${genBoxShadow(getOneSideBoxShadow(rightGap, rect.width, shadowPositionMap.right))},
        ${genBoxShadow(bottomSingle)},
        ${genBoxShadow(getOneSideBoxShadow( rect.left, rect.width, shadowPositionMap.left))},
        ${genBoxShadow(repeatBoxShadow(topSingle, rect.width, rect.left, rightGap))},
        ${genBoxShadow(repeatBoxShadow(bottomSingle, rect.width, rect.left, rightGap))}
      `,
      left: rect.left + 'px',
      top: rect.top + 'px',
      width: rect.width + 'px',
      height: rect.height + 'px'
    })
  }

  React.useEffect(() => {computeEle()
  }, [])

  return (
    <div className="box">
      <div className="target_box" ref={targetRef}> 我就是新性能 </div>
      <div className="mask_box" style={maskBox}></div>
    </div>
  )
}

第四种姿态 – canvas

css能实现的货色,个别状况下 js也能(我凭着教训乱说的没有证据),利用 canvas间接将所需的成果画进去,省心又间接~

canvas 画布大小和页面统一,而后应用遮罩色填充画布,最初将挡在所需高亮的 新性能区域 canvas 区域擦除掉,就实现了

function Guide() {const [width, setWidth] = useState(0)
  const [height, setHeight] = useState(0)
  const targetRef = React.createRef()
  const canvasRef = React.createRef()
  const computeEle = () => {
    const clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    const clientWidth = document.documentElement.clientWidth || document.body.clientWidth
    setWidth(clientWidth)
    setHeight(clientHeight)
    const rect = targetRef.current.getBoundingClientRect()
    const c = canvasRef.current
    const ctx = c.getContext('2d')
    ctx.fillStyle = 'rgba(0, 0, 0, 0.45)'
    ctx.fillRect(0, 0, c.width, c.height)
    ctx.clearRect(rect.left,rect.top, rect.width, rect.height)
  }

  React.useEffect(() => {computeEle()
  }, [])

  return (
    <div className="box">
      <div className="target_box" ref={targetRef}> 我就是新性能 </div>
      <canvas id="myCanvas" ref={canvasRef} width={clientWidth} height={clientHeight}> 您的浏览器不反对 HTML5 canvas 标签。</canvas>
    </div>
  )
}

相比于第二种姿态的 bordercanvas 就更厉害啦,能够进行任意形态元素的高亮,只有你晓得怎么画进去就行

The End

只是一个小小的新性能疏导就有那么多的实现形式,可见前端的确是灵便又乏味的,大家平时在做需要的时候都能够多加思考一二,不要总是局限于以往的思维,放开脑洞,或者就能找到更好的解决方案~

正文完
 0