记录--前端使用a链接下载内容增加loading效果

news2024/11/18 23:41:26

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。

代码如下:

// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
  sheetName = sheetName || 'sheet1'
  var workbook = {
    SheetNames: [sheetName],
    Sheets: {}
  }
  workbook.Sheets[sheetName] = sheet
  // 生成excel的配置项
  var wopts = {
    bookType: 'xlsx', // 要生成的文件类型
    bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
    type: 'binary'
  }
  var wbout = XLSX.write(workbook, wopts)
  var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
  // 字符串转ArrayBuffer
  function s2ab(s) {
    var buf = new ArrayBuffer(s.length)
    var view = new Uint8Array(buf)
    for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
    return buf
  }
  return blob
}

/**
 * 通用的打开下载对话框方法,没有测试过具体兼容性
 * @param url 下载地址,也可以是一个blob对象,必选
 * @param saveName 保存文件名,可选
 */
export const openDownloadDialog = (url, saveName) => {
  if (typeof url === 'object' && url instanceof Blob) {
    url = URL.createObjectURL(url) // 创建blob地址
  }
  var aLink = document.createElement('a')
  aLink.href = url
  aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
  var event
  if (window.MouseEvent) event = new MouseEvent('click')
  else {
    event = document.createEvent('MouseEvents')
    event.initMouseEvent(
      'click',
      true,
      false,
      window,
      0,
      0,
      0,
      0,
      0,
      false,
      false,
      false,
      false,
      0,
      null
    )
  }
  aLink.dispatchEvent(event)
}

<el-button
  @click="clickExportBtn"
>
  <i class="el-icon-download"></i>下载数据
</el-button>
<div class="mongolia" v-if="loadingSummaryData">
  <el-icon class="el-icon-loading loading-icon">
    <Loading />
  </el-icon>
  <p>loading...</p>
</div>

clickExportBtn: _.throttle(async function() {
  const downloadDatas = []
  const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
  summaryDataForDownloads.map(summaryItem =>
    downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
  )
  //  donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
  this.loadingSummaryData = true
  const downloadBlob = aoa2sheet(downloadDatas.flat(1))
  openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
  this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.9);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 1.5rem;
  color: #409eff;
  z-index: 9999;
}
.loading-icon {
  color: #409eff;
  font-size: 32px;
}

解决方案探究:

  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。

    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。

    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。

    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。

  • 查阅资料后找到了如下几种方案:

1.使用 setTimeout 使 openDownloadDialog 异步执行

clickExport() {
  this.loadingSummaryData = true;

  setTimeout(() => {
    openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

    this.loadingSummaryData = false;
  });
}

2.对 openDownloadDialog 内部进行优化

  • 避免大循环或递归逻辑

  • 将计算工作分批进行

  • 使用 Web Worker 隔离耗时任务

  在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

  1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。

  2. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。

  3. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。

  4. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。

  5. 代码应该是自包含的,不依赖外部变量或状态。

  6. 可以导入其他脚本依赖,比如用 import 引入工具函数等。

  7. 避免修改或依赖全局作用域,比如定义全局变量等。

总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。

// 创建 Worker
const worker = new Worker('downloadWorker.js'); 

// 点击下载时向 Worker 发送消息
function clickDownload() {

  showLoading();

  worker.postMessage({
    url: fileURL,
    filename: 'report.xlsx'
  });

  worker.onmessage = function(e) {
    // 收到下载完成的消息
    hideLoading();
  }

}

// 显示 loading
function showLoading() {
  loading.style.display = 'block'; 
}

// 隐藏 loading
function hideLoading() {
  loading.style.display = 'none';
}

// downloadWorker.js

onmessage = function(e) {
  const url = e.data.url;
  const filename = e.data.filename;

  // 创建并点击链接触发下载
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();

  postMessage('下载完成');
}

<div id="loading" style="display:none;">
  Downloading...
</div>

<button onclick="clickDownload()">
  Download
</button>

<script src="downloadWorker.js"></script>

3.使用 requestIdleCallback 进行调度

clickExport() {
  this.loadingSummaryData = true;

  requestIdleCallback(() => {
    openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

    this.loadingSummaryData = false; 
  });
}

当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

所以需要权衡执行速度和避免阻塞之间的关系:

  • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。

  • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

