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

这篇具有很好参考价值的文章主要介绍了【文件上传系列】No.2 秒传(原生前端 + Node 后端)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

上一篇文章

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


秒传效果展示

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式


秒传思路

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


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

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

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

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

前端

思路

具体思路如下

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

工作者线程简介

《高级JavaScript程序设计》27 章简介: 【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

// 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 结果【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

SparkMD5 计算 Hash 性能简单测试

js-spark-md5 的 github 地址

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

// 计算时间的代码
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();
  };
};

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

工作者线程:计算 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就差不多啦

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

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

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

再把后端的参数调整一下

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

最后我的文件结构如下:

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

添加 hash 进度

简单写一下页面,效果如下:
【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

后端

接口:判断秒传

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

/**
 * 功能:验证服务器中是否存在文件
 * - 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 计算后把结果传给后端,让后端去验证

【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式

秒传就差不多啦!【文件上传系列】No.2 秒传(原生前端 + Node 后端),# 精讲,前端,状态模式文章来源地址https://www.toymoban.com/news/detail-754828.html

参考文章

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

到了这里,关于【文件上传系列】No.2 秒传(原生前端 + Node 后端)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端上传文件夹或文件至后端(SpringBoot)

    前端上传文件夹使用的是 input 标签的file属性,最重要的是 webkitdirectory 这个属性,有了这个属性之后input才可以选择文件夹,但也只能选择文件夹了。 在webkitdirectory的官方文档里有对该属性的说明。 我们可以在这基础上做延伸,做一个表单来上传文件夹: form要加上 enctype=“

    2024年02月05日
    浏览(46)
  • node 第十四天 基于express的第三方中间件multer node后端处理用户上传文件

    Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。它是写在 busboy 之上的所以非常高效。 前面我们已经知道了怎样利用express提供的静态资源处理中间件 express.static() 处理用户请求静态资源文件(图片, js, css等) 接下来学习如何处理用

    2024年02月06日
    浏览(33)
  • 【Java实现文件上传】java后端+vue前端实现文件上传全过程详解(附源码)

    【 写在前面 】其实这篇文章我早就想写了,只是一直被需求开发耽搁,这不晚上刚好下班后有点时间,记录一下。需求是excel表格的上传,这个是很多业务系统不可或缺的功能点,再此也希望您能够读完我这篇文章对文件上传不再困惑。(文件下载见另外一篇) 涉及知识点

    2024年02月06日
    浏览(40)
  • 后端:使用easyExcel实现解析Excel文件读取数据。前端:Excel模板下载、前端上传文件

            本篇是EasyExcel快速入门知识,讲解如何读取Excel文件,对Excel中错误信息如空字符、必填项为空、表格格式校验做到处理 ,并给出了实际项目中示例代码;为什么要使用easyexcel;原因是相比于poi,easyexcel更加轻量级,读取写入API方便,并且在工作中占用内存较小;

    2024年02月05日
    浏览(53)
  • elementUI自定义上传文件(前端后端超详细过程)

    elementUI自定义上传文件(前端后端超详细过程) 简介 : 自定义 上传文件并与其他参数一同提交到后台( 主要使用axios ) 简单介绍组件( upload)的属性(熟悉参数的直接略过) 总结elmentUI一下它的使用和常用属性的作用。 主要目的自定义上传文件 2.1 组件代码 2.2 data中的属性

    2024年02月02日
    浏览(28)
  • elementUI自定义上传文件 前端后端超详细过程

    下面是使用Element UI自定义上传文件的前后端详细过程: 前端过程: 引入Element UI组件库:在前端项目中引入Element UI库,可以通过CDN引入或者通过npm安装并导入。 创建上传组件:在前端代码中创建一个上传组件,可以使用 el-upload 组件来实现文件上传功能。在组件中设置上传

    2024年02月11日
    浏览(29)
  • 前端上传的文件,后端将如何进行存储以及回显

    完成文件上传这个功能需要涉及到两个部分: 前端程序 服务器程序 文件上传后将如何进行储存 本地磁盘储存 云服务器oss储存 ##后端项目创建 创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok) 配置文件application.properties中引入mybatis的配置信息,准备对应

    2024年02月04日
    浏览(35)
  • 大文件上传demo,前端基于Uppy,后端基于koa

    文件上传基本上所有的管理系统之类的项目都有这么一个功能。因为使用了 Element ,可以方便的使用 其提供的 Upload 组件,对于普通上传来说基本上就够用了。但是有时候会涉及到大文件上传的需求,这时就会面临一些问题:比如文件上传超时。 自己做的话很麻烦,需要考虑

    2024年02月09日
    浏览(32)
  • 前端vue elementUI upload上传组件封装&多文件上传&进度条,后端servlet request.getPart()接收文件信息

    选中多个文件上传 通过 axios请求 onUploadProgress 方法监听 on-progress on-success 用这两个钩子函数实现进度条 下面有对应的函数。 本文是每个文件一个请求上传 也可以用一个请求上传多个文件,需要将文件遍历添加到 form 表单中,后端用 request.getParts(); 获取集合,有需要的可以改

    2024年02月11日
    浏览(35)
  • 基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传

    1. 前言 文件上传 小文件(图片、文档、视频)上传可以直接使用很多ui框架封装的上传组件,或者自己写一个input 上传,利用FormData 对象提交文件数据,后端使用spring提供的MultipartFile进行文件的接收,然后写入即可。但是对于比较大的文件,比如上传2G左右的文件(http上传

    2024年02月06日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包