【文件上传系列】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. 布隆过滤器

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis集群(cluster)笔记

1. 定义&#xff1a; 由于数据量过大&#xff0c;单个Master复制集难以承担&#xff0c;因此需要对多个复制集进行集群&#xff0c;形成水平扩展每个复制集只负责存储整个数据集的一部分&#xff0c;这就是Redis的集群&#xff0c;其作用是提供在多个Redis节点间共享数据的程序…

【数据结构】栈和队列超详解!(Stack Queue)

文章目录 前言一、栈1、栈的基本概念2、栈的实现&#xff08;数组实现&#xff09;3、栈的基本操作3.1 栈的结构设计3.2 栈常见的基本函数接口 4、栈的实现4.1 初始化栈4.2 栈的销毁4.3 入栈4.4 出栈4.5 判空4.6 长度4.7 获取栈顶元素 完整代码Stack.hStack.cTest.c 二、队列1、…

【JavaWeb学习专栏 | CSS篇】css简单介绍 css常用选择器集锦

个人主页&#xff1a;[兜里有颗棉花糖(https://xiaofeizhu.blog.csdn.net/) 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【JavaWeb学习专栏】【Java系列】 希望本文内容可以帮助到大家&#xff0c;一起加油吧&#xff01;…

Ubuntu安装向日葵【远程控制】

文章目录 引言下载向日葵安装向日葵运行向日葵卸载向日葵参考资料 引言 向日葵是一款非常好用的远程控制软件。这一篇博文介绍了如何在 Ubuntu Linux系统 中安装贝瑞向日葵。&#x1f3c3;&#x1f4a5;&#x1f4a5;&#x1f4a5;❗️ 下载向日葵 向日葵官网: https://sunl…

CAN总线协议编程实例

1. can.h #ifndef __CAN_H #define __CAN_H#include "./SYSTEM/sys/sys.h"/******************************************************************************************/ /* CAN 引脚 定义 */#define CAN_RX_GPIO_PORT GPIOA #define CAN_RX_GPI…

unity 2d 入门 飞翔小鸟 死亡 显示GameOver(十四)

1、添加Img create->ui->img 把图片拖进去 2、和分数一样、调整位置 3、修改角色脚本 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Fly : MonoBehaviour {//获取小鸟&#xff08;刚体&#xff09;private Rigidbod…

在springboot中引入参数校验

一、概要 一般我们判断前端传过来的参数&#xff0c;需要对某些值进行判断&#xff0c;是否满足条件。 而springboot相关的参数校验注解&#xff0c;可以解决我们这个问题。 二、快速开始 首先&#xff0c;我用的springboot版本是 3.1.5 引入参数校验相关依赖 <!--1…

ArkUI Button组件

Button 1.声明button组件 Button(label?:ResourceStr) label是按钮上面显示的文字 如果不传入label 则需要在内部嵌套其他组件 内部嵌套其他组件 可以放入icon图标来构建自己想要的样式 按钮类型 按钮使用type(ButtonType.xxx)属性来设置&#xff0c;xxx的类型分为三种 1.…

无人机自动停机坪的多样化选择

随着巡查无人机的广泛应用&#xff0c;无人机自动停机坪成为一项重要的支持设施&#xff0c;主要用于提供停放、充电/换电、机身保护以及气象监测等功能。尽管许多人认为无人机自动停机坪只是一个简单的箱体结构&#xff0c;但实际上&#xff0c;国内无人机自动停机坪产品在外观…

Android View.inflate 和 LayoutInflater.from(this).inflate 的区别

前言 两个都是布局加载器&#xff0c;而View.inflate是对 LayoutInflater.from(context).inflate的封装&#xff0c;功能相同&#xff0c;案例使用了dataBinding。 View.inflate(context, layoutResId, root) LayoutInflater.from(context).inflate(layoutResId, root, fals…

HarmonyOS开发工具DevEco Studio的下载和安装

一、DevEco Studio概述 一、下载安装鸿蒙应用开发工具DevEco Studio 开发鸿蒙应用可以从鸿蒙系统上运行第一个程序Hello World开始。 为了得到这个Hello World&#xff0c;你需要得到这个Hello World的源代码&#xff0c;源代码是用人比较容易看得懂的计算机编程语言规范写的…

总结一篇本地idea配合阿里云服务器使用docker

idea打包打镜像发到阿里云服务器 先说一下使用docker desktop软件怎么使用 1.下载docker desktop官网&#xff0c;先注册个账号吧&#xff0c;后面桌面软件登录会用到&#xff08;当然&#xff0c;配合这个软件使用需要科学上网&#xff09; 安装这个要配合wsl使用&#xf…

Spring Bean基础

写在最前面: 本文运行的示例在我github项目中的spring-bean模块&#xff0c;源码位置: spring-bean 前言 为什么要先掌握 Spring Bean 的基础知识&#xff1f; 我们知道 Spring 框架提供的一个最重要也是最核心的能力就是管理 Bean 实例。以下是其原因&#xff1a; 核心组件…

静态SOCKS5的未来发展趋势和新兴应用场景

随着网络技术的不断发展和进步&#xff0c;静态SOCKS5代理也在不断地完善和发展。未来&#xff0c;静态SOCKS5代理将会呈现以下发展趋势和新兴应用场景。 一、发展趋势 安全性更高&#xff1a;随着网络安全问题的日益突出&#xff0c;用户对代理服务器的安全性要求也越来越高…

SQL语句的执行顺序怎么理解?

SQL语句的执行顺序怎么理解&#xff1f; 我们常常会被SQL其书写顺序和执行顺序之间的差异所迷惑。理解这两者的区别&#xff0c;对于编写高效、可靠的SQL代码至关重要。今天&#xff0c;让我们用一些生动的例子和场景来深入探讨SQL的执行顺序。 一、书写顺序 VS 执行顺序 SQ…

SVN修改已提交版本的日志方法

1.在工做中一直是使用svn进行項目的版本控制的&#xff0c;有时候因为提交匆忙&#xff0c;或是忘了添加Log&#xff0c;或是Log内容有错误。遇到此类状况&#xff0c;想要在查看项目的日志时添加log或是修改log内容&#xff0c;遇到以下错误&#xff1a; Repository has not b…

openEuler 22.03 升级openssh9.5

安装telnet 进行下面操作前&#xff0c;务必确保telnet服务安装成功。 安装xinetd yum install xinetd -y安装telnet服务&#xff0c;下载地址下载地址 rpm -ivh telnet-0.17-86.aarch64.rpm rpm -ivh telnet-server-0.17-86.aarch64.rpm重启 service xinetd restart确保能…

php实现个性化域名(短网址)和个性化登录模版的解决方案

在PHP中&#xff0c;个性化域名通常指的是根据用户或业务需求动态生成具有特定规律的子域名。实现个性化域名的方法主要依赖于服务器配置和路由规则。下面是一些基本的步骤和考虑因素&#xff0c;以帮助你了解如何个性化域名&#xff0c;并了解这样做的好处。 如何实现个性化域…

基于java swing 药品销售管理系统

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…