【文件上传系列】No.2 秒传(原生前端 + Node 后端)

上一篇文章

【文件上传系列】No.1 大文件分片、进度图展示(原生前端 + Node 后端 & Koa)


秒传效果展示

请添加图片描述


秒传思路

整理的思路是:根据文件的二进制内容生成 Hash 值,然后去服务器里找,如果找到了,说明已经上传过了,所以又叫做秒传(笑)


整理文件夹、path.resolve() 介绍

接着上一章的内容,因为前端和后端的服务都写在一起了,显得有点凌乱,所以我打算分类一下

在这里插入图片描述

改了文件路径的话,那么各种引用也要修改,引用就很好改了,这里就不多说了

这里讲一下 path 的修改,为了方便修改 path,引用了 path 依赖,使用 path.resolve() 方法就很舒服的修改路径,常见的拼接方法如下图测试:(如果不用这个包依赖的话,想一下如何返回上一个路径呢?可能使用 split('/)[1] 类似这种方法吧。)

在这里插入图片描述

会使用这个包依赖之后就可以修改服务里的代码了:

在这里插入图片描述

200 页面正常!资源也都加载了!

在这里插入图片描述

前端

思路

具体思路如下

  1. 计算文件整体 hash ,因为不同的文件,名字可能相同,不具有唯一性,所以根据文件内容计算出来的 hash 值比较靠谱,并且为下面秒传做准备。
  2. 利用 web-worker 线程:因为如果是很大的文件,那么分块的数量也会很多,读取文件计算 hash 是非常耗时消耗性能的,这样会使页面阻塞卡顿,体验不好,解决的一个方法是,我们开一个新线程来计算 hash

工作者线程简介

《高级JavaScript程序设计》27 章简介: JavaScript 环境实际上是运行在托管操作系统中的虚拟环境。在浏览器中每打开一个页面,就会分配一个它自己的环境。这样,每个页面都有自己的内存、事件循环、DOM,等等。每个页面就相当于一个沙盒,不会干扰其他页面。
对于浏览器来说,同时管理多个环境是非常简单的,因为所有这些环境都是并行执行的。

工作者线程的数据传输如下:

在这里插入图片描述

注意在 worker 中引入的脚本也是个请求!

在这里插入图片描述

// index.html
function handleCalculateHash(fileChunkList) {
  let worker = new Worker('./hash.js');
  worker.postMessage('你好 worker.js');
  worker.onmessage = function (e) {
    console.log('e:>>', e);
  };
}
handleCalculateHash();
// worker.js
self.onmessage = (work_e) => {
  console.log('work_e:>>', work_e);
  self.postMessage('你也好 index.html');
};

计算整体文件 Hash

前端拿到 Blob,然后通过 fileReader 转化成 ArrayBuffer,然后用 append() 方法灌入 SparkMD5.ArrayBuffer() 实例中,最后 SparkMD5.ArrayBuffer().end() 拿到 hash 结果在这里插入图片描述

在这里插入图片描述

SparkMD5 计算 Hash 性能简单测试

js-spark-md5 的 github 地址

配置 x99 2643v3 六核十二线程 基础速度:3.4GHz,睿频 3.6GHz只测试了一遍

请添加图片描述

// 计算时间的代码
self.onmessage = (e) => {
  const { data } = e;
  self.postMessage('你也好 index.html');
  const spark = new SparkMD5.ArrayBuffer();
  const fileReader = new FileReader();
  const blob = data[0].file;
  fileReader.readAsArrayBuffer(blob);
  fileReader.onload = (e) => {
    console.time('append');
    spark.append(e.target.result);
    console.timeEnd('append');
    spark.end();
  };
};

在这里插入图片描述

工作者线程:计算 Hash

这里有个注意点,就是我们一定要等到 fileReader.onload 读完一个 chunk 之后再去 append 下一个块,一定要注意这个顺序,我之前想当然写了个如下的错误版本,就是因为回调函数 onload 还没被调用(文件没有读完),我这里只是定义了回调函数要干什么,但没有保证顺序是一块一块读的。

// 错误版本
const chunkLength = data.length;
let curr = 0;
while (curr < chunkLength) {
  const blob = data[curr].file;
  curr++;
  const fileReader = new FileReader();
  fileReader.readAsArrayBuffer(blob);
  fileReader.onload = (e) => {
    spark.append(e.target.result);
  };
}
const hash = spark.end();
console.log(hash);

如果想保证在回调函数内处理问题,我目前能想到的办法:一种方法是递归,另一种方法是配合 await

这个是非递归版本的,比较好理解。

// 非递归版本
async function handleBlob2ArrayBuffer(blob) {
  return new Promise((resolve) => {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(blob);
    fileReader.onload = function (e) {
      resolve(e.target.result);
    };
  });
}
self.onmessage = async (e) => {
  const { data } = e;
  self.postMessage('你也好 index.html');
  const spark = new SparkMD5.ArrayBuffer();
  for (let i = 0, len = data.length; i < len; i++) {
    const eachArrayBuffer = await handleBlob2ArrayBuffer(data[i].file);
    spark.append(eachArrayBuffer);   // 这个是同步的,可以 debugger 打断点试一试。
  }
  const hash = spark.end();
};

递归的版本代码比较简洁

// 递归版本
self.onmessage = (e) => {
  const { data } = e;
  console.log(data);
  self.postMessage('你也好 index.html');
  const spark = new SparkMD5.ArrayBuffer();

  function loadNext(curr) {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(data[curr].file);
    fileReader.onload = function (e) {
      const arrayBuffer = e.target.result;
      spark.append(arrayBuffer);
      curr++;
      if (curr < data.length) {
        loadNext(curr);
      } else {
        const hash = spark.end();
        console.log(hash);
        return hash;
      }
    };
  }
  loadNext(0);
};

我们在加上计算 hash 进度的变量 percentage就差不多啦

官方建议用小切块计算体积较大的文件,点我跳转官方包说明

在这里插入图片描述

ok 这个工作者线程的整体代码如下:

importScripts('./spark-md5.min.js');
/**
 * 功能:blob 转换成 ArrayBuffer
 * @param {*} blob
 * @returns
 */
async function handleBlob2ArrayBuffer(blob) {
  return new Promise((resolve) => {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(blob);
    fileReader.onload = function (e) {
      resolve(e.target.result);
    };
  });
}

/**
 * 功能:求整个文件的 Hash
 * - self.SparkMD5 和 SparkMD5 都一样
 * - 1. FileReader.onload	处理 load 事件。该事件在读取操作完成时触发。
 * - 流程图展示
 * - 注意这里的 percentage += 100 / len; 的位置,要放到后面
 * - 因为如果是小文件的话,块的个数可能是1,最后 100/1 就直接是 100 了
 * ┌────┐                                   ┌───────────┐                                     ┌────┐
 * │    │   Object      fileReader          │           │      new SparkMD5.ArrayBuffer()     │    │
 * │Blob│ ────────────────────────────────► │ArrayBuffer│ ───────────────┬──────────────────► │Hash│
 * │    │   Method   readAsArrayBuffer      │           │       append() └────►  end()        │    │
 * └────┘                                   └───────────┘                                     └────┘
 */
self.onmessage = async (e) => {
  const { data } = e;
  const spark = new SparkMD5.ArrayBuffer();
  let percentage = 0;
  for (let i = 0, len = data.length; i < len; i++) {
    const eachArrayBuffer = await handleBlob2ArrayBuffer(data[i].file);
    percentage += 100 / len;
    self.postMessage({
      percentage,
    });
    spark.append(eachArrayBuffer);
  }
  const hash = spark.end();
  self.postMessage({
    percentage: 100,
    hash,
  });
  self.close();
};

主线程调用 Hash 工作者线程

把处理 hash 的函数包裹成 Promise,前端处理完 hash 之后传递给后端

把每个chunk 的包裹也精简了一下,只传递 Blobindex

在这里插入图片描述

再把后端的参数调整一下

在这里插入图片描述

最后我的文件结构如下:

在这里插入图片描述

添加 hash 进度

简单写一下页面,效果如下:
请添加图片描述

在这里插入图片描述

后端

接口:判断秒传

写一个接口判断一下是否存在即可

/**
 * 功能:验证服务器中是否存在文件
 * - 1. 主要是拼接的任务
 * - 2. ext 的值前面是有 . 的,注意一下。我之前合并好的文件 xxx..mkv 有两个点...
 * - 导致 fse.existsSync 怎么都找不到,哭
 * @param {*} req
 * @param {*} res
 * @param {*} MERGE_DIR
 */
async handleVerify(req, res, MERGE_DIR) {
  const postData = await handlePostData(req);
  const { fileHash, fileName } = postData;
  const ext = path.extname(fileName);
  const willCheckMergedName = `${fileHash}${ext}`;
  const willCheckPath = path.resolve(MERGE_DIR, willCheckMergedName);
  if (fse.existsSync(willCheckPath)) {
    res.end(
      JSON.stringify({
        code: 0,
        message: 'existed',
      })
    );
  } else {
    res.end(
      JSON.stringify({
        code: 1,
        message: 'no exist',
      })
    );
  }
}

前端这边在 hash 计算后把结果传给后端,让后端去验证

在这里插入图片描述

秒传就差不多啦!请添加图片描述

参考文章

  1. path.resolve() 解析
  2. 字节跳动面试官:请你实现一个大文件上传和断点续传
  3. 《高级JavaScript设计》第四版:第 27 章
  4. Spark-MD5
  5. 布隆过滤器

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pytorch:YOLOV1的pytorch实现

pytorch&#xff1a;YOLOV1的pytorch实现 注&#xff1a;本篇仅为学习记录、学习笔记&#xff0c;请谨慎参考&#xff0c;如果有错误请评论指出。 参考&#xff1a; 动手学习深度学习pytorch版——从零开始实现YOLOv1 目标检测模型YOLO-V1损失函数详解 3.1 YOLO系列理论合集(Y…

【IDEA】解决mac版IDEA,进行单元测试时控制台不能输入问题

我的IDEA版本 编辑VM配置 //增加如下配置&#xff0c;重启IDEA -Deditable.java.test.consoletrue测试效果

风力发电对讲 IP语音对讲终端IP安防一键呼叫对讲 医院对讲终端SV-6005网络音频终端

风力发电对讲 IP语音对讲终端IP安防一键呼叫对讲 医院对讲终端SV-6005网络音频终端 目 录 1、产品规格 2、接口使用 2.1、侧面接口功能 2.2、背面接口功能 2.3、面板接口功能 3、功能使用 1、产品规格 输入电源&#xff1a; 12V&#xff5e;24V的直流电源 网络接口&am…

初级数据结构(二)——链表

文中代码源文件已上传&#xff1a;数据结构源码 <-上一篇 初级数据结构&#xff08;一&#xff09;——顺序表 | NULL 下一篇-> 1、链表特征 与顺序表数据连续存放不同&#xff0c;链表中每个数据是分开存放的&#xff0c;而且存放的位置尤其零散&#…

C# WPF上位机开发(会员管理软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 好多同学都认为上位机只是纯软件开发&#xff0c;不涉及到硬件设备&#xff0c;比如听听音乐、看看电影、写写小的应用等等。如果是消费电子&#…

人工智能从 DeepMind 到 ChatGPT ,从 2012 - 2024

本心、输入输出、结果 文章目录 人工智能从 DeepMind 到 ChatGPT &#xff0c;从 2012 - 2024前言2010年&#xff1a;DeepMind诞生2012&#xff5e;2013年&#xff1a;谷歌重视AI发展&#xff0c;“拿下”Hinton2013&#xff5e;2014年&#xff1a;谷歌收购DeepMind2013年&…

调用别人提供的接口无法通过try catch捕获异常(C#),见鬼了

前几天做CA签名这个需求时发现一个很诡异的事情&#xff0c;CA签名调用的接口是由另外一个开发部门的同事(比较难沟通的那种人)封装并提供到我们这边的。我们这边只需要把数据准备好&#xff0c;然后调他封装的接口即可完成签名操作。但在测试过程中&#xff0c;发现他提供的接…

2024年网络安全竞赛-数字取证调查attack817

​ 数字取证调查 (一)拓扑图 服务器场景:FTPServer20221010(关闭链接) 服务器场景操作系统:未知 FTP用户名:attack817密码:attack817 分析attack.pcapng数据包文件,通过分析数据包attack.pcapng找出恶意用户第一次访问HTTP服务的数据包是第几号,将该号数作为Flag值…

力扣刷题总结 字符串(2)【KMP】

&#x1f525;博客主页&#xff1a; A_SHOWY&#x1f3a5;系列专栏&#xff1a;力扣刷题总结录 数据结构 云计算 数字图像处理 28.找出字符串中第一个匹配项的下标mid经典KMP4593重复的子字符串mid可以使用滑动窗口或者KMP KMP章节难度较大&#xff0c;需要深入理解其中…

Uncaught SyntaxError: Unexpected end of input (at manage.html:1:21) 的一个解

关于Uncaught SyntaxError: Unexpected end of input (at manage.html:1:21)的一个解 问题复现 <button onclick"deleteItem(${order.id},hire,"Orders")" >delete</button>报错 原因 函数参数的双引号和外面的双引号混淆了&#xff0c;改成…

数据可视化|jupyter notebook运行pyecharts,无法正常显示“可视化图形”,怎么解决?

前言 本文是该专栏的第39篇,后面会持续分享python数据分析的干货知识,记得关注。 相信有些同学在本地使用jupyter notebook运行pyecharts的时候,在代码没有任何异常的情况下,无论是html还是notebook区域,都无法显示“可视化图形”,界面区域只有空白一片。遇到这种情况,…

精选Axure原型设计模板,RP原型组件库(PC端移动端元件库及Axure函数及运算符说明)

好的原型组件会大大的提高产品经理的工作效率&#xff0c;小7在陆续整理、精选Axure 8的原型设计模板&#xff0c;包含了原型设计的常用元素和AxureRP 8函数及运算符的说明文档&#xff0c;及各种设备模板框架。 本文也是基于小7另一篇文章的补充&#xff0c;更多更详细的资料…

ffprobe命令行超详细使用详解

本文做为阅读ffprobe源码的前课程。为了之后方便理解ffprobe的源码,咱们先从ffprobe的命令学习。 课程内容如下: 文章目录 一、ffprobe主要选项说明1、每次使用ffprobe都打印编译环境的信息,太烦了2、如何分析媒体文件中存在的流信息3、如何指定查询某路流信息4、查看输入文…

2020年第九届数学建模国际赛小美赛B题血氧饱和度的变异性解题全过程文档及程序

2020年第九届数学建模国际赛小美赛 B题 血氧饱和度的变异性 原题再现&#xff1a; 脉搏血氧饱和度是监测患者血氧饱和度的常规方法。在连续监测期间&#xff0c;我们希望能够使用模型描述血氧饱和度的模式。   我们有36名受试者的数据&#xff0c;每个受试者以1 Hz的频率连…

基于ssm人力资源管理系统论文

摘 要 随着企业员工人数的不断增多&#xff0c;企业在人力资源管理方面负担越来越重&#xff0c;因此&#xff0c;为提高企业人力资源管理效率&#xff0c;特开发了本人力资源管理系统。 本文重点阐述了人力资源管理系统的开发过程&#xff0c;以实际运用为开发背景&#xff0…

Qt开发 之 记一次安装 Qt5.12.12 安卓环境的失败案例

文章目录 1、安装Qt2、安卓开发的组合套件2.1、CSDN地址2.2、官网地址2.3、发现老方法不适用了 3、尝试用新方法解决3.1、先安装JDK&#xff0c;搞定JDK环境变量3.1.1、安装jdk3.1.2、确定jdk安装路径3.1.3、打开系统环境变量配置3.1.4、配置系统环境变量3.1.5、验证JDK环境变量…

OrangePi ZERO2 刷机与启动

镜像准备 用读卡器和Win32Diskimager刷写镜像到内存卡&#xff0c;镜像文件见下面百度云链接&#xff1a;https://pan.baidu.com/s/14aKTznc4Jvw4SoFF54JUTg 提取码&#xff1a;1815 刷写完毕后插回香橙派 串口登录 用MobaXterm和USB-TTL进行串口登录&#xff0c;MobaXterm软…

线程安全3--wait和notify

文章目录 wait and notify&#xff08;等待通知机制notify补充 wait and notify&#xff08;等待通知机制 引入wait notify就是为了能够从应用层面上&#xff0c;干预到多个不同线程代码的执行顺序&#xff0c;这里说的干预&#xff0c;不是影响系统的线程调度策略&#xff08…

持续集成交付CICD:Jenkins流水线实现Nexus制品晋级策略

目录 一、理论 1.开发测试运维环境 二、实验 1.Nexus制品晋级策略 一、理论 1.开发测试运维环境 &#xff08;1&#xff09;环境 1&#xff09;持续集成开发环境&#xff08;DEV: Development Environment&#xff09; 直接通过源代码编译打包&#xff0c;其会跑单元测试…

C# | 使用AutoResetEvent和ManualResetEvent进行线程同步和通信

使用AutoResetEvent和ManualResetEvent进行线程同步和通信 文章目录 使用AutoResetEvent和ManualResetEvent进行线程同步和通信介绍AutoResetEventManualResetEvent 异同点使用场景和代码示例AutoResetEvent 使用示例ManualResetEvent 使用示例阻塞多个线程并同时激活 介绍 在…