- 原文地址:老手疏导性能的四种姿态
- 原文作者:清夜
- 舒适提醒:如果你对文章有歧义或者倡议,欢送来 Github 里提 PR 或者 Issue 一起保护文章~
- 文章著作权归原文作者所有,转载请注明出处哦~
产品新性能的开发,个别须要遵循的其中两条准则:
- 不要信赖用户
这个意思并不是说要把用户当成敌人,而是站在系统安全的角度,更具体点说,是不要信赖用户的任何输出,尽可能地不要在零碎中留下任何破绽,根本的比方 csrf、xss 的防备,这条准则个别由后端来保障 - 将用户当成傻子
同样不是友好用户的意思,而是要求产品能从用户的角度去适应用户的行为,升高用户的应用门槛,比方表单填写的各种提醒、显眼的提交按钮等,因为波及到与用户的间接接触,所以这一条次要从前端层面进行保障
可能对于常常跟互联网产品打交道的你我来说,对于一个新性能甚至是新产品能够做到边应用边摸索边学会,但你不能保障所有人都具备这个能力,很多人基本不晓得 Ctrl + F
是啥,更不可能晓得浏览器的兼容模式是什么意思,如果产品的流量够大,那么哪怕只有 1%
的用户在应用产品时遇到困难,都是不小的损失
针对这种状况,早就有了很多成熟的解决方案,比方,老手疏导
如上图,其实就是一个遮罩层加一个弹窗,只不过比个别弹窗略微简单一点的是,遮罩层之上除了弹窗之外,还有须要高亮疏导的元素区域,并且弹窗的地位也并不是固定的,而是追随高亮疏导区域进行定位
解释性弹窗这块没什么可说的,次要是遮罩层这块略微有点意思,最近我刚好遇到了这个需要,所以就多思考了一下,发现这个这遮罩层的实现思路倒是不少~
第一种姿态 - mask 拼接
看图谈话
原谅我的灵魂绘图b( ̄▽ ̄)d
上图整个大矩形看成是浏览器页面,页面区域被五个区块瓜分:top
、right
、bottom
、left
以及 新性能区域
其中 新性能区域
元素就是咱们所要展现的新性能区域,而 top
、right
、bottom
、left
则都是遮罩元素,它们一起将除了 新性能区域
这块区域之外的残余区域填满,这样看起来可能像是页面上有一个充斥整个页面区域的遮罩层,而后 新性能区域
悬浮在这个遮罩层之上,其实并不是
起因呢也很简略,新性能区域
元素并不是悬浮在页面之上的弹窗,而是位于页面的主体文档流中,一般而言不会是悬浮的,也不会被动设置一个比拟大的 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
还是间接上图
这种实现办法只用到了一个额定元素,脑洞也略微大一点,第一种办法中的 top
、right
、bottom
、left
四个元素别离应用一个元素的四条边(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.clientHeightconst clientWidth = document.documentElement.clientWidth || document.body.clientWidthconst 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> )}
相比于第二种姿态的 border
,canvas
就更厉害啦,能够进行任意形态元素的高亮,只有你晓得怎么画进去就行
The End
只是一个小小的新性能疏导就有那么多的实现形式,可见前端的确是灵便又乏味的,大家平时在做需要的时候都能够多加思考一二,不要总是局限于以往的思维,放开脑洞,或者就能找到更好的解决方案~