【文件上传系列】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. 布隆过滤器
  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 微信小程序开发是一种基于微信平台的应用程序开发方式,可以在微信内直接运行的应用程序。它可以实现类似于APP的功能,但相比APP更加轻量、快速、易于开发和使用。 微信小程序的开发主要分为前端后端两部分。 前端开发是指通过使用小程序开发框架(如原生开发、wepy等)进行界面和交互的开发。开发者可以使用HTML、CSS和Javascript等技术进行整体的页面布局设计和交互逻辑的编写。通过小程序开发框架提供的API和组件,可以实现丰富的界面效果和功能。在前端开发中,可以通过调用后端接口获取数据或进行页面跳转等操作。 后端开发是指通过使用Java等编程语言进行服务器端的开发。在微信小程序中,后端开发主要用于数据的处理和管理。开发者可以通过后端开发来搭建服务器、编写API接口,实现数据存储和处理、权限控制、业务逻辑等功能。后端开发需要结合小程序前端的需求,定义数据的格式和访问方式,并与前端进行交互。 微信小程序开发前端后端的配合是整个开发过程中的重要环节。开发者需要根据产品需求和设计稿进行界面和交互的开发,并将数据以适当的格式输给后端进行处理和管理。前后端的协作可以通过API接口进行,前端调用后端提供的接口,递参数并获取数据。开发者可以根据具体需求和开发框架的限制进行功能开发和调试,最终实现微信小程序的各项功能需求。 总之,微信小程序开发需要前端后端的配合,前端负责界面和交互的开发,后端负责数据的处理和管理,两者相互合作,共同实现微信小程序的功能和效果。 ### 回答2: 微信小程序开发包括前端后端两个主要部分。 1. 前端开发:微信小程序前端开发主要使用HTML、CSS和JavaScript等技术,通过编写小程序页面的HTML结构、样式和交互逻辑来实现小程序的界面和功能。开发者可以使用微信提供的开发者工具进行开发和调试,还可以利用第三方框架如Vue.js、React等来简化开发流程。 在前端开发中,开发者需要了解微信小程序的基本组件和API,以及小程序的生命周期、页面间的跳转和递数据等。同时,还需要掌握微信小程序的开发规范和设计原则,以保证用户体验和小程序的可靠性。 2. 后端开发:微信小程序后端开发主要使用Java等编程语言进行实现。后端开发者需要负责处理小程序前端发送来的请求,验证用户身份,获取和处理数据,并将结果返回给前端。 在后端开发中,开发者需要使用Java开发框架如Spring Boot、Spring Cloud等,搭建服务器环境并实现业务逻辑。此外,还需要与数据库进行交互,操作和管理数据。同时,为了提高小程序的性能和可靠性,开发者还需要进行性能优化、错误处理和安全防护等工作。 综上所述,微信小程序开发需要前端后端的配合合作。前端负责实现小程序的界面和用户交互,后端负责处理数据和业务逻辑。通过整合两者的能力,可以开发出功能完善、用户体验较好的微信小程序。 ### 回答3: 微信小程序是一种基于微信平台的应用程序开发模式,它具有轻量级、快速加载和便捷的特点。微信小程序开发涉及到前端后端两方面的技术。 在前端开发中,我们需要掌握HTML、CSS和JavaScript等基础技术,同时需要熟悉微信小程序提供的开发框架和API。前端开发主要包括页面布局、样式设计、交互逻辑实现等工作。通过使用微信小程序的框架和API,我们可以快速地开发小程序,并且能够提供良好的用户体验。 在后端开发中,我们通常选择使用Java语言进行开发。Java是一种常用的编程语言,具有广泛的应用领域和稳定的性能。后端开发主要涉及到数据处理、业务逻辑编写、接口开发等方面的工作。我们可以使用Java的一些开发框架和工具,如Spring、SpringBoot等,来进行后端开发。这些框架和工具提供了丰富的功能模块,可以帮助我们快速地构建小程序所需的后端服务。 总而言之,微信小程序的开发涉及到前端后端两方面的技术。前端开发主要负责小程序的页面设计和交互逻辑实现,后端开发则负责提供数据处理和业务逻辑支持。通过前端后端的协同工作,我们能够开发出功能完善、用户友好的微信小程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值