5分钟带你开发属于自己的引导组件

564 阅读2分钟

引导组件的主要目的是帮助用户熟悉和了解应用程序的功能和操作流程。通常,引导组件分为三个部分:DOM 元素部分(从 A 到 B)、遮罩与高亮部分,以及弹出提示框部分(Popover 部分)。

ok,我们现在从遮罩与高亮部分开始逐步拆解并实现

遮罩与高亮

我们使用 SVG 元素的 Path 属性来实现遮罩和高亮效果,这种方法简单易用。以下是主要的应用指令:

  • M 将画笔移动到指定的坐标位置,但不画线条
  • a 绘制一个椭圆弧线 可用 a5,5 0 0 1 -5,5 绘制椭圆圆弧实现
  • L 从当前位置画一条直线到指定的坐标 (x,y) 位置
  • h 从当前位置画一条水平线到指定的 x 坐标
  • v 从当前位置画一条垂直线到指定的 y 坐标

遮罩

利用 window.innerWidthwindow.innerHeight 属性,可以快速绘制全屏遮罩。

function genPath() {
  return `M0,0 L0,${window.innerWidth} L0,${window.innerHeight} L${window.innerWidth},${window.innerHeight} L${window.innerWidth},0 z
`
}

高亮

我们利用 Element 的 getBoundingClientRect() 方法获取元素的尺寸和相对于视口的坐标信息,从而能够精确计算高亮区域的宽度、高度以及路径坐标的位置信息。

通过这些位置坐标信息,我们可以绘制内部交叉路径将内容框起来,并利用 SVG 的 fill-rule: evenodd 属性来确定多边形内部区域,从而实现高亮效果。

// ...
const toDefinition= toEl.value.getBoundingClientRect()
const highlightBoxX = toDefinition.x
const highlightBoxY = toDefinition.y

const highlightBoxWidth = toDefinition.width
const highlightBoxHeight = toDefinition.height

function genPath() {
  return `M0,0 L0,${window.innerWidth} L0,${window.innerHeight} L${window.innerWidth},${window.innerHeight} L${window.innerWidth},0 Z M${highlightBoxX} ${highlightBoxY}, h${highlightBoxWidth} a${config.padding},${config.padding} 0 0 1 ${config.padding},${config.padding} v${highlightBoxHeight} a${config.padding},${config.padding} 0 0 1 -${config.padding},${config.padding} h-${highlightBoxWidth} a${config.padding},${config.padding} 0 0 1 -${config.padding},-${config.padding} v-${highlightBoxHeight} a${config.padding},${config.padding} 0 0 1 ${config.padding},-${config.padding} z
`
}

这样就完成了之前提到的遮罩和高亮区域的创建。

DOM元素部分

以下是 DOM 元素结构,其中包含了任意的文章内容部分、弹出提示框部分(Popover 部分)以及遮罩部分。

<!-- 任意的内容节点 -->
<div class="start relative">start</div>
<div class="first relative">first</div>
<!-- start -->
<button @click="handleStart">start</button>
<!-- 内容 -->
<teleport to="body">
  <div class="message" :style="popover.style" v-if="open">
    <div>{{ current.render() }}</div>
    <button @click="handlePrev">prev</button>
    <button @click="handleNext">next</button>
  </div>
</teleport>
<!-- 遮罩 -->
<teleport to="body">
  <svg class="overlay" width="100%" height="100%" v-show="open">
    <path ref="path"></path>
  </svg>
</teleport>

用命令式方式定义从 A 到 B 的步骤结构及内容。

 // 当前节点
const current = ref(null)
// 内容
const popover = reactive({
  style: '',
})
// 元素
const toEl = ref(null)
// 命令式引导节点以及内容
const steps = [
  {
    el: '.start',
    render() {
      return 123
    },
  },
  // ...
]

定义事件处理函数的方法

// 设置对应的内容
function setCurrent(index = 0) {
  current.value = steps[index]
  toEl.value = current.value.el
  genPath()
}
// 开始
function handleStart() {
  open.value = true
  setCurrent()
}
// 上一步
function handlePrev() {
  const prev = steps.findIndex((item) => item.el === toEl.value) - 1

  if (prev < 0) {
    return
  }
  setCurrent(prev)
}
// 下一步
function handleNext() {
  const next = steps.findIndex((item) => item.el === toEl.value) + 1

  if (next > steps.length - 1) {
    return
  }

  setCurrent(next)
}

弹出提示框部分(Popover 部分)

弹出提示框部分(Popover 部分),我们采用render函数展示内容,主要难点在于确定弹出框的位置。

由于我们已经获取了目标元素的位置和信息,所以定义位置相对容易。以下是一个计算右侧边距的示例:

const config = {
  padding: 5
}
const styles = (toDefinition) => {
  const messageDefinition = document
    .querySelector('.message')
    .getBoundingClientRect()
  // 超出右侧
  const isOuterRight = toDefinition.x + messageDefinition.width > window.innerWidth
  // ...
  return {
    left: isOuterRight ? 'auto' : toDefinition.x - config.padding * 2 + 'px',
    top: toDefinition.y + toDefinition.height + config.padding * 2 + 'px',
    right: isOuterRight
      ? window.innerWidth - toDefinition.x - toDefinition.width + 'px'
      : 'auto',
  }
}

// ... 
popover.style = style(toEL.value.getBoundingClientRect)

通过以上内容,我们探讨了引导组件的构建方法。

如有表述不准确的地方,请大家指正。

参考

  1. driver.js