JavaScript使用WebUploader做大文件的分块和断点续传

之前仿造uploadify写了一个HTML5版的文件上传插件,没看过的朋友可以点此先看一下~得到了不少朋友的好评,我自己也用在了项目中,不论是用户头像上传,还是各种媒体文件的上传,以及各种个性的业务需求,都能得到满足。小小开心了一把。

  但无论插件再怎么灵活,也难以应付所有的需求,比如,你要上传一个2G的文件。以现在我们的网速,恐怕再快也得传半小时。要命的是,如果你在上传到90%的时候不小心关掉了浏览器,或者是手一抖摁了F5,完了,一切还得从头再来。这种用户体验简直太糟糕了。所以,断点续传就十分有必要了。什么是续传我就不解释了,用QQ传文件这么多年,大家都见过了。

  这里要说的是断点续传都有哪些技术要点。使用传统的表单提交文件或是HTML5的FormData都是将文件“整块”提交,服务端取到该文件后再进行转移、重命名等操作,因此,无法实时保存文件的已上传部分。而且在http协议下,我们无法保持浏览器与服务端的长连接,不能以文件流的形式来提交。所以要解决的问题具体来讲有以下几点:

对上传的文件进行分割,每次只上传一小片。服务端接收到文件后追加到原来部分,最后合并成完整的文件。

每次上传文件片前先获取已上传的文件大小,确定本次应切割的位置

每次上传完成后更新已上传文件大小的记录

标识客户端和服务端的文件,保证不会把A文件的内容追加到B文件上

  在参考了张鑫旭大哥的这篇文章后,我将学到的技术应用在了我的插件Huploadify中,成功的添加了断点续传功能。在此将技术和插件都分享给大家。

工作原理/技术要点

  首先的首先,要明确,如果我们有一个10M的文件,每次切割上传1M,那么是需要发10次请求来完成的。在http协议下,只能这么搞。断点上传分三步来完成:

选择一个文件后,获取该文件在服务器上的大小,通过本地存储或自定义的函数来获取。

根据已上传大小切割文件,发出n次请求不断向服务器提交文件片,服务端不断追加文件内容

当已上传文件大小达到文件总大小时,上传结束 

  首先是文件的分割,HTML5新增了Blob数据类型,并且提供了一个可以分割数据的方法:slice(),其用法和字符串、数组的slice()方法一样,可以截取一个二进制文件的一部分。

  其次是文件片的保存与追加,我后台用PHP写的,先用file_get_contents获取文件的二进制格式,再用file_put_contents每次将文件追加,具体的写法可以参照后面,或者是下载我打包好的文件。

  接下来我们还需要实时保存已上传文件的大小,以便于下次上传前进行正确切割。使用HTML5的localStorage是一种方法,将已上传的大小保存在本地,下次上传前先从本地读取。不过这种方式是很局限的,抛开用户可能通过各种管家清除掉本地数据不讲,假如用户在A页面上传了一个文件的50%,然后在B页面想把该文件上传到另外一个地方,结果从本地一读文件已上传50%了,直接从51%的位置开始上传了,显然是个错误。问题就在于本地不能存太多的信息,通过File API只能获取到文件的原始名称,无法正确的与服务器上的文件正确匹配。所以真正在项目中用,还得依靠服务端来保存这些数据。

  关于如何将数据存在服务端,已经前端如何取数据,我在下面会讲到。

  技术要点就上面的那么多了,其实也没有多少技术含量哈~来看看我的插件如何使用吧。

续传功能的使用方法

  文件的引入就不讲了,可参考上一篇关于插件的介绍。关键点是新增的几个配置,先来看一下:

在服务端保存数据

  用户在使用上传的时候可能有各种你意想不到的操作,这里我发挥想象描述一下用户可能的行为:

同一台机器使用不同帐号登录,上传同一个文件

文件上传了一部分,然后修改了文件内容,再次上传

文件上传完成100%,再次上传该文件

