yliu

时来天地皆同力,运去英雄不自由

团队规范系列之用户体验规范


bg

最近一周的工作重心就是在梳理团队规范,在写的过程也查缺补漏了不少知识,剔除掉关于公司场景的部分就有了这一系列的文章,预计写四部分:

  1. git 规范
  2. 工程规范
  3. 用户体验规范
  4. 命名规范

用户体验规范

关于用户体验是一个很庞大的命题并且每个人对于体验的理解也各不相同,同时伴随着时效性,随着新技术的出现可能之前的体验就很快落伍了,所以下面内容只能简短概括下。

通用准则

增加可点击区域大小

1.png

以上图为例,如果必须点击到checkbox区域才能点击生效,必然会导致体验不佳

组件的考虑一致性同时也包含了页面结构,对于上图所示 label 和 checkbox 就是一个整体,这里抛砖引玉说一些常见适用该准则的元素:

  • header 区域右上角头像和姓名部分;
  • Label、li 等整体一行元素;
  • 自定义图标
  • ...

在对上面元素进行交互的时候,尽量避免用户的点击成本,下面说两个常见的做法

增大整体区域点击,例如有下面一个头像区域

<p class="portrait">
  <img class="portrait-img" src="xxx" />
  <span class="portrait-name">xxxx</span>
</p>

如果原本是点击事件绑定在.portrait-img,那么请考虑增加到.portrait

增大元素本身的点击区域

<i class="i-con"></i>

对于上面的 i 元素,通常用来设置自定义图标,但是在实际操作中,用户的鼠标或者手势可能存在偏差所以需要增加这个元素的本身范围。

回归css box的概念,我们可以增加border来完成增加区域的效果

.i-con {
  /* 省略其它代码 */
  border: 5px solid transparent;
}

注意:pc 上还需要考虑光标的一致性,以及 hover 等一系列整体的效果

图片相关优化

对于很多项目而言,图片是常见的优化点,毕竟多方面的优化远没有压缩一张图片来的直观,而对于用户体验来说也同样是

预加载

不同于懒加载,预加载的目的就是提前输出图片,避免用户等待。

举一个常见的例子:轮播图就很适合使用预加载,否则用户看到下一张图片的时候还继续等待加载就会感觉很突兀。

预加载的原理就是提前请求,之后重复请求相同资源时会返回缓存文件,我们可以封装一个方法,利用Image对 src 赋值即可

const preloading = (src) => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = src;
    img.onload = resolve;
    img.onerror = reject;
  });
};

错误图片

各个浏览器对错误图片的处理方式各不相同,虽然存在alt提示信息,但是不够统一和美观

2.png

例如上面这种错误信息看起来就不够美观,更好的做法给出默认的错误图片,监听图片的onerror事件来重新赋值,如果不想一个个写,可以监听window.addEventListener error来完成全局监听

window.addEventListener(
  'error',
  function (event) {
    var dom = event.target;
    if (/img/i.test(dom.nodeName)) {
      dom.src = 'xxxx.png';
    }
  },
  true
);

如果想增加对错误图片进行重试,或者对 JSP 动态渲染内容进行拦截,可以参考这篇文章如何优雅处理图片异常

空数据默认处理

对空数据放任不管,可能会导致用户认为系统出现了问题,通常情况下我们要尽量避免空数据的出现,一般而言有两种处理方式

使用 empty(空状态)进行填充

3.png

一般 UI 框架现在都内置 empty 组件,可以跟产品和设计沟通无误后,对于常见的 table 或者 list 无数据的进行默认的提示

<template>
  <template v-if="list.length"> /* ... */ </template>
  <template v-else>
    <empty />
  </template>
</template>

隐藏整体区域

1.png

对于上面的作者帮当,如果当前子项不存在数据,只展示一个标题和完整榜单的信息可能会很奇怪那不妨考虑删除整体区域

优先使用语义化标签

尽量避免无语义的div、span滥用,因为搜索引擎抓录不友好,维护起来不够直观,而且使用语义化标签可以节省很多代码性工作,下面举一个例子

5.png

