Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

纪录我可以做的提升用户体验的优化 #147

Open
yanyue404 opened this issue Jun 11, 2020 · 0 comments
Open

纪录我可以做的提升用户体验的优化 #147

yanyue404 opened this issue Jun 11, 2020 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented Jun 11, 2020

目录

多请求下 loading 的显示与隐藏

在 vue 中一般会搭配 axios 做全局的请求方法,在拦截器中利用第三方组件控制 loading 展示和关闭,代码是这样的:

// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    Toast.loading({
      message: '加载中...',
      forbidClick: true,
      loadingType: 'spinner',
    });
    return config;
  },
  (error) => {
    return Promise.error(error);
  },
);

// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    Toast.clear();
    if (response.status === 200) {
      return Promise.resolve(response);
    } else {
      return Promise.reject(response);
    }
  },
  (error) => {
    console.log('error', error);
    Toast.clear();
    if (error.response.status) {
      switch (error.response.status) {
        default:
          this.$toast({ message: '未知系统异常', time: 2000 });
      }
      return Promise.reject(error.response);
    }
  },
);

在多个请求的情况下,依照上面的设置方法,loading 在频繁的打开与关闭,会造成 loading 的闪烁,加载完成的真实时间可能被影响,造成用户体验不好。

下面的是解决方案,请求的数量做一个全局的变量,初始值为 0,请求进入的时候 +1,请求被成功响应的时候 -1,当请求数量未 0 再没有需要请求的内容,loading 关闭,在这里 loading 的初始时间可以设置的稍长一些。同时,应当在请求错误与路由切换的时候做 归零操作,隐藏 loading,reset。

export default class loadingControl {
  constructor() {
    this.isShowLoading = true;
    this.loadingCount = 0;
  }
  addLoading = () => {
    this.isShowLoading = true;
    this.loadingCount++;
  };
  isCloseLoading() {
    this.loadingCount--;
    if (this.loadingCount == 0) {
      this.isShowLoading = false;
    }
  }
  reset() {
    this.loadingCount = 0;
    this.isShowLoading = false;
  }
}
import loadingControl from './loadingControl';
const LOADING = (window.LOADING = new loadingControl());

// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    LOADING.addLoading();
    Toast.loading({
      message: '加载中...',
      forbidClick: true,
      loadingType: 'spinner',
      duration: 10000,
    });
    return config;
  },
  (error) => {
    return Promise.error(error);
  },
);

// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    LOADING.isCloseLoading();
    console.log('剩余请求数量', LOADING.loadingCount);
    LOADING.loadingCount === 0 ? Toast.clear() : '';
    if (response.status === 200) {
      return Promise.resolve(response);
    } else {
      return Promise.reject(response);
    }
  },
  (error) => {
    LOADING.reset();
    Toast.clear();
    console.log('error', error);
    if (error.response.status) {
      switch (error.response.status) {
        default:
          this.$toast({ message: '未知系统异常', time: 2000 });
      }
      return Promise.reject(error.response);
    }
  },
);
// 路由改变,重置 loading
window.LOADING.reset();

单个页面控制,使用 promise 扩展

Promise.every([
  this.getArticleList(),
  this.getVideoList(),
  this.getKnowList(),
  this.getMessageList(),
]).then(([...res]) => {
  this.setData({
    ...res[0],
    ...res[1],
    ...res[2],
    ...res[3],
    isLoading: false,
  });
});

保留用户阅读位置

import { debounce } from '@/utils';
class RememberScrollController {
  constructor() {
    this.readList = [];
    this.limit = 10;
  }
  async init() {
    // 纪录位置
    await this.isScroll();
    this.listenScrollEvent();
  }
  async isScroll() {
    const $ReadList = this.readList;
    const path = window.location.href;
    const selected = $ReadList.filter((v) => v.path === path);

    if (selected.length === 1) {
      console.log('已经阅读过', selected);
      await setTimeout(() => {
        window.scrollTo(0, selected[0].top);
      }, 200);
    }
  }
  scrollTo(x, y) {
    window.scrollTo(x, y);
  }
  insertReadList(scrollTop) {
    const path = window.location.href;
    const $ReadList = this.readList;
    let $Limit = this.Limit;
    const obj = {
      path,
      top: scrollTop,
    };
    const selected = $ReadList.filter((v) => v.path === path);
    if (selected.length === 0) {
      if ($ReadList.length >= $Limit) {
        $ReadList.shift(); // 超量,从前删
      }
      $ReadList.push(obj);
    } else {
      selected[0].top = scrollTop;
    }
    // console.log('缓存的阅读列表',$ReadList);
  }
  // 监听纪录滚动高度
  listenScrollEvent() {
    const listenFn = debounce(() => {
      const scrollTop =
        document.documentElement.scrollTop || document.body.scrollTop;
      this.insertReadList(scrollTop);
    }, 200);
    window.addEventListener('scroll', listenFn);
  }
}
this.$RememberScrollController .init(); // 使用,假如原型链已设置