此外,可以结合两者试试:

clickExport() {

  this.loadingSummaryData = true;

  setTimeout(() => {

    requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
    });

    this.loadingSummaryData = false;

  }, 200);

}

setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

分析线程模型,找到具体的阻塞点

使用 Performance 工具分析线程

debugger 及 console.log 打印关键函数的执行时间

检查是否有非主线程的任务阻塞了主线程

调整 vue 的批量更新策略

new Vue({
  config: { 
    // 修改批量更新的队列长度
    batchUpdateDuration: 100 
  }
})

本文转载于:

https://juejin.cn/post/7268050036474609683

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/962325.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023_Spark_实验六:Scala面向对象部分演示(二)(IDEA开发)

7、Scala中的apply方法&#xff08;&#xff09; 遇到如下形式的表达式时&#xff0c;apply方法就会被调用&#xff1a; Object(参数1,参数2,......,参数N) 通常&#xff0c;这样一个apply方法返回的是伴生类的对象&#xff1b;其作用是为了省略new关键字 Object的apply方法…

我开课了!《机器学习》公益课9月4日开课

我是黄海广&#xff0c;大学老师&#xff0c;我上的一门课叫《机器学习》&#xff0c;本科生学机器学习有点难&#xff0c;但也不是没有可能&#xff0c;我在摸索中&#xff0c;设计适合本科生的机器学习课程&#xff0c;写了教材&#xff0c;录了视频&#xff0c;做了课件。我…

如何使用SQL系列 之 了解SQL中的约束规则

简介 在设计数据库时&#xff0c;有时可能需要对某些列中允许的数据设置限制。例如&#xff0c;如果你要创建一张表来保存摩天大楼的信息&#xff0c;你可能希望在保存每座大楼高度的列中禁止使用负值。 关系型数据库管理系统(RDBMS)允许你使用约束来控制哪些数据被添加到表中…

PID 算法

1.1 概述 比例&#xff08;Proportion&#xff09;积分&#xff08;Integral&#xff09;微分&#xff08;Differential&#xff09;控制器&#xff08;PID控制器或三项控制器&#xff09;是一种采用反馈的控制回路机制&#xff0c;广泛应用于工业控制系统和需要连续调制控制的…

ArrayList(扩容机制)

文章目录 一、前言二、ArrayList扩容机制1、适用于什么场景&#xff1f;2、ArrayList特点3、ArrayList扩容机制3.1、内存分配的效率&#xff1a;3.2、数据迁移的代价3.3、性能和空间的平衡 三、总结 一、前言 对于ArrayList集合可能大家并不陌生&#xff0c;但ArrayList集合的…

Camera | 12.瑞芯微摄像头自动焦距马达驱动移植

本为你主要讲解如何让摄像头ov13850支持自动对焦功能。 摄像头的对角主要通过VCM马达驱动芯片DW9714来实现的。 一、环境 soc : rk3568 board: EVB1-DDR4-V10 软 件&#xff1a;Android 11 Linux&#xff1a;4.19.232 Camera:ov13850二、DW9714 1.DW9714简介 DW9714专…

企业知识管理的解决方案

人们发现&#xff0c;挖掘知识、创造知识、生产知识&#xff0c;用知识为自己的产品赋予高附加值&#xff0c;才是企业和社会可持续发展的动力之源。所以知识管理越来越受到重视。 知识管理作为一个新兴的管理概念&#xff0c;已经被学术界所接受&#xff0c;但尚未形成一个…

网站用户体验之深度感悟

个性化定制界面和极简版原装界面&#xff0c;哪一个你用起来更加顺手呢&#xff0c;相比之下你更喜欢哪一个&#xff1f; 界面选择&#xff1a; &#xff08;提醒&#xff1a;仅个人感悟。~~&#xff09; 方向一&#xff1a;表明自己的喜好 我个人觉得更喜欢个性化定制界面。…

虚拟机有代理备份、无代理备份是什么?

有代理备份&#xff1a; 在虚拟机内部安装备份代理程序&#xff0c;然后把虚拟机当作物理机一样来进行备份任务。借助虚拟机系统中内置的程序来进行备份的&#xff0c;就像在正常系统中备份那样&#xff0c;借助备份和还原&#xff08;Windows7&#xff09;功能对系统进行备份…

