学完这 4 个小技巧,让你的移动端交互体验更加优秀

蚊子前端博客
发布于 2021-02-22 14:34
现在在手机等移动端设备访问的人越来越多,交互上的良好体验也变得越来越重要

现在在手机等移动端设备访问的人越来越多,我们前端开发者一直致力于将设计稿还原成页面,供用户访问。但除高度还原设计稿外,交互上的良好体验也是我们应该做到的。

玩玩手机-蚊子的前端博客

1. 即时反馈

我们在玩游戏的过程中,通常都会遇到一个词:“打击感”,通俗的理解就是我们做出的每一个操作,都有很强烈的反馈,比如视觉上的动画变化,听觉上产生的声音,或者移动设备的震动感等。

1.1 按钮的即时反馈

在前端页面中,也应当像游戏中的打击感一样,用户任何的操作都应当予以即时的反馈,告诉用户他的操作是有效的,系统已收到他的操作,内部正在处理中。

例如用户在点击页面中的按钮时,按钮最好有一种被按下的效果:

COPYCSS

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 变量来接收接口返回的数据:

COPYJAVASCRIPT

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 状态来展示。

COPYJAVASCRIPT

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 属性时,滑动有滚动条区域时,总感觉有一种卡顿感,就是手指滑动时页面就跟着滑动,手指离开则页面停止滑动。

这里我们添加上一个属性即可:

COPYCSS

body { -webkit-overflow-scrolling: touch; }

3. 考虑移动设备的握持姿势

在现在手机屏幕越来越大的趋势下,单手握持手机时,大模板只能在以左下角或者右下角为中心的区域活动。因此,在底部区域操作的情况越来越多,例如底部区域的导航,弹窗中点击空白区域即可关闭等等。

3.1 避免滚动穿透

在一个可滚动的页面中,呼起一个弹窗,这个弹窗中的内容也比较多,也需要滚动,如果不加处理的话,可能会造成两个区域同时滚动,体验不好。也就是避免滚动穿透。

这里我们就要把底层的滚动锁住,只可以滚动处在最上层的区域。这里的原理我就不多讲解,推荐一个我一直在使用的组件tua-body-scroll-lock,该组件导出了 2 个方法:

  • lock: 锁定区域,传入 dom 元素,则表示该 dom 区域内是可以滚动的;

  • unlock: 解除锁定,当弹窗消除时,需要解除被锁定的区域;

在 react 中的使用方式:

COPYJAVASCRIPT

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 标签。但这种方式,总感觉与页面元素之间产生了割裂,因此如果可以的话,尽量模拟出一个 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 元素,否则使用兜底图片:

COPYJAVASCRIPT

// 判断图片是否可以加载成功 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. 总结

我们在移动端开发的过程中,总会有多种解决方案。如果我们站在用户的角度多想一想,就能让产品的交互体验变的更好。

手机没电了-蚊子的前端博客

标签:
阅读(937)

公众号:

qrcode

微信公众号:前端小茶馆

公众号:

qrcode

微信公众号:前端小茶馆