按钮点击频发

    1. 节流
    1. 首次点击后置为 disable

1px 像素

.cus-border-bottom {
  position: relative;
}
.cus-border-bottom::after {
  content: ' ';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1px;
  transform: scale(1, 0.5);
  transform-origin: left top;
  box-sizing: border-box;
  border-bottom: 1px solid #cccccc;
}

.cus-border-all {
  position: relative;
}
.cus-border-all::after {
  content: ' ';
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 200%;
  transform: scale(0.5);
  transform-origin: left top;
  box-sizing: border-box;
  border: 1px solid #cccccc;
}

promise 扩展

Promise.all 错误处理,使用 every 方法,所有的 promise 都错误才触发 reject

Promise.every = (promiseAry) => {
  return new Promise((resolve, reject) => {
    let resultAry = [],
      errorAry = [],
      index = 0,
      index__error = 0;
    for (let i = 0; i < promiseAry.length; i++) {
      Promise.resolve(promiseAry[i])
        .then((result) => {
          index++;
          resultAry[i] = result;
          if (index === promiseAry.length) {
            resolve(resultAry);
          }
        })
        .catch((reason) => {
          index__error++;
          index++;
          errorAry[i] = reason;
          if (index__error === promiseAry.length) {
            reject(errorAry);
          }
        });
    }
  });
};

localStorage 设置缓存过期时间

class StorageFn {
  constructor() {
    this.ls = window.localStorage;
  }
  setItem(key, val, expires) {
    // 设置过期时间 https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/171
    if (typeof expires !== 'undefined') {
      var expiresDate = new Date(expires).valueOf();
      this.ls.setItem(key + '_expires', expiresDate);
    }
    this.ls.setItem(key, val);
  }

  getItem(key) {
    var expires = this.ls.getItem(key + '_expires');
    if (expires && new Date() > new Date(Number(expires))) {
      this.ls.removeItem(key);
      this.ls.removeItem(key + '_expires');
    }
    if (key) return this.ls.getItem(key);
    return null;
  }

  removeItem(key) {
    this.ls.removeItem(key);
  }

  /*移除所有localStorage*/
  clear() {
    this.ls.clear();
  }
}
const Storage = new StorageFn();
Storage.setItem('key', 'value', new Date(Date.now() + 10000)); // 10 秒钟后过期
Storage.getItem('key');
(function () {
  var getItem = localStorage.getItem.bind(localStorage);
  var setItem = localStorage.setItem.bind(localStorage);
  var removeItem = localStorage.removeItem.bind(localStorage);
  localStorage.getItem = function (keyName) {
    var expires = getItem(keyName + '_expires');
    if (expires && new Date() > new Date(Number(expires))) {
      removeItem(keyName);
      removeItem(keyName + '_expires');
    }
    return getItem(keyName);
  };
  localStorage.setItem = function (keyName, keyValue, expires) {
    if (typeof expires !== 'undefined') {
      var expiresDate = new Date(expires).valueOf();
      setItem(keyName + '_expires', expiresDate);
    }
    return setItem(keyName, keyValue);
  };
})();

token 失效处理

let ERROR_NUM = 0;

const http = (
  url,
  data = {},
  method,
  contentType = 'application/x-www-form-urlencoded',
  custom_post = false,
) => {
  const config__header = {
    'x-tenant-header': 'web-spcloud-sales',
    'Content-Type': contentType,
  };
  const token = wx.getStorageSync('ACCESS_TOKEN') || '';
  // 设置参数 {token: false},可以不设置授权 header 头
  if (token && data.token === undefined) {
    config__header['Authorization'] = 'Bearer ' + token;
  }
  if (data.token === false) {
    delete data.token;
  }

  if (custom_post) {
    data = util.qsStringify(data);
  }

  return new Promise(function (resolve, reject) {
    wx.request({
      url: RootUrl + url,
      data: data,
      method: method,
      header: config__header,
      success: (res) => {
        if (res.statusCode == 200) {
          console.log('Request Successful', {
            url,
            params: data,
            result: res,
          });
          resolve(res);
        } else {
          console.log('Request Error', {
            url,
            params: data,
            result: res,
          });

          // token 已经失效

          ERROR_NUM++;

          if (
            res.data &&
            res.data.resp_msg &&
            res.data.resp_msg.indexOf('Invalid access token') !== -1 &&
            ERROR_NUM === 1
          ) {
            wx.showModal({
              title: '警告',
              showCancel: false,
              content: '登录信息已经失效,请重新进行登陆认证!',
              confirmText: '确定',
              success: (res) => {
                if (res.confirm) {
                  wx.Storage.clear(function () {
                    wx.navigateTo({
                      url: '../auth_login/auth_login',
                    });
                  });
                }
              },
            });
          }
          reject(res);
          // util.toast('服务器异常,请稍后再试');
        }
      },
      complete: () => {
        // wx.hideLoading();
      },
      fail: (err) => {
        console.log('failed --- 网络出错');
        reject(err);
      },
    });
  });
};

