Published on

Vue指令 实现禁止文件重复下载

Reading time
4 分钟
作者

前言

日常进行开发工作时,经常会遇到下载文件的场景,通常用户点击下载后需要禁止重复点击下载以保证不会发送大量请求,按钮禁用时再添加一个loading状态提供更好的视觉体验。 单个页面内实现非常简单:设置一个loading变量,在用户进行操作前后分别设置按钮的loading状态和disabled状态即可。但是1个页面写一次,10个页面、100个页面呢?因此我们需要一种通用的方法去解决这个问题,自然而然也就想到了Vue指令。

实现思路

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令

整体实现分为两部分(Vue + Element + Axios为例):

  1. axios中,文件下载时向全局添加flag变量代表下载状态
  2. 指令中,当检测到文件下载时禁用按钮并添加loading状态,下载完毕时恢复原状

实现过程

一、全局上报下载进度

定义一个函数用来向全局上报下载进度,当文件长度可计算时,定义一个名为isFileDownloading的flag,用于判断当前文件是否下载完毕:

// axios.js

// 向全局添加下载进度
const addDownloadProgress = (progressEvent) => {
  // 仅针对文件长度可计算的情况
  if (progressEvent.lengthComputable) {
    window.isFileDownloading = progressEvent.loaded !== progressEvent.total;
  } else {
    window.isFileDownloading = false;
  }
};

Axios官网的请求配置有一项onDownloadingProgress属性用于处理下载事件

// `onDownloadProgress` allows handling of progress events for downloads
// browser only
onDownloadProgress: function (progressEvent) {
  // Do whatever you want with the native progress event
}

当接口是用于下载文件时才汇报进度:

// axios.js

// 下载文件时向全局汇报进度,用于禁止重复下载指令
if (conf.responseType === 'blob') {
  if (!conf['onDownloadProgress']) {
    conf['onDownloadProgress'] = addDownloadProgress;
  }
}

二、监听全局变量并更新状态

使用Vue指令,使用定时器轮询上述步骤创建的flag,根据flag修改按钮状态,主要代码:

// preventReDownload.js

let changeToDownloading = null;

export default {
  install(Vue) {
    Vue.directive('preventReDownload', {
      bind(el) {
        let timer = null;
        changeToDownloading = () => {
          el.disabled = true;
          el.classList.add('is-disabled', 'is-loading');
          // 轮询axios向全局变量中添加的文件下载状态flag
          timer = setInterval(() => {
            if (!window.isFileDownloading) {
              console.log('done!');
              clearInterval(timer);
              el.disabled = false;
              el.classList.remove('is-disabled', 'is-loading');
              el.removeChild(el.firstChild);
            }
          }, 500);
        };
        el.addEventListener('click', changeToDownloading);
      },
      unbind() {
        document.removeEventListener('click', changeToDownloading);
      }
    });
  }
};

加上圆圈动效的完整代码:

// preventReDownload.js

// function 将按钮改变为下载中状态
let changeToDownloading = null;
// loading 动效圆圈
const loadingEle = document.createElement('div');
loadingEle.setAttribute('class', 'el-loading-spinner');
loadingEle.innerHTML = `
  <svg class="circular" viewBox="0 0 16 16">
    <circle class="path" cx="8" cy="8" r="7" fill="none"/>
  </svg>
`;

export default {
  install(Vue) {
    Vue.directive('preventReDownload', {
      bind(el) {
        let timer = null;
        changeToDownloading = () => {
          el.disabled = true;
          el.classList.add('is-disabled', 'is-loading');
          el.prepend(loadingEle);
          // 轮询axios向全局变量中添加的文件下载状态flag
          timer = setInterval(() => {
            if (!window.isFileDownloading) {
              console.log('done!');
              clearInterval(timer);
              el.disabled = false;
              el.classList.remove('is-disabled', 'is-loading');
              el.removeChild(el.firstChild);
            }
          }, 500);
        };
        el.addEventListener('click', changeToDownloading);
      },
      unbind() {
        document.removeEventListener('click', changeToDownloading);
      }
    });
  }
};

使用方法

在按钮中使用指令v-prevent-re-download,当点击按钮时触发下载接口:

<el-button v-prevent-re-download type="primary" @click="onDownload">下载</el-button>

可以看到按钮变成loading与disabled状态,控制台打印了下载过程以及下载完成的信息:

按钮状态
控制台信息

缺陷

这个方法有一些瑕疵:

  1. 代码有一定入侵性,对请求封装做了一定的修改,而且当该请求本身已经设置了isFileDownloading时就会出问题,因此我跳过了本身已设置该属性的接口
  2. Loading状态的样式还有些问题,plain属性的按钮和表格操作列按钮都无法显示,因为这个圈是白色的,这个根据需要自行修改吧😂

参考资料

Vue 利用指令实现禁止反复发送请求的两种方法