vite + vue-ts 十分钟 撸一个新手引导

avatar
研发 @比心APP

背景

新手引导,小伙伴们一定见过不下n次,新功能上线或者系统使用引导,都可能会用到它。与核心业务逻辑没有太大关系,但却尤为重要。中页面上的某个元素高亮并将其他部分用蒙层盖住,加上一个指引说明和下一步。工作量不大,每次都是一顿操作就加上了,但是日复一日,当我们再次遇到类似的需求,是不是非常悔恨上一次为什么不做个通用的呢? 身为程序媛,我也走过同样的路,并且这条路都快走成水泥道了、、、

所以这次下定决心,这次一定搞个通用的,给小哥哥用上!!!

下面花十分钟 实现一个新手引导 话不多说, 上效果~

 

非常简单,跟我来一起看看怎么实现吧!

研发思路

核心逻辑选择与框架解耦 方便在移动端和PC端都能使用 不受研发框架限制 demo显示选用Vue

技术栈:vite + vue-ts 模板 详情

首先 先把demo静态代码生成出来 因为对精细度要求不高 所以选择 codeFun 插件快速生成

选择高亮元素 复制节点  朴实无华的复制~  canvas 生成的方案在一些特定的机型上有些兼容问题

/**
 * @description: 克隆目标元素
 * @param {HTMLElement} element DOM节点
 * @return {*}
 */
export const cloneElement = (elementHTMLElement): Node => {
  const clone = element.cloneNode(trueas HTMLElement
  // 获取元素的大小及其相对于视口的位置
  const rectObject = element.getBoundingClientRect()
  const { width, height, x, y } = rectObject
  clone.style.width = width + 'px'
  clone.style.height = height + 'px'
  clone.style.position = 'absolute'
  clone.style.left = x + 'px'
  clone.style.top = y + 'px'
  clone.style.margin = '0'
  
  return clone
}

初始化盒子 选择fixed 定位遮盖全屏并加上半透明背景 隐藏起来并添加到body上 层级加到最高

/**
   * @description: 初始化盒子
   * @param {*}
   * @return {*}
   */
  initBox() {
    this.domBox.id = 'guideBox'
    this.domBox.style.position = 'fixed'
    this.domBox.style.top = '0'
    this.domBox.style.bottom = '0'
    this.domBox.style.left = '0'
    this.domBox.style.right = '0'
    this.domBox.style.zIndex = '100'
    this.domBox.style.background = 'rgba(0,0,0,.5)'
    this.domBox.style.display = 'none' 
    document.body.appendChild(this.domBox)
  }

添加一个指引图片 获取图片指定方位的真实位置并给图片添加点击事件 点击跳转到下一步

// 创建指引图
    const tip = document.createElement('img'as HTMLImageElement
    tip.src = content
    const [top, left] = await getPlacement(selector, content, placement, this.offset)
    tip.style.position = 'absolute'
    tip.style.top = top
    tip.style.left = left
    // 点击跳转下一步
    tip.onclick = () => {
      this.stepIndex = Number(this.stepIndex) + 1
      this.startStep()
    }
    this.domBox.appendChild(tip)
    // 显示盒子
    this.domBox.style.display = 'block'

其中比较麻烦的是处理引导图和元素的位置关系

 

参考 antd 中tooltip组件 确定好如上几个方位 指定引导图只能按照上述几个位置摆放 根据高亮元素和指引图片的具体尺寸 计算指引图片的不同位置 举个例子: LT, Left, LB  这三个位置 指引图都是在左边 那么他们的left 属性 都是相对高亮元素的left值减少: 指引图片的宽度 + 间隙宽度  以此类推 上下右侧  上下注意需要居中  那便是:1/2高亮元素宽度 减掉 1/2指引图宽度 都是绝对定位哦

// 在上 top减去偏移量  在下 top加上偏移量  在左 left减去偏移量  在右 left加上偏移量
export const placementOffsetMapPlacement.PlacementOffsetMap = {
  top({ rw, rh, cw, ch, offset }) => {
    return [-ch - offset, rw/2 - cw/2]
  },
  topLeft({ rw, rh, cw, ch, offset }) => {
    return [-ch - offset, 0]
  },
  topRight:  ({ rw, rh, cw, ch, offset }) => {
    return [-ch - offset, rw - cw]
  },
  bottom({ rw, rh, cw, ch, offset }) => {
    return [rh + offset, rw/2 - cw/2]
  },
  bottomLeft({ rw, rh, cw, ch, offset }) => {
    return [rh + offset, 0]
  },
  bottomRight({ rw, rh, cw, ch, offset }) => {
    return [rh + offset, rw - cw]
  },
  left({ rw, rh, cw, ch, offset }) => {
    return [rh/2 - ch/2, -cw - offset]
  },
  leftTop({ rw, rh, cw, ch, offset }) => {
    return [0, -cw - offset]
  },
  leftBottom({ rw, rh, cw, ch, offset }) => {
    return [rh - ch, -cw - offset]
  },
  right({ rw, rh, cw, ch, offset }) => {
    return [rh/2 - ch/2, rw + offset]
  },
  rightTop({ rw, rh, cw, ch, offset }) => {
    return [0, rw + offset]
  },
  rightBottom({ rw, rh, cw, ch, offset }) => {
    return [rh - ch, rw + offset]
  },
};

接下来 我们要处理下状态存储 一般都是要求存localStorage的 状态存在前端 分别在开始引导前和走完引导后 做存储状态检车和存储数据

/**
   * @description: 第一步开始前
   * @param {*}
   * @return {*}
   */
  beforeEnter() {
    return new Promise((resolve, reject) => {
      if (this.storageKey) {
        resolve(!storage.get(this.storageKey))
      } else {
        resolve(true)
      }
    })
  }

  /**
   * @description: 最后一步结束后
   * @param {*}
   * @return {*}
   */
  afterLeave() {
    return new Promise((resolve, reject) => {
      if (this.storageKey) {
        resolve(storage.set(this.storageKey1))
      } else {
        resolve(true)
      }
    })
  }

最后用最简单的方式导出代码,添加build命令 执行 tsc 并更新 tsconfig.json 文件

// script
"build""tsc"

// tsconfig.json
{
  "compilerOptions": {
    "module""ES6",
    "strict"true,
    "outDir""./lib",
    "jsx""preserve",
    "sourceMap"true,
    "lib": ["esnext""dom"],
    "declaration"true
  },
  "include": ["src/utils/*.ts"]
}

完成!! 就这么简单, 还可以根据自己的需求添加各种参数: 是否需要动画、添加到盒子元素非body、后端存储状态、指引图支持DOM、再加点步骤判断什么的,厉害死了

代码&使用

使用案例:

npm i tool-guide
import Guide from 'tool-guide'

const guide = new Guide({
  steps: [{
    element'.section_5',
    placement'bottom',
    content'https://yppphoto.hibixin.com/yppphoto/8c936439588546be907df129bc48d1f0.png'
  }, {
    element'.section_7',
    placement'bottomLeft',
    content'https://yppphoto.hibixin.com/yppphoto/dd4a5f0a24154e36a09c67e6f8496aef.png'
  }, {
    element'.image_4',
    placement'bottomRight',
    content'https://yppphoto.hibixin.com/yppphoto/6114d84ed9aa425e97363abf98643813.png'
  }],
  storageKey'demo'
})


guide.startStep()

短短几行,就实现了新手引导的常规功能, 具体代码请查看: github.com/DDU1222/too…

参数

Guide.GuideOptions

参数类型描述默认值是否必传
stepsGuide.Step[]步骤[]
offsetnumber高亮元素与指引图间隙8px
storageKeystring数据存储key 不传不存''

Guide.Step

参数类型描述默认值是否必传
elementHTMLElement  string  null高亮元素
contentstring提示内容 现在是张图
placementPlacement.PlacementEnum提示内容方位

Placement.PlacementEnum

export type PlacementEnum =
  | 'top'
  | 'left'
  | 'right'
  | 'bottom'
  | 'topLeft'
  | 'topRight'
  | 'bottomLeft'
  | 'bottomRight'
  | 'leftTop'
  | 'leftBottom'
  | 'rightTop'
  | 'rightBottom';

总结

so easy!  以后再也不用把新手引导写的到处都是了,一劳永逸,这个故事告诉我们,一定要在任何时候都想着偷懒,万一下次再遇到新手引导,我就可以愉快的划水了

参考:

新手引导动画的4种实现方式:juejin.cn/post/684490…

Hi~ 这将是一个通用的新手引导解决方案:juejin.cn/post/696049…