1. 即时反馈
我们在玩游戏的过程中,通常都会遇到一个词:“打击感”,通俗的理解就是我们做出的每一个操作,都有很强烈的反馈,比如视觉上的动画变化,听觉上产生的声音,或者移动设备的震动感等。
1.1 按钮的即时反馈
在前端页面中,也应当像游戏中的打击感一样,用户任何的操作都应当予以即时的反馈,告诉用户他的操作是有效的,系统已收到他的操作,内部正在处理中。
例如用户在点击页面中的按钮时,按钮最好有一种被按下的效果:
button:active { transform: translateY(4px); }
若按钮被下压的效果不太适合页面整体的风格,您也可以做一个背景颜色上的变化。
1.2 持续性的反馈
每个用户的设备型号、网络状态等情况都不一样,我们不能要求每个用户都在良好的 WiFi 下操作我们的页面。
若用户的某个行为产生了网络请求,并要根据请求返回的结果,反馈给用户。这种情况,页面都应当给用户一种持续性的反馈,表示一个动作正在后台执行。如果没有这种效果,即使已经在请求接口了,用户也会认为点击没有反应,会多次的去点击按钮,以期望得到响应。
我们可以在这里给自己定下一条规则:
凡是有网络请求的情形,均要有 loading 效果的持续性反馈。
我们通常可以在用户触发的按钮上展示 loading 效果,也可以在全局页面上展示 loading 效果,这个根据每个页面的风格自行选择即可。
例如页面上有个红包需要点击按钮开启,当用户点击按钮后,按钮就可以展示出一个旋转的 loading 效果,待接口返回结果再打开红包,展示具体的金额,或者其他的结果。
1.3 页面初始化
在现在大部分前后端分离的场景下(同时没有使用同构直出方案),都是先展示出一个没有数据的前端页面,然后请求数据,待数据返回后再渲染页面。
这种情形和上面 1.2 中是一样的,不过这个是在刚进入页面就触发的!这里我们也是要展示出 loading 效果的,只不过是 loading 展示的时机的问题。
1.先一个全局 loading 的开启页,在数据没有返回回来时,看不到任何相关活动元素;
2.先用初始化的假数据或者兜底数据,渲染一个基本框架,然后在某个位置展示 loading 效果,并请求数据,数据返回后再替换假数据进行渲染。
这两种方式也是各有不同的使用场景,就我个人而言,我更喜欢第 2 种方式,能够第一时间将页面中的元素展示给用户;但如果页面布局因接口的数据改变较大,建议还是采用第 1 种方式,这样 loading 结束时,不会出现页面大幅度闪动的感觉。
1.4 数据的展示
我们拿到接口的数据后,通常会有两种展示状态:
1.无数据,进行“暂无数据”之类的提示;
2.有数据,正常展示数据;
比如一个展示奖品列表中数据中,这里我们通常会初始化一个 list 变量来接收接口返回的数据:
const List = () => { const [list, setList] = useState([]); useEffect(() => { // 设置数据 // setList([]); }, []); return ( <div className="list"> {list.length ? ( <div className="container"> {list.map((item) => ( <div key={item.key}>{item.title}</div> ))} </div> ) : ( <div className="nothing">暂无数据</div> )} </div> ); };
在请求接口的过程中,页面会展示什么?“暂无数据”,给用户的第一视觉感受就是:我的奖品丢了。等过一会儿接口返回数据了,然后又重新将数据展示出来。
这里,我们就忽略了一个很重要的状态:loading状态。因为“暂无数据”,也是一种结果,不是过程,是要告诉用户,您当前是没有数据的。因此,不能把“暂无数据”作为 loading 状态来展示。
const List = () => { const [loading, setLoading] = useState(true); const [list, setList] = useState([]); useEffect(() => { // 设置数据 // setList([]); setLoading(false); // 请求完接口,再把loading状态取消,该展示什么结果就展示什么 }, []); if (loading) { return ( <div className="list"> <div className="loading">请求数据中...</div> </div> ); } return ( <div className="list"> {list.length ? ( <div className="container"> {list.map((item) => ( <div key={item.key}>{item.title}</div> ))} </div> ) : ( <div className="nothing">暂无数据</div> )} </div> ); };
2. 行为跟随
这里我也不太想好用个什么名字,概况来说,告诉用户刚才发生了什么,将用户操作可视化, 来增强用户对操作行为的感知度, 同时也能对元素内容的认知。
因用户行为产生的新交互,应当与当前用户的行为相关。
2.1 点击按钮后呼起弹窗
用户点击按钮后,会弹出一个弹窗,弹窗可以从按钮所在的方向或者位置,弹出到整个页面的中心。
给到用户的感受就是该弹窗与按钮是相关的。
2.2 列表中有对象变动时
例如在一个表格或者列表中,有新增、修改或者删除一行(一列)的行为,可以用一个动画和背景色来区分该元素, 过一段时间再恢复正常。
2.3 丝滑的滑动跟随
在不添加任何 CSS 属性时,滑动有滚动条区域时,总感觉有一种卡顿感,就是手指滑动时页面就跟着滑动,手指离开则页面停止滑动。
这里我们添加上一个属性即可:
body { -webkit-overflow-scrolling: touch; }
3. 考虑移动设备的握持姿势
在现在手机屏幕越来越大的趋势下,单手握持手机时,大模板只能在以左下角或者右下角为中心的区域活动。因此,在底部区域操作的情况越来越多,例如底部区域的导航,弹窗中点击空白区域即可关闭等等。
3.1 避免滚动穿透
在一个可滚动的页面中,呼起一个弹窗,这个弹窗中的内容也比较多,也需要滚动,如果不加处理的话,可能会造成两个区域同时滚动,体验不好。也就是避免滚动穿透。
这里我们就要把底层的滚动锁住,只可以滚动处在最上层的区域。这里的原理我就不多讲解,推荐一个我一直在使用的组件tua-body-scroll-lock,该组件导出了 2 个方法:
1.lock: 锁定区域,传入 dom 元素,则表示该 dom 区域内是可以滚动的;
2.unlock: 解除锁定,当弹窗消除时,需要解除被锁定的区域;
在 react 中的使用方式:
useEffect(() => { // 锁定body的滚动,只在弹窗内部滚动 // 只有需要设置可以滚动区域时,才使用该方法 if (props.scrollContainer) { lock(props.scrollContainer); } return () => { if (props.scrollContainer) { unlock(props.scrollContainer); } }; }, [props.scrollContainer]);
同时的,我们最好在遮罩区域添加可以关闭弹窗的操作,避免用户伸手够弹窗右上角的关闭按钮。
3.2 原生 select 标签的使用
在移动端开发中,下拉框我们使用原生 select 标签时,iOS 和 Android 的表现是不一样的,iOS 会出现在屏幕的底部,滚动选择某个选项;而 Android 中,则是屏幕中间弹出一个弹层,然后可以进行选择。
如果图方便的话,其实可以使用原生的 select 标签。但这种方式,总感觉与页面元素之间产生了割裂,因此如果可以的话,尽量模拟出一个 select 标签。
4. 良好的兜底策略
每个用户的设备型号、网络状态等情况都不一样。总会因为各种各样的原因,导致页面展示异常。因此,我们应当做好提示和一些兜底策略。
4.1 全屏沉浸式页面应当保持关闭操作
通常情况下,在移动端 APP 中打开的页面,顶部都会有一个白色的标题栏。但有些活动页面为了更好地沉浸式体验,会把白色标题栏去掉,同时还去掉了右划退出的操作,只能点击自定义的返回按钮才能退出。
例如这个页面,左上角的返回按钮是页面本身自定义的。而这个页面必须是接口正常返回数据后才展示出来,在最开始时,如果有异常时,会展示错误信息,但没有返回按钮。这就导致用户无法退出该活动,只能杀掉 APP 再重新进入。
体验非常不好,这里我们就要保证:全屏沉浸式页面不管是哪种状态,应当全程保持关闭操作!
当然,现在已经没有这个问题了。
4.2 永远不要相信后台一直很稳定
后台经常说的一句话是“不要相信任何从前端传过来的数据”,我们也一样:
永远不要相信后台一直很稳定。
我们要做好接口服务可能会挂掉的预案:
1.设置请求接口的超时时间,不要让用户无限制等待;
2.良好的提示;
3.有条件时,可以自动重试,或者让用户手动尝试重试请求接口;
4.采用兜底策略遮盖;
前 3 种我们都可以理解,当接口异常并无法继续后续的操作时,应当告知用户有服务有异常了,可以稍后重试。
对于第 4 种,通常可能会发生在高并发的抽奖过程中,越是让用户重试,并发量就越高。因此在抽奖异常时,可以直接告诉用户未中奖,而不是“服务异常”之类的话术。要不然,一方面会引起用户的不满,另一方面会造成用户的大量重试。
这个百度在春晚发红包中,就有用到过,在服务器短时间内承受到高并发量时,则直接告诉用户未抽中红包;同时,对于一些抽奖会同时发放多个奖品时,也要做好每个奖品服务都可以会挂掉的准备,比如同时会发放 3 个奖品:
1.服务都正常,正常发放;
2.2 个正常,就只发放 2 个奖品,左右排列;
3.只有 1 个服务正常,则只发放 1 个奖品,居中排列;
4.均异常,则告诉用户未中奖;
千万不要留有空间或者槽位告诉用户“该位置本应该有奖品,但实际上没有”的感觉。
4.3 懒加载
懒加载是一个老生常谈的话题,这里我们只针对图片懒加载来进行梳理。
在页面中图片比较多时,请尽量使用图片懒加载,并考虑好图片加载失败的情况,可以先创建一个 Image 来先加载图片,加载城后再给到页面中的 dom 元素,否则使用兜底图片:
// 判断图片是否可以加载成功 const loadImage = (imgUrl: string): Promise<HTMLImageElement> => { return new Promise((resolve, reject) => { const img = new Image(); img.src = imgUrl; if (img.complete) { return resolve(img); } img.onload = () => { resolve(img); }; img.onerror = reject; }); }; // IntersectionObserver的回调,当dom元素进入到可是区域内时 const targetExposeCallback = async (dom: HTMLElement) => { let original = dom.getAttribute('data-original'); if (original) { try { await loadImage(original); } catch (err) { // 1x1的图片 original = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; } setLoading(false); if (dom.tagName.toLowerCase() === 'img') { dom.setAttribute('src', original); } else { // eslint-disable-next-line dom.style.backgroundImage = `url("${original}")`; } dom.setAttribute('data-original', ''); } };
同时,我们在体验的过程中发现,在有些华为手机里,图片还没加载完毕时,会展示一个裂开的图片,如果该图片 alt 注释,也把 alt 注释显示出来,稍过一会儿,等图片加载完毕了,就正常展示图片了。
这种情况,我们也可以使用图片懒加载,或者将图片设置为背景图片,避免出现图片裂开的状态。
5. 总结
我们在移动端开发的过程中,总会有多种解决方案。如果我们站在用户的角度多想一想,就能让产品的交互体验变的更好。
以上就是JS如何让你的移动端交互体验更加优秀的详细内容,更多关于JS让你的移动端交互体验更加优秀的资料请关注自学编程网其它相关文章!
- 本文固定链接: https://zxbcw.cn/post/212294/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)