同一个页面有多个上传按钮,上传同一个文件,或在不同页面上传同一个文件

  仅仅上面四条,是不是情况就够复杂了?再加上你系统还有自己的业务逻辑,所以在服务端保存已上传文件数据是非常有必要的。而且保存数据和获取数据的函数都交给你来定义,抱着插件有足够的灵活性。

  因为涉及到了服务端的技术,无法演示,我将我项目中的真实使用场景在此讲解一下,来展示一下如何自已定义方法来实现服务端保存数据的可靠上传。我定义的getUploadedSize函数如下:

文件初始化

文件上传完毕的代码

文件块的处理代码,up6对文件块的处理,以及文件续传的逻辑进行了大幅度的优化,开发者不需要关心续传的细节,因为up6默认就是自动续传

  我向后台的某个地址发送一个请求,传递文件名和文件的最后修改时间为参数,后台根据这两个参数来找到与前台所选择的文件对应的服务器上的文件,将服务器返回的文件大小return出去,来被插件使用。为什么要传递这两个参数呢?我们在前台无法知道服务器上的这个文件的名称,所以使用原始文件名作为一个辅助标识。为了防止用户在两次上传间隔修改了文件,我们把文件的最后修改时间也传给服务端,让服务端进行比较,若时间不对应则返回已上传大小为0,重新上传此文件。

  再来看后台都要做哪些工作。数据库中需要有一张表来记录每个已文件的情况,包含的字段大致有:

字段

描述

uid

用户ID

id

文件ID标识(唯一)

lenSvr

服务器文件大小

lenLoc

本地文件大小

blockOffset

文件块偏移(在整个文件中的位置)

blockSize

文件块大小

blockIndex

文件块索引(基于1)

blockMd5

文件块MD5

complete

当前文件是否已经传完

  根据client_filename和last_modified_date,再加上系统中的其他关联信息,可以定位到本次上传的文件在服务端的大小,然后返回给客户端。当然这是我自己的用法,你也可以根据自己的需求灵活设计。总之最终的目的就是要找到前台选择的文件在服务器上真正对应的文件,并将已上传大小正确返回。

  另外需注意的一点,就是在续传的第二步,不断提交文件片的过程中,也需要服务端准确定位到相应的文件,不能把A的数据追加到B上。采用的方式也是提交fileName和lastModifyDate两个参数(已写在插件内部,可服务端直接获取),服务端找到对应的文件进行追加。

  另外再啰嗦一句,后台获取文件的时候需要取成二进制的,而我们提交是使用FormData来提交的,所以PHP代码需要这么写:

file_put_contents('uploads/'.$filename,file_get_contents($_FILES["file"]["tmp_name"]),FILE_APPEND);

  如果上面的说明还是不够清楚,就需要你自己来探索一下了,毕竟考虑到插件可能应用在复杂的系统中,很多工作还是需要你来做的。或者你也可以给我留言,我很乐意为你解答疑惑。

该版本的其他改动

  从1.0到2.0,Huploadify又新加了很多东西,不过只是新加,使用方式跟之前的没有变化。例如上面的断点续传功能,你如果不想使用,只需设置breakPoints为false即可,插件仍按照以前的方式工作。除了断点续传这个大头,插件还做了如下改动:

增加了onSelect回调函数,在选择了文件之后触发,用法与uploadify官网的一致

up6提供了3个事件,选择文件,选择文件夹,粘贴

用户选择文件时会触发open_files,选择文件夹触发open_folders,粘贴会触发以上两个事件,因为用户可能粘贴的文件和文件夹

删除掉正在上传的文件,中断发送请求

完善了input file组件的accept属性支持,浏览时只显示运行的文件格式

  4. 对外开放了方法调用接口,upload、stop、cancel、disable、ennable。我在demo中有演示。使用方法如下:var up = $('#upload').Huploadify({

    auto:false,

    fileTypeExts:'*.jpg;*.png;*.exe;*.mp3;*.mp4;*.zip;*.doc;*.docx;*.ppt;*.pptx;*.xls;*.xlsx;*.pdf',

    multi:true

});

