• 原文地址:新手引导功能的四种姿势
  • 原文作者:清夜
  • 温馨提示:如果你对文章有歧义或者建议,欢迎来 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.clientHeightconst clientWidth = document.documentElement.clientWidth || document.body.clientWidthsetMaskTop({ 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.clientHeightconst clientWidth = document.documentElement.clientWidth || document.body.clientWidthsetMaskBox({'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.clientHeightconst clientWidth = document.documentElement.clientWidth || document.body.clientWidthconst rX = rect.left + rect.width / 2const 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 = 0let shadowItem = nullwhile (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 : 1for (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.rightconst 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.clientHeightconst clientWidth = document.documentElement.clientWidth || document.body.clientWidthsetWidth(clientWidth)setHeight(clientHeight)const rect = targetRef.current.getBoundingClientRect()const c = canvasRef.currentconst 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

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

新手引导功能的四种姿势相关推荐

  1. Springboot 解决跨域的四种姿势

    Springboot 解决跨域的四种姿势 姿势一 实现WebMvcConfigurer#addCorsMappings的方法 import org.springframework.context.an ...

  2. 尝试从redis未授权访问到getshell的四种姿势(失败)

    尝试从redis未授权访问到getshell的四种姿势(失败) 使用Goby扫描某SRC的C段,发现一个Redis未授权,经验证漏洞存在,故尝试Getshell提高危害(拿更多赏金) 虽然Getshe ...

  3. Python 读取文件四种姿势:实例详解,从此读取文件更得心应手

    Python文件读取的四种方式如下: 1. 逐行读取文件内容 逐行读取文件内容可以使用 for 循环和文件句柄对象,如下所示: with open('file.txt', 'r') as f:for ...

  4. 某金融产品妹纸:四种姿势教你跪舔程序员不要不要的

    我 是一个妹纸 是一位互联网金融产品的产品经理 谈起跟程序员哥哥死磕的经历 我先征求了一下朋友们意见 结果 ...... 难道程序员和产品经理真的到了只能互相砍杀的地步了吗?在工作中,我遇到了几种不同 ...

  5. 物联网的基本功能和四种形态

    物联网的最基本功能特征,提供"无处不在的连接和在线服务",具备十大基本功能,如下: 在线监测:这是物联网最基本的功能,物联网业务一般以集中监测为主.控制为辅. 定位追溯:一般基于传 ...

  6. python爬取网站四种姿势_python爬取网站数据四种姿势,你值得拥有~

    前言 首先,分析来爬虫的思路:先在第一个网页(https://www.wikidata.org/w/index.php?title=Special:WhatLinksHere/Q5&limit ...

  7. Gmail新增新功能 支援四种语言等智能功能

    mail是Google最成功的产品之一,推出至今满15周年,为了庆祝Gmail这15年的进展,Google又为Gmail新增了几项新功能,来协助用户更有效率地工作,包含智能写信(Smart Compo ...

  8. 抖音小姐姐属于你的四种姿势

    作 者:煌金的咸鱼 来 源:咸鱼学Python 广而告之:由于此订阅号换了个皮肤,系统自动取消了读者的公众号置顶.导致用户接受文章不及时.您可以打开订阅号,选择置顶(标星)公众号.重磅干货,第一时间送 ...

  9. 一文搞懂四种 WebSocket 使用方式

    在上家公司做IM消息系统的时候,一直是使用 WebSocket 作为收发消息的基础组件,今天就和大家聊聊在 Java 中,使用 WebSocket 所常见的四种姿势,如果大家以后或者现在碰到有要使用 ...

最新文章

  1. [摘]终于找到一个有助理解left/right/full outer join的例子
  2. 总结 | 机器学习的通俗讲解!
  3. LINUX API函数大全
  4. Java 优先级的问题
  5. 上海11月份计算机方面的会议,计算机类 | 10月截稿会议信息6条
  6. 智能车s3010舵机工作电压_全国大学生智能车竞赛决战南京信息工程大学
  7. IE浏览器怎么在桌面隐藏图标
  8. 任女尔(1990-),女,北京卡达克数据技术中心软件业务本部助理工程师,主要研究方向为大数据、云计算。...
  9. 图解linux启动过程
  10. case when 子查询_标准SQL——子查询、分组查询、多表联查
  11. web服务器、Apache 和 tomcat 的关系
  12. flash中zip/unip的实际意图
  13. 修复WordPress中的Cookie阻止错误
  14. mdf导入mysql navicat_Navicat如何导入sql server的MDF文件和LDF文件
  15. 电脑如何查看如何无限WiFi密码?
  16. 华容道源代码android,基于Android的华容道游戏的源代码
  17. win8计算机可用内存不足,win8电脑运行提示物理内存不足的解决方法
  18. linux day03 基础知识学习
  19. php-tanita,♥【停更】 ♥【 鉴于有些人把分享了的二次分享到别的论坛在此停更】♥♥【Tanitas8专贴】♥♥...
  20. access身份证号掩码_关于二级access的输入掩码!

热门文章

  1. 云服务器可以修改ip,云服务器的ip可以更换吗
  2. 阿里云服务器通用型实例g5与g6区别及如何选择?
  3. 如何准备OCP考试?
  4. springboot集成配置swaggerUI
  5. 52o1314小符号_特殊名字符号(52o1314特殊上下组合小符号)
  6. Scratch3.0 二次开发(4)修改界面字体大小
  7. 计算机虚拟内存的用途,虚拟内存有什么作用?
  8. 字符串中的 ↵ 回车符替换
  9. 爬取百度贴吧图片-python-requests
  10. Linux获取最后一个参数(参数统计)