知识图谱笔记:TransE

1 知识图谱介绍 一条知识图谱可以表示为一个三元组(sub,rel,obj)。 举个例子&#xff1a;小明的爸爸是大明&#xff0c;表示成三元组是&#xff08;小明&#xff0c;爸爸&#xff0c;大明&#xff09;。前者是主体&#xff0c;中间是关系&#xff0c;后者是客体。主体和客体统…

layui引入百度地图

<script type"text/javascript" src"//api.map.baidu.com/api?typewebgl&v1.0&ak你的ak"></script> <script src"https://code.bdstatic.com/npm/jquery1.12.4/dist/jquery.min.js"></script> <script src&…

科技驱动产业升级:浅谈制造型企业对MES系统的应用

在科技不断进步的背景下&#xff0c;制造型行业也在持续发展&#xff0c;但随之而来的挑战也不断增加。传统的管理方式已经无法满足企业的需求&#xff0c;因此许多制造型企业开始寻找新的管理模式。制造执行系统&#xff08;MES&#xff09;作为先进的制造信息技术之一&#x…

【微信小程序】父子组件的创建、通信与事件触发;组件生命周期

前言 关于微信小程序中父子组件的创建、传值&#xff0c;以及涉及到的组件生命周期。 使用组件的优点 组件的使用可以提高开发效率并确保功能在各个页面上的应用和修改的一致性。 例如&#xff0c;对于一些重复的功能&#xff0c;比如顶部导航栏或评论区&#xff0c;将其提炼…

MongoDB基础知识点

MongoDB基础知识点 1.MongoDB简介1.1基本信息1.2作用1.3下载 2.MongoDB安装1.Ubuntu22.042.Windows(非msi) 3.MongoDB基本操作1.基本概念2.MongoDB文件增删改查(CURD)1.插入数据2.查询数据3.修改数据4.删除数据5.删除字段 4.MongoDB实战管理系统数据库设计1.设计数据库2.Mongod…

c++ folly::baton

Baton folly::Baton 是 Facebook 开源的一个同步原语&#xff0c;它提供了一种简单而灵活的方式来进行线程间的同步。它属于 Folly 库&#xff0c;是 C 编程语言的一个组件。 Baton 通常用作线程间同步、等待、通知的标识符号&#xff0c;常用姿势是&#xff0c;一些线程调用…

正中优配:红筹股是啥意思?

随着我国经济的高速开展&#xff0c;越来越多的人开始参加到股票出资中。其中&#xff0c;红筹股作为一种特别类型的股票&#xff0c;备受一些出资者的关注&#xff0c;但对于一般出资者来说&#xff0c;红筹股详细含义还不是特别清楚。本文将从多个角度探讨红筹股的含义、特征…

分库分表知识大全及Sharding-JDBC实践

文章目录 一、为什么要分库分表1.1 什么是分库1.2 什么是分表1.3 为什么要分库1.3.1 磁盘存储1.3.2 并发连接支撑 1.4 为什么要分表 二、分库分表解决方案2.1 垂直&#xff08;纵向&#xff09;切分2.1.1 垂直切分优点2.1.2 垂直切分缺点 2.2 水平&#xff08;横向&#xff09;…

了解NAT

局域网内的终端是如何共用一个wan口来上网的&#xff1f; 主要通过NAT&#xff08;Network Address Translation&#xff0c;网络地址转换&#xff09;协议来实现的 NAT是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地…

无涯教程-Flutter - 数据库

SQLite" class"css-1occaib">SQLite数据库是基于事实和标准SQL的嵌入式数据库引擎&#xff0c;它是小型且经过时间考验的数据库引擎&#xff0c;sqflite软件包提供了许多函数&#xff0c;可以有效地与SQLite数据库一起使用&#xff0c;它提供了操作SQLite数据…

【FreeRTOS】互斥量的使用与逐步实现

在FreeRTOS中&#xff0c;互斥量是一种用于保护共享资源的同步机制。它通过二进制信号量的方式&#xff0c;确保在任意时刻只有一个任务可以获取互斥量并访问共享资源&#xff0c;其他任务将被阻塞。使用互斥量的基本步骤包括创建互斥量、获取互斥量、访问共享资源和释放互斥量…