up.upload(1);//开始上传文件,接收一个参数,表示上传第几个文件,可传入*上传队列中的所有文件

up.stop();//暂停上传队列中的所有文件,不接收参数。用于开启了断点需传

up.cancel(1);//删除队列中的某个文件,接收一个参数,表示删除第几个文件,可传入*删除队列中的所有文件

up.disable();//使选择文件按钮失效,不接收参数

up.ennable();//使选择文件按钮生效,不接收参数  5. 修改其他已知bug

结束

  插件刚刚完成,与我们的后端程序员调试完成了断点续传功能暂未发现问题,欢迎大家在使用的时候给我提任何问题。老实来讲这个功能使用起来还是挺费解的,为了最大程度的保证灵活做成这样,大家可以与我多多交流~

  我在demo中使用了本地存储来做已上传文件大小的保存,下载压缩包后可看一下效果。上传一个比较大的视频文件,上传到中间关闭浏览器,再次打开浏览器上传同一个文件,会看到从上次断掉的地方继续上传。

详细内容可以参考我写的这篇文章:http://blog.ncmem.com/wordpress/2019/08/09/%e5%a4%a7%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
现在大部分的网站使用的是标准HTML的上传方式来上传文件。一般情况下标准HTML方式在网页中只能上传4MB左右的文件,如果访问的用户比较多的时侯这种方式容易上传失败。虽然标准HTML上传方式开发起来比较简单,但是这种方式用户体验比较差,上传的文件大小受到限制,所以如果我们需要上传上百或者更大的上G的文件时,HTML标准上传方式是无法满足我们的需求的。 而另一方面,随着互联网行业的发展用户产生的新的需求也越来越多,同时对用户体验也提出了更高的要求,传统的HTML方式也越来越难已满足新的用户需求。现在大部分的用户有文件批量上传的需求,希望只通过点击一次鼠标就能够批量的上传多张图片,而不是一张张的选择文件上传,这样操作即浪费时间又非常烦琐。 近年来,由于数码和影视行业的迅猛发展刺激了用户对大文件的上传需求,现在越来越多的用户希望能够通过WEB的方式上传更大的文件,比如电影和图片。这些类型的文件通常都非常大,一般都在500MB以上,高清的影视文件至少在1G以上。这样的大文件是根本无法通过标准HTML方式来上传的。 不仅如此,由于国内网络环境比较特殊,有许多地区的网络不够稳定,在上传文件的过程中可能会发生断网的情况。如果用户正在上传一个1000MB的文件,已经上传了500MB,这时网络出现问题上传中止了。那么下一次用户需要要重新上传前面的500MB,而不是从500MB开始上传,这将浪费用户的许多时间。 新颖网络HTTP文件断点续传控件是专门用于解决HTTP大文件上传的需求而开发的产品。通过我们的HttpPartition模块用户能够非常方便的一次性选择超过200个的文件。而且我们升级了用户体验,用户现在不仅能够通过点击按钮来选择多个文件,还可以通过HttpDroper来拖拽文件甚至是文件夹。 现在我们能够轻松支持2G左右的大文件上传。为了减轻服务器的压力在HttpUploader模块中我们并不是一次上传2G的数据,而是将2G化分为小的数据块,每次向服务器上传约128KB左右的数据。同时在每次上传的数据中附带了文件大小,起始位置,文件MD5等信息。对于开发人员来说,有了这些信息,断点续传功能将会变的和普通的文件上传功能一样简单。 相信新颖网络HTTP断点续传控件能够帮助您赢利市场。 版权所有 2009-2012 北京新颖网络 保留所有权利 官方网站:http://www.ncmem.com/ 产品首页:http://www.ncmem.com/webplug/http-uploader3/index.aspx 在线演示:http://www.ncmem.com/products/http-uploader/demo/index.html 产品介绍:http://www.cnblogs.com/xproer/archive/2012/02/17/2355440.html 开发文档-ASP:http://www.cnblogs.com/xproer/archive/2012/02/17/2355458.html 开发文档-PHP:http://www.cnblogs.com/xproer/archive/2012/02/17/2355467.html 开发文档-JSP:http://www.cnblogs.com/xproer/archive/2012/02/17/2355462.html 开发文档-ASP.NET:http://www.cnblogs.com/xproer/archive/2012/02/17/2355469.html 升级日志:http://www.cnblogs.com/xproer/archive/2012/02/17/2355449.html 示例下载:http://www.ncmem.com/download/HttpUploader3-demo.rar 文档下载:http://www.ncmem.com/download/HttpUploader3-doc.rar 问题反馈:http://www.ncmem.com/blog/guestbook.asp Windows数字证书补丁:http://www.ncmem.com/download/rootsupd.rar Microsoft Visual C++ 2008 Redistributable Package (x86):http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=29
好的,以下是一个使用 Node.js 实现大文件切片上传和断点续传的功能示例: ```javascript const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = 8000; const UPLOAD_DIR = path.resolve(__dirname, './uploads'); // 创建上传目录 if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR); } // 上传文件处理函数 const handleUpload = (req, res) => { const { filename, chunkIndex, totalChunks } = req.headers; const chunkDir = path.resolve(UPLOAD_DIR, filename); // 如果是第一个分片,创建文件夹 if (chunkIndex === '0') { fs.mkdirSync(chunkDir); } // 获取上传的分片数据 const chunksData = []; req.on('data', (chunk) => { chunksData.push(chunk); }); req.on('end', () => { const buffer = Buffer.concat(chunksData); // 写入分片文件 fs.writeFileSync(path.resolve(chunkDir, chunkIndex), buffer); // 如果当前分片是最后一个分片,则合并文件 if (Number(chunkIndex) === Number(totalChunks) - 1) { const filePath = path.resolve(UPLOAD_DIR, filename); const chunks = fs.readdirSync(chunkDir); const writeStream = fs.createWriteStream(filePath); chunks.forEach((chunk) => { const chunkPath = path.resolve(chunkDir, chunk); const chunkBuffer = fs.readFileSync(chunkPath); fs.unlinkSync(chunkPath); // 删除分片文件 writeStream.write(chunkBuffer); }); writeStream.end(() => { res.end('upload success'); }); } else { res.end('chunk upload success'); } }); }; // 断点续传处理函数 const handleResumeUpload = (req, res) => { const { filename } = req.headers; const filePath = path.resolve(UPLOAD_DIR, filename); const fileStat = fs.statSync(filePath); res.setHeader('Content-Length', fileStat.size); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Accept-Ranges', 'bytes'); const range = req.headers.range || 'bytes=0-'; const positions = range.replace(/bytes=/, '').split('-'); const start = parseInt(positions[0], 10); const end = positions[1] ? parseInt(positions[1], 10) : fileStat.size - 1; const chunkSize = end - start + 1; res.setHeader('Content-Range', `bytes ${start}-${end}/${fileStat.size}`); res.setHeader('Cache-Control', 'no-cache'); const readStream = fs.createReadStream(filePath, { start, end }); readStream.on('open', () => { readStream.pipe(res); }); readStream.on('error', () => { res.end('Error'); }); }; // 创建 HTTP 服务器 const server = http.createServer((req, res) => { if (req.url === '/upload' && req.method === 'POST') { handleUpload(req, res); } else if (req.url === '/resume-upload' && req.method === 'GET') { handleResumeUpload(req, res); } else { res.end('Hello World!'); } }); // 启动服务器 server.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); }); ``` 使用示例: 1. 开启服务器:`node server.js` 2. 上传文件使用 POST 请求发送文件分片(每个分片的大小可以自定义),请求头需要包含 `filename`(文件名)、`chunkIndex`(当前分片索引,从 0 开始)、`totalChunks`(总分片数)三个字段。 3. 断点续传使用 GET 请求获取已上传的文件,请求头需要包含 `filename`(文件名)字段和 `range`(请求的字节范围)字段。如果请求头中没有 `range` 字段,则返回整个文件的内容。如果请求头中有 `range` 字段,则只返回指定字节范围内的文件内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值