前言
最好的用户体验是即时的,因此追求更短的等待时间和更快的反馈非常重要,然而匆忙的将任何信息显示给用户未必是最佳体验,举例来说:
- 进入网页
- 反馈「加载数据中……」
- 显示加载完成的数据
以上流程简洁实在,会有什么问题呢?问题出在时机和时间。
不要呈现尴尬的体验给用户
用户无法在 30 毫秒内了解加载发生后的结束,只会看到一团信息闪过
追求尽可能即时的反馈是好事,但如果加载信息因为「反馈即时」而很快过期,只会变成一团无法理解的噪音体验,思考:
- 加载体验的形式
- 用户需要花多少时间理解
- 加载体验存在的必要与意义
用户对感知的经验法则
像是 0.1 / 1 / 10 秒的概念来自 Response Times: The 3 Important Limits 分割出了三种体验的定义:
- 0.1 秒:即时感
- 1 秒:流畅感
- 10 秒:耐心极限
虽然不同群体、时代、个体和加载体验对于都有不同的标准和影响,但还是可以有相关的概念。
不同形式的加载体验
为了避免用户等待造成不好的体验,界面构建适当的反馈是必要的,具体来说常见的加载界面有以下几种,它们有各自的适用场景和取舍:
- 加载文字 Text Loader:一串文字用于告知加载状态
- 加载图示 Loader / Spinner:一个动画图示用于告知加载中的状态
- 进度条 Progress Bar:一个进度条用于告知加载的进度
- UI 骨架 UI Skeleton:一个低仿真的闪烁界面告知加载中的状态
像是 UI 骨架就能很好的缓和加载和成果的边界,且有极低的认知负担,甚至不太需要额外的动画过渡缓和体验,但并不一定适用任何界面。
最低加载时间代码实践
举例三个显示水果清单的组件,个别有 10 / 100 / 1000 毫秒的响应时间:
<FruitList :response-time="10"></FruitList><FruitList :response-time="100"></FruitList><FruitList :response-time="1000"></FruitList>
<template> <div class="fruit-list"> <h2>水果清单</h2>
<div class="actions"> <button @click="fetchFruits" :disabled="loading"> {{ loading ? '加载水果清单中…' : '获取水果清单' }} </button> <button @click="clear" :disabled="loading"> 清除水果清单 </button> </div>
<div class="status"> <p v-if="loading">等待响应...</p> </div>
<ul v-if="fruits.length && !loading"> <li v-for="(fruit, index) in fruits" :key="index">{{ fruit }}</li> </ul>
<p v-else-if="!loading && !fruits.length">目前没有水果数据。</p> </div></template>
<script setup lang="ts">import { ref, onMounted } from 'vue'
const { responseTime } = defineProps<{ responseTime: number}>()
type Fruit = string
function getFruits(): Promise<Fruit[]> { return new Promise((resolve, reject) => { setTimeout(() => { resolve(['苹果', '香蕉', '芒果', '奇异果']) }, responseTime) })}
const fruits = ref<Fruit[]>([])const loading = ref(false)
async function fetchFruits() { loading.value = true try { const result = await getFruits() fruits.value = result } catch (err: unknown) { fruits.value = [] } finally { loading.value = false }}
function clear() { fruits.value = []}
onMounted(() => { fetchFruits()})</script>
很明显 10 毫秒的加载时间对大多数人来说理解都极为吃力,通过制作 promiseWithMinimumTime
函数来在合理的时机返回结果。
- 输入:触发函数与最短 n 毫秒触发时间
- 计算:
- 大于 n 就执行
- 小于 n 就于 n 执行
<FruitList :response-time="10" :min-loading-time="100" /><FruitList :response-time="100" :min-loading-time="100" /><FruitList :response-time="1000" :min-loading-time="100" />
const { loading: isLoading, result: fruits, load: loadFruitList } = useLoadWithMinimumTime(getFruits, { minimumTime: minLoadingTime,})
import { ref, type Ref } from 'vue'
function promiseWithMinimumTime<T>(promise: Promise<T>, minimumTime: number): Promise<T> { return new Promise((resolve, reject) => { const startTime = Date.now()
promise .then(result => { const elapsedTime = Date.now() - startTime const remainingTime = minimumTime - elapsedTime
if (remainingTime > 0) { setTimeout(() => { resolve(result) }, remainingTime) } else { resolve(result) } }) .catch(error => { const elapsedTime = Date.now() - startTime const remainingTime = minimumTime - elapsedTime
if (remainingTime > 0) { setTimeout(() => { reject(error) }, remainingTime) } else { reject(error) } }) })}
interface UseLoadWithMinimumTimeOptions { minimumTime?: number}
export function useLoadWithMinimumTime<T>( promiseFn: () => Promise<T>, options: UseLoadWithMinimumTimeOptions = {},) { const { minimumTime = 0 } = options
const loading = ref(false) const error = ref('') const result = ref<T>()
async function load() { loading.value = true error.value = ''
try { const promise = promiseFn() const promiseWithMinTime = promiseWithMinimumTime(promise, minimumTime) const promiseResult = await promiseWithMinTime result.value = promiseResult } catch (err: unknown) { result.value = undefined if (err instanceof Error) { error.value = err.message } else if (typeof err === 'string') { error.value = err } else { error.value = '发生了一个未知错误。' } } finally { loading.value = false } }
return { loading, error, result, load, }}
总结
每个 UI 改变状态时都应该适当的时间甚至是动画辅助理解。
如果用户对于操作界面不熟悉或是有闲暇时间思考体验上的问题可以考虑:
- 大多数用户已经习惯相关 UI 体验的问题
- 额外的逻辑要理解与维护
- 没有太多实质性的效益,更多是对用户体验的洞察与细微改善
延伸阅读
- You Don’t Need Animations - Emil Kowalski
- Should a “loading” text or spinner stay a minimum time on screen?