例如上面的按钮,假设它的功能是跳转到一个新的页面,如果在 spa 页面中,我们可以直接使用vue-router自带的router-link跳转即可完成,而在传统开发中可以在button外部嵌套一个a标签完成跳转

<a href="xxx">
  <button>xxx</button>
</a>
<!-- 或者 -->
<router-link to="xxx">
  <button>xxx</button>
</router-link>

而且这样做很容易配合浏览器的一些其它行为,比如右击打开在新标签页

操作反馈

操作反馈是提升用户体验的一个重要指标,具体可以展开为三部分来说

Require 反馈

在与后端进行通信的过程中,如果成功或者失败,都请告知用户,而且提示的消息必须友好。

这里有一个准则

  • 成功消息可以由前端来定义,可以结合各种操作场景做到更细致的提示
  • 对于错误消息,则返回后端返回的 message 等信息
  • 对于 500 等错误,请在拦截器统一改成网络连接错误,请稍后重试,而不是提示一串超时等英文信息

元素反馈

在衡量一个 ui 框架的时候就有两点很重要:

  1. 组件本身的交互是否友好,比如动画是否流畅,有没有 hover、active 等效果
  2. 对于无障碍阅读是否友好

可见交互的重要性,要让用户感觉自己在点击一个真实存在的元素,而不是像图片一样的静止

6.gif

例如上图中,最少要存在hoveractive等效果

等待反馈

7.png

因为与后端的通信是异步的,而且用户的网络好坏也不是固定不变的,所以给元素添加loading就很有必要,这里说两个常见的场景

表单提交

7.png

需要操作交互的常见都可以考虑使用loading,这样可以防止用户重复点击和避免用户不知道有没有点击成功

重新获取数据

8.png

获取前置信息

在 pc 上,整体进入页面的时候为了掩盖获取一些前置的信息的场景,可以选择使用 loading 元素减少用户等待的焦虑感

缓存

缓存本质上就是拿空间换时间,对于客户端而言,更多的瓶颈是在时间上,下面就说两种常见的缓存场景

组件信息缓存

9.png

例如上面表单,如果用户误操作关闭网页,从头开始填写体验就不是特别好,可以在未提交成功的状态下结合本地做持久化缓存

下面给一个简单示例(没有给出完成清除本地储存)

<script>
import { reactive, watchEffect } from 'vue';

export default {
  setUp() {
    const key = 'form';
    const form = reactive({
      name: '',
      password: '',
    });
    const set = (key, value) => {
      localStorage.setItem(key, JSON.stringify(value));
    };
    const get = () => {
      const value = localStorage.getItem(key);
      try {
        return JSON.parse(value);
      } catch {
        return undefined;
      }
    };
    watchEffect(() => {
      set(key, form);
    });
    Object.assign(form, get('form'));
  },
};
</script>

接口缓存

如果存在频繁请求且很耗时,接口本身基本不会变更的情况,可以考虑接口缓存,下面给出一段简单的代码示例

生产环境可以考虑使用一些库 axios-request-cache

import axios from 'axios';
const { toString } = Object.prototype;

// 数据存储
export const cache = {
  data: {},
  set(key, data) {
    this.data[key] = data;
  },
  get(key) {
    return this.data[key];
  },
  clear(key) {
    delete this.data[key];
  },
};

// 建立唯一的key值
export const buildUniqueUrl = (url, method, params = {}, data = {}) => {
  const paramStr = (obj) => {
    if (toString.call(obj) === '[object Object]') {
      return JSON.stringify(
        Object.keys(obj)
          .sort()
          .reduce((result, key) => {
            result[key] = obj[key];
            return result;
          }, {})
      );
    } else {
      return JSON.stringify(obj);
    }
  };
  url += `?${paramStr(params)}&${paramStr(data)}&${method}`;
  return url;
};