图像资源压缩

借助 compressorjs 完成文件压缩资源优化。

import Compressor from 'compressorjs';
const compressPicture = (file) => {
  new Compressor(file, {
    quality: 0.6,
    success(result) {
      const formData = new FormData();
      // The third parameter is required for server
      formData.append('file', result, result.name);
      return formData;
    },
  });
};
清晰度 输入大小 输出大小 压缩率 描述
0 2.12 MB 114.61 KB 94.72% -
0.2 2.12 MB 349.57 KB 83.90% -
0.4 2.12 MB 517.10 KB 76.18% -
0.6 2.12 MB 694.99 KB 67.99% 推荐
0.8 2.12 MB 1.14 MB 46.41% 推荐
1 2.12 MB 2.12 MB 0% 不推荐
NaN 2.12 MB 2.01 MB 5.02% -

自定义文件名下载

借助 downloadjs 实现 javascript 下载文件。

import download from 'downloadjs';
// 支持 image,excel, rar
const downloadFile = (file) => {
  var x = new XMLHttpRequest();
  x.open('GET', file.url, true);
  x.responseType = 'blob';
  x.onload = function (e) {
    download(e.target.response, file.name, file.contentType);
  };
  x.send();
};

Tab 标签页下划线滚动

<template>
  <div class="container">
    <div class="tabs oneLineBetween">
      <div
        :ref="'tab_' + index"
        :class="[tabIndex === index ? 'active' : '', 'tab']"
        @click="tabChange(index)"
        v-for="(tabName, index) in tabs"
        :key="index"
      >
        {{ tabName }}
      </div>
      <div class="line" :style="lineStyle"></div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tabIndex: 0,
      tabs: ['全部保单', '已承保', '未承保', '已失效'],
      lineStyle: {},
    };
  },
  watch: {
    tabIndex(newValue, oldValue) {
      this.setLine();
    },
  },
  mounted() {
    this.$nextTick(() => {
      this.setLine();
    });
  },
  methods: {
    tabChange(index) {
      this.tabIndex = index;
    },
    setLine() {
      let selected = this.$refs['tab_' + this.tabIndex][0];
      const left = selected.offsetLeft;
      const width = selected.offsetWidth;
      const lineStyle = {
        width: width + 'px',
        transform: `translateX(${left}px)`,
      };
      this.lineStyle = lineStyle;
    },
  },
};
</script>

<style scoped lang="scss">
.container {
  min-height: 100vh;
  background: rgba(247, 247, 247, 1);
  .tabs {
    position: relative;
    height: 102px;
    padding: 0 50px;
    .tab {
      height: 100%;
      font-size: 28px;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: rgba(153, 153, 153, 1);
      line-height: 102px;
    }
    .active {
      font-size: 28px;
      font-family: PingFangSC-Medium, PingFang SC;
      font-weight: 500;
      color: rgba(210, 163, 108, 1);
    }
    .line {
      transition-duration: 0.3s;
      position: absolute;
      left: 0;
      z-index: 1;
      bottom: 1px;
      display: block;
      height: 1px;
      background: #d2a36c;
    }
  }
}
</style>

缓存用户编辑未提交的数据

例子:Session Storage 缓存,新建 issues切换到 pull requests,保留编辑数据,重新打开 new issues 缓存载入。

Key Value
session-resume:/yanyue404/blog_Private/issues/new: [["issue_title","谭光志(woai3c)"],["issue_body","##2020 国内公司前端团队都在搞些什么?"]]

登录后重定向用户之前访问的网址

未登录访问的网址: https://github.com/doocs/md

没有之前访问地址的去登录: https://github.com/login

含有之前访问地址的去登录:https://github.com/login?return_to=%2Fdoocs%2Fmd

登录后继续访问:https://github.com/doocs/md

异步请求进度条

借助 NProgress 实现用户可感知页面异步加载速度的进度条。

import axios from 'axios';
import NProgress from 'nprogress';

// 添加一个请求拦截器,用于设置请求过渡状态
axios.interceptors.request.use((config) => {
  // 请求开始,蓝色过渡滚动条开始出现
  NProgress.start();
  return config;
}, (error) => {
  return Promise.reject(error);
});

// 添加一个返回拦截器
axios.interceptors.response.use((response) => {
  // 请求结束,蓝色过渡滚动条消失
  NProgress.done();
  return response;
}, (error) => {
  // 请求结束,蓝色过渡滚动条消失
  // 即使出现异常,也要调用关闭方法,否则一直处于加载状态很奇怪
  NProgress.done();
  return Promise.reject(error);
});

...

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant