背景
新手引导,小伙伴们一定见过不下n次,新功能上线或者系统使用引导,都可能会用到它。与核心业务逻辑没有太大关系,但却尤为重要。中页面上的某个元素高亮并将其他部分用蒙层盖住,加上一个指引说明和下一步。工作量不大,每次都是一顿操作就加上了,但是日复一日,当我们再次遇到类似的需求,是不是非常悔恨上一次为什么不做个通用的呢? 身为程序媛,我也走过同样的路,并且这条路都快走成水泥道了、、、
所以这次下定决心,这次一定搞个通用的,给小哥哥用上!!!
下面花十分钟 实现一个新手引导 话不多说, 上效果~
非常简单,跟我来一起看看怎么实现吧!
研发思路
核心逻辑选择与框架解耦 方便在移动端和PC端都能使用 不受研发框架限制 demo显示选用Vue
技术栈:vite + vue-ts 模板 详情
首先 先把demo静态代码生成出来 因为对精细度要求不高 所以选择 codeFun 插件快速生成
选择高亮元素 复制节点 朴实无华的复制~ canvas 生成的方案在一些特定的机型上有些兼容问题
/**
* @description: 克隆目标元素
* @param {HTMLElement} element DOM节点
* @return {*}
*/
export const cloneElement = (element: HTMLElement): Node => {
const clone = element.cloneNode(true) as 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 placementOffsetMap: Placement.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.storageKey, 1))
} 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
参数 | 类型 | 描述 | 默认值 | 是否必传 |
---|---|---|---|---|
steps | Guide.Step[] | 步骤 | [] | 是 |
offset | number | 高亮元素与指引图间隙 | 8px | 否 |
storageKey | string | 数据存储key 不传不存 | '' | 否 |
Guide.Step
参数 | 类型 | 描述 | 默认值 | 是否必传 | ||
---|---|---|---|---|---|---|
element | HTMLElement | string | null | 高亮元素 | 是 | |
content | string | 提示内容 现在是张图 | 是 | |||
placement | Placement.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…