// 防止重复请求
export default (options = {}) =>
  async (config) => {
    const defaultOptions = {
      // 设置为0,不清除缓存
      time: 0,
      ...options,
    };
    const index = buildUniqueUrl(config.url, config.method, config.params, config.data);
    let responsePromise = cache.get(index);
    if (!responsePromise) {
      responsePromise = (async () => {
        try {
          const response = await axios.defaults.adapter(config);
          return Promise.resolve(response);
        } catch (reason) {
          cache.clear(index);
          return Promise.reject(reason);
        }
      })();
      cache.set(index, responsePromise);
      if (defaultOptions.time !== 0) {
        setTimeout(() => {
          cache.clear(index);
        }, defaultOptions.time);
      }
    }
    // 为防止数据源污染
    return responsePromise.then((data) => JSON.parse(JSON.stringify(data)));
  };

风格一致性

风格一致性包含很多部分,这里不太好全部列举,就说两个常见的

视觉颜色一致

每个页面都有一个主色号,对于设计图或者原型图没有给到的部分,自己动手或者引用组件时需要考虑是否跟主色号存在冲突,这里推荐阅读一下视觉规范,下面给出一个示例

2.png

3.png

在默认的 vant 组件中,tab 默认的颜色是#ee0a24,如果直接放上去就会导致页面风格不统一,因为页面整体风格是蓝色

组件交互一致性

这里在自定义扩展或者二次封装比较常见,还是以表单为例

9.png

如果我们想自定义根据业务进行一个文件上传的组件,除了考虑功能本身的实现,还要考虑一致性的问题,例如校验的问题

这里Ant Design 在不符合条件的form-item下面出现一个红字提示,而如果我们使用message或者其他提示就会造成提示信息的不一致性,那更好的做法就是根据官方文档提供的自定义插件做法进行组件开发。

pc

考虑最小布局

请设置min-width,防止因为宽度不够导致元素挤在一起

body {
  /* 省略其它 */
  min-width: 1200px;
  overflow: auto;
}

表格宽度

对于表格,根据字段的权值不同分配的宽度也应该不同,尽量不要使用auto避免二次表格宽度计算造成视觉上的浮动

13.png

对于不可省略的信息可以让其换行展示,否则请考虑ellipsis

.ellipsis {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

指标卡片添加 tip

在一些中台产品中,很容易遇到一些专业名词,比如pvuv等,对于这种专业名词,建议添加一个问号的提示图标,鼠标移动上去给与提示

14.png

上面效果仅供参考

支持键盘快捷键

9.png

如上图所示,在我们使用form或者input时,如果按下enter时请确保可以进行正常的搜索或者提交操作,不仅局限于表单提交,对于一些常见的搜索场景都需要考虑

一个好的建议,对于需要 submit 的子元素,都绑定.enter修饰符

<input @keyup.enter="submit" />

SPA 路由导航

对于单页面导航请考虑使用nprogress这样的进度条,为你的页面添加进度通知

15.png

移动端

左右滑动监听

浏览了许多 H5 的页面,对于手势左右滑动基本上都没做支持,而在很多 App 上,例如知乎,从首页进去问题左滑就可以返回

微信图片_20210608100923.jpg

这里稍微建议一下,对于下面两种情况可以考虑引入左滑返回

  • 跟上面图片一致的 tab 页比较多,可以考虑做成 tab 下的元素滑动监听
  • 对于一些列表和详情页

推荐一个库源码也很简洁可以基于这个库进行二次封装,提高用户的体验 swipy

尽量使用 SVG

SVG 是一种图像文件格式,它的英文全称为 Scalable Vector Graphics,意思为可缩放的矢量图形。基于 XML 的标记语言

SVG 是矢量图,它有很多优点

  • SVG 是可伸缩的,在任何的分辨率下被高质量地打印
  • SVG 可在图像质量不下降的情况下被放大
  • 修改方便,可以在记事本之类的软件中被打开

而在开发移动端,面对的用户手机的屏幕也各不相同,之前的做法是对不同 ratio 的手机选择不同的图片,但是根据上面 SVG 的优点,我们可以直接选用 SVG 当做图片

而在 SVG 不适用的场景,我们可以利用媒体查询,选择合适的高倍图

export const getMultipleImg = (img1, img2) => {
  const mql = window.matchMedia('@media only screen and (-webkit-min-device-pixel-ratio:3)');
  if (mql.matches) {
    return img2;
  }
  return img1;
};