Skip to content
On this page

菊花图和出错提示的正确实践

菊花图和出错提示往往是伴生的,菊花图出现之后很可能会看到出错提示,所以放在一起讲。

关于“失败提示”和“出错提示”的知识,请先阅读《API 响应规范》,现在假设你已经掌握了这些知识。简单说,失败提示就是用户操作失败后的提示,出错提示仅需考虑网络异常这一情况。

什么时候使用菊花图

  • GET 时:《首页首屏优化》一节说过:不确定高度、不确定内容数量,内容数量可能为 0 的内容区块,应使用菊花图,不要使用骨架屏和垫底素材。

  • POST、PUT、DELETE 时:视情况在按钮上添加菊花图,并且按钮置为disabled

永远不要使用全屏的菊花图

永远不要用全屏的菊花图。因为:

  1. 用户对全屏菊花图的容忍度只有 5 秒钟,而网络超时的时长往往设定是 30 秒(为了让用户利用这个时间从弱网环境走到强网环境),让全屏菊花图旋转 30 秒会让用户发疯。

  2. 如果菊花图的存在时间少于半秒,那么会发生一次全屏闪烁,用户无法容忍全屏闪烁。

  3. 如果页面有多个区块,分别包含网络请求,那么全屏菊花图是代表哪个区块的加载动画呢?如果一个请求出错,另一个健康,那么菊花图消失还是保留?如果保留,那么出错 Toast 跟菊花图叠加?如果消失,那么用户能快速发现是哪个区块出错吗?

正确呈现菊花图和各种提示

GETPOST、PUT、DELETE
菊花图区块最上方居中放置菊花图,可加文字加载中被点击的按钮加菊花图,且可能要给相关区块加遮罩防点击
为空、失败时提示区块最上方居中写出无内容或其他文字屏幕中央 Toast 失败原因,可能 UI 也要有变化
网络出错时提示区块最上方居中写出网络不给力,再放重新加载按钮屏幕中央 Toast 网络不给力

GET 下菊花图和各种提示解决方案

上文说过,菊花图只应当出现在 UI 区块的上方居中位置,而且与网络不给力重新加载按钮有关联。那么,如何正确地、系统地使用菊花图、各种提示、重新加载按钮呢?

准备一个标记叫做 loadingStatus

loadingStatus有 4 种状态(并不是简单的truefalse):

  1. 空串:发起请求之前默认是空串,或者当有结果且结果有内容,也就是最常见的情况,此时应重置为空串。

  2. pending:当请求未决时,应设为pending,然后提示菊花图即可。

  3. empty:当请求有结果且是内容为空时,应设为empty,然后提示没有内容即可。

  4. networkError:当请求有结果且是网络出错时,应设为networkError,然后提示网络不给力即可。

注意:

如果一个页面有多个区块要加载,可以给loadingStatus加修饰词,比如文章和评论分别加载的时候:

js
export default {
  data() {
    return {
      articleLoadingStatus: '',
      article: {},
      commentLoadingStatus: '',
      commentList: [],
    };
  },
};

Vue HTML 模板范例

根据“内容区和异常的关系”以及“旧数据与新数据的关系”可分 3 种情况讨论,本文只讨论前两种:

内容与异常的关系新数据与旧数据的关系解释解决方案
当异常显现则内容区隐藏新数据替换旧数据最简单的情况,当数据极少且数量固定时使用见下文
内容区始终显现,异常以遮罩盖住内容区新数据替换旧数据PC 端常见于表格,移动端常见于内容极少的场合见下文
内容区始终显现,异常显示在内容区尾部新数据追加到旧数据尾部即滚动加载,PC 端罕见,移动端常见用组件库
  1. 假设当异常显现则内容区隐藏。以加载一个笔记列表为例,伪代码如下:
html
<div v-if="noteLoadingStatus === ''">
  <div v-for="item in noteList" :key="item.id">
    <!-- item 的内容 -->
  </div>
</div>

<div v-else>
  <u-loading-icon v-if="noteLoadingStatus === 'pending'"></u-loading-icon>
  <div v-else-if="noteLoadingStatus === 'networkError'">
    <div>网络不给力</div>
    <u-button text="重新加载" class="margin-top-20" @click="getNoteList"></u-button>
  </div>
  <u-empty v-else mode="list" text="暂无内容"></u-empty>
</div>
  1. 假设异常以遮罩覆盖内容区,且新数据替换旧数据。伪代码如下:
html
<div class="relative">
  <div v-for="item in noteList" :key="item.id">
    <!-- item 的内容 -->
  </div>

  <div class="absolute w-100 top-0">
    <u-loading-icon v-if="noteLoadingStatus === 'pending'"></u-loading-icon>
    <div v-else-if="noteLoadingStatus === 'networkError'">
      <div>网络不给力</div>
      <u-button text="重新加载" class="margin-top-20" @click="getNoteList"></u-button>
    </div>
    <u-empty v-else mode="list" text="暂无内容"></u-empty>
  </div>
</div>

解释一下:

  1. 将 ajax 的超时时间设为 30 秒,因为手机存在弱网可能性。(题外话:如果是 PC 平台就没必要 30 秒那么久,毕竟 PC 网络相对说非常稳定,我个人习惯是设为 11 秒。)

  2. 发送请求前,noteLoadingStatus设为pending

  3. 假设 30 秒内收到响应,且数据有内容,那么noteLoadingStatus重置为空串,且v-for生效,于是看到了正确内容。

  4. 假设 30 秒内收到响应,但数据没内容,则noteLoadingStatus设为empty,这时候模板显示下方分支,屏幕显示暂无内容的分支。

  5. 假设 30 秒内没有响应,则noteLoadingStatus设为networkError,提示网络不给力

  6. 相当重要的一点:在网络不给力的下方,必须放上重新加载按钮,毕竟 Webview 是没有浏览器的“刷新”按钮的。

Vue JS 范例

伪代码如下,不解释:

js
getNoteList() {
  this.noteLoadingStatus = 'pending';
  return this.$api.getNoteList().then(
    response => {
      if (response.data && response.data.length) {
        this.noteList = response.data;
        this.noteLoadingStatus = '';
      } else {
        this.noteLoadingStatus = 'empty';
      }
    },
    () => {
      this.noteLoadingStatus = 'networkError';
    }
  );
},

POST PUT DELETE 下菊花图和各种提示解决方案

loadingStatus 只需要 true 和 false 状态

与 GET 下的loadingStatus相比,POST PUT DELETE 因为只需要 Toast ,不需要在界面上保持状态,所以不需要那么多状态,仅truefalse足够。

Vue HTML 模板范例

以删除表格一行,也就是删除一条记录为例,伪代码如下:

html
<van-button :disabled="delNoteLoading" @click="onClickDel">删除</van-button>

Vue JS 范例

伪代码如下,不解释:

js
delNote() {
  this.delNoteLoading = true;
  return this.$api.delNote({/* 参数 */}).then(
    response => {
      if (/* response.code 命中成功 code */) {
        // ... 此处省略后续
      } else {
        // 如果没有失败可能性,则省略此分支
        // 如果有 1 个分支,则直接处理,比如 Toast(response.msg);
        // 如果有多个分支,则需要 switch...case... 处理
      }
      this.delNoteLoading = false;
    },
    () => {
      Toast('网络不给力');
      this.delNoteLoading = false;
    }
  );
},

杨亮的前端解决方案