现在在手机等移动端设备访问的人越来越多,我们前端开发者一直致力于将设计稿还原成页面,供用户访问。但除高度还原设计稿外,交互上的良好体验也是我们应该做到的。 我们在玩游戏的过程中,通常都会遇到一个词:“ 在前端页面中,也应当像游戏中的 例如用户在点击页面中的按钮时,按钮最好有一种被按下的效果: 若按钮被下压的效果不太适合页面整体的风格,您也可以做一个背景颜色上的变化。 每个用户的设备型号、网络状态等情况都不一样,我们不能要求每个用户都在良好的 WiFi 下操作我们的页面。 若用户的某个行为产生了网络请求,并要根据请求返回的结果,反馈给用户。这种情况,页面都应当给用户一种 我们可以在这里给自己定下一条规则: 凡是有网络请求的情形,均要有 loading 效果的持续性反馈。 我们通常可以在用户触发的按钮上展示 loading 效果,也可以在全局页面上展示 loading 效果,这个根据每个页面的风格自行选择即可。 例如页面上有个红包需要点击按钮开启,当用户点击按钮后,按钮就可以展示出一个旋转的 loading 效果,待接口返回结果再打开红包,展示具体的金额,或者其他的结果。 在现在大部分前后端分离的场景下(同时没有使用同构直出方案),都是先展示出一个没有数据的前端页面,然后请求数据,待数据返回后再渲染页面。 这种情形和上面 1.2 中是一样的,不过这个是在刚进入页面就触发的!这里我们也是要展示出 loading 效果的,只不过是 loading 展示的时机的问题。 这两种方式也是各有不同的使用场景,就我个人而言,我更喜欢第 2 种方式,能够第一时间将页面中的元素展示给用户;但如果页面布局因接口的数据改变较大,建议还是采用第 1 种方式,这样 loading 结束时,不会出现页面大幅度闪动的感觉。 我们拿到接口的数据后,通常会有两种展示状态: 比如一个展示奖品列表中数据中,这里我们通常会初始化一个 list 变量来接收接口返回的数据: 在请求接口的过程中,页面会展示什么?“暂无数据”,给用户的第一视觉感受就是:我的奖品丢了。等过一会儿接口返回数据了,然后又重新将数据展示出来。 这里,我们就忽略了一个很重要的状态: 这里我也不太想好用个什么名字,概况来说,告诉用户刚才发生了什么,将用户操作可视化, 来增强用户对操作行为的感知度, 同时也能对元素内容的认知。 因用户行为产生的新交互,应当与当前用户的行为相关。 用户点击按钮后,会弹出一个弹窗,弹窗可以从按钮所在的方向或者位置,弹出到整个页面的中心。 给到用户的感受就是该弹窗与按钮是相关的。 例如在一个表格或者列表中,有新增、修改或者删除一行(一列)的行为,可以用一个动画和背景色来区分该元素, 过一段时间再恢复正常。 在不添加任何 CSS 属性时,滑动有滚动条区域时,总感觉有一种卡顿感,就是手指滑动时页面就跟着滑动,手指离开则页面停止滑动。 这里我们添加上一个属性即可: 在现在手机屏幕越来越大的趋势下,单手握持手机时,大模板只能在以左下角或者右下角为中心的区域活动。因此,在底部区域操作的情况越来越多,例如底部区域的导航,弹窗中点击空白区域即可关闭等等。 在一个可滚动的页面中,呼起一个弹窗,这个弹窗中的内容也比较多,也需要滚动,如果不加处理的话,可能会造成两个区域同时滚动,体验不好。也就是避免滚动穿透。 这里我们就要把底层的滚动锁住,只可以滚动处在最上层的区域。这里的原理我就不多讲解,推荐一个我一直在使用的组件tua-body-scroll-lock,该组件导出了 2 个方法: 在 react 中的使用方式: 同时的,我们最好在遮罩区域添加可以关闭弹窗的操作,避免用户伸手够弹窗右上角的关闭按钮。 在移动端开发中,下拉框我们使用原生 select 标签时,iOS 和 Android 的表现是不一样的,iOS 会出现在屏幕的底部,滚动选择某个选项;而 Android 中,则是屏幕中间弹出一个弹层,然后可以进行选择。 如果图方便的话,其实可以使用原生的 select 标签。但这种方式,总感觉与页面元素之间产生了割裂,因此如果可以的话,尽量模拟出一个 select 标签。 每个用户的设备型号、网络状态等情况都不一样。总会因为各种各样的原因,导致页面展示异常。因此,我们应当做好提示和一些兜底策略。 通常情况下,在移动端 APP 中打开的页面,顶部都会有一个白色的标题栏。但有些活动页面为了更好地沉浸式体验,会把白色标题栏去掉,同时还去掉了右划退出的操作,只能点击自定义的返回按钮才能退出。 例如这个页面,左上角的返回按钮是页面本身自定义的。而这个页面必须是接口正常返回数据后才展示出来,在最开始时,如果有异常时,会展示错误信息,但没有返回按钮。这就导致用户无法退出该活动,只能杀掉 APP 再重新进入。 体验非常不好,这里我们就要保证:全屏沉浸式页面不管是哪种状态,应当全程保持关闭操作! 当然,现在已经没有这个问题了。 后台经常说的一句话是“不要相信任何从前端传过来的数据”,我们也一样: 永远不要相信后台一直很稳定。 我们要做好接口服务可能会挂掉的预案: 前 3 种我们都可以理解,当接口异常并无法继续后续的操作时,应当告知用户有服务有异常了,可以稍后重试。 对于第 4 种,通常可能会发生在高并发的抽奖过程中,越是让用户重试,并发量就越高。因此在抽奖异常时,可以直接告诉用户未中奖,而不是“服务异常”之类的话术。要不然,一方面会引起用户的不满,另一方面会造成用户的大量重试。 这个百度在春晚发红包中,就有用到过,在服务器短时间内承受到高并发量时,则直接告诉用户未抽中红包;同时,对于一些抽奖会同时发放多个奖品时,也要做好每个奖品服务都可以会挂掉的准备,比如同时会发放 3 个奖品: 千万不要留有空间或者槽位告诉用户“该位置本应该有奖品,但实际上没有”的感觉。 懒加载是一个老生常谈的话题,这里我们只针对图片懒加载来进行梳理。 在页面中图片比较多时,请尽量使用图片懒加载,并考虑好图片加载失败的情况,可以先创建一个 Image 来先加载图片,加载城后再给到页面中的 dom 元素,否则使用兜底图片: 同时,我们在体验的过程中发现,在有些华为手机里,图片还没加载完毕时,会展示一个裂开的图片,如果该图片 alt 注释,也把 alt 注释显示出来,稍过一会儿,等图片加载完毕了,就正常展示图片了。 这种情况,我们也可以使用图片懒加载,或者将图片设置为背景图片,避免出现图片裂开的状态。 我们在移动端开发的过程中,总会有多种解决方案。如果我们站在用户的角度多想一想,就能让产品的交互体验变的更好。1. 即时反馈 #
打击感
”,通俗的理解就是我们做出的每一个操作,都有很强烈的反馈,比如视觉上的动画变化,听觉上产生的声音,或者移动设备的震动感等。1.1 按钮的即时反馈 #
打击感
一样,用户任何的操作都应当予以即时的反馈,告诉用户他的操作是有效的,系统已收到他的操作,内部正在处理中。button:active {
transform: translateY(4px);
}
1.2 持续性的反馈 #
持续性的反馈
,表示一个动作正在后台执行。如果没有这种效果,即使已经在请求接口了,用户也会认为点击没有反应,会多次的去点击按钮,以期望得到响应。1.3 页面初始化 #
1.4 数据的展示 #
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 丝滑的滑动跟随 #
body {
-webkit-overflow-scrolling: touch;
}
3. 考虑移动设备的握持姿势 #
3.1 避免滚动穿透 #
useEffect(() => {
// 锁定body的滚动,只在弹窗内部滚动
// 只有需要设置可以滚动区域时,才使用该方法
if (props.scrollContainer) {
lock(props.scrollContainer);
}
return () => {
if (props.scrollContainer) {
unlock(props.scrollContainer);
}
};
}, [props.scrollContainer]);
3.2 原生 select 标签的使用 #
4. 良好的兜底策略 #
4.1 全屏沉浸式页面应当保持关闭操作 #
4.2 永远不要相信后台一直很稳定 #
4.3 懒加载 #
// 判断图片是否可以加载成功
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', '');
}
};
5. 总结 #
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论