首页 > 其他分享 >大文件上传

大文件上传

时间:2023-07-20 15:11:06浏览次数:18  
标签:文件 file 切片 let context 上传

文件异步上传实现

普通表单上传

使用PHP来展示常规的表单上传是一个不错的选择。首先构建文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表明表单需要上传二进制数据。

<form action="/index.php" method="POST" enctype="multipart/form-data">
  <input type="file" name="myfile">
  <input type="submit">
</form>

然后编写index.php上传文件接收代码,使用move_uploaded_file方法即可

form表单上传大文件时,很容易遇见服务器超时的问题。通过xhr,前端也可以进行异步上传文件的操作,一般由两个思路。

文件编码上传

将文件进行编码,然后在服务端进行解码,其主要实现原理就是将图片转换成base64进行传递

var imgURL = URL.createObjectURL(file);
ctx.drawImage(imgURL, 0, 0);
// 获取图片的编码,然后将图片当做是一个很长的字符串进行传递
var data = canvas.toDataURL("image/jpeg", 0.5); 

在服务端需要做的事情也比较简单,首先解码base64,然后保存图片即可

base64编码的缺点在于其体积比原图片更大(因为Base64将三个字节转化成四个字节,因此编码后的文本,会比原文本大出三分之一左右),对于体积很大的文件来说,上传和解析的时间会明显增加。

除了进行base64编码,还可以在前端直接读取文件内容后以二进制格式上传

// 读取二进制文件
function readBinary(text){
   var data = new ArrayBuffer(text.length);
   var ui8a = new Uint8Array(data, 0);
   for (var i = 0; i < text.length; i++){ 
     ui8a[i] = (text.charCodeAt(i) & 0xff);
   }
   console.log(ui8a)
}

var reader = new FileReader();
reader.onload = function(){
      readBinary(this.result) // 读取result或直接上传
}
// 把从input里读取的文件内容,放到fileReader的result字段里
reader.readAsBinaryString(file);

formData异步上传

FormData对象主要用来组装一组用 XMLHttpRequest发送请求的键/值对,可以更加灵活地发送Ajax请求。可以使用FormData来模拟表单提交。

let files = e.target.files // 获取input的file对象
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);

服务端处理方式与直接form表单请求基本相同。

iframe无刷新页面

在低版本的浏览器(如IE)上,xhr是不支持直接上传formdata的,因此只能用form来上传文件,而form提交本身会进行页面跳转,这是因为form表单的target属性导致的,其取值有

  • _self,默认值,在相同的窗口中打开响应页面
  • _blank,在新窗口打开
  • _parent,在父窗口打开
  • _top,在最顶层的窗口打开
  • framename,在指定名字的iframe中打开

如果需要让用户体验异步上传文件的感觉,可以通过framename指定iframe来实现。把form的target属性设置为一个看不见的iframe,那么返回的数据就会被这个iframe接受,因此只有该iframe会被刷新,至于返回结果,也可以通过解析这个iframe内的文本来获取。

function upload(){
    var now = +new Date()
    var id = 'frame' + now
    $("body").append(`<iframe style="display:none;" name="${id}" id="${id}" />`);

    var $form = $("#myForm")
    $form.attr({
        "action": '/index.php',
        "method": "post",
        "enctype": "multipart/form-data",
        "encoding": "multipart/form-data",
        "target": id
    }).submit()

    $("#"+id).on("load", function(){
        var content = $(this).contents().find("body").text()
        try{
            var data = JSON.parse(content)
        }catch(e){
            console.log(e)
        }
    })
}

大文件上传

现在来看看在上面提到的几种上传方式中实现大文件上传会遇见的超时问题,

  • 表单上传和iframe无刷新页面上传,实际上都是通过form标签进行上传文件,这种方式将整个请求完全交给浏览器处理,当上传大文件时,可能会遇见请求超时的情形
  • 通过fromData,其实际也是在xhr中封装一组请求参数,用来模拟表单请求,无法避免大文件上传超时的问题
  • 编码上传,我们可以比较灵活地控制上传的内容

大文件上传最主要的问题就在于:在同一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。如果我们将这个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始,这样是否可以解决大文件上传的问题呢?

综合上面的问题,看来大文件上传需要实现下面几个需求

  • 支持拆分上传请求(即切片)
  • 支持断点续传
  • 支持显示上传进度和暂停上传

文件切片

编码方式上传中,在前端我们只要先获取文件的二进制内容,然后对其内容进行拆分,最后将每个切片上传到服务端即可。

在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice,通过这个方法,我们就可以对二进制文件进行拆分。

function slice(file, piece = 1024 * 1024 * 5) {
 let totalSize = file.size; // 文件总大小
 let start = 0; // 每次上传的开始字节
 let end = start + piece; // 每次上传的结尾字节
 let chunks = []
 while (start < totalSize) {
 // 根据长度截取每次需要上传的数据
 // File对象继承自Blob对象,因此包含slice方法
 let blob = file.slice(start, end); 
 chunks.push(blob)
 start = end;
 end = start + piece;
 }
 return chunks
}

将文件拆分成piece大小的分块,然后每次请求只需要上传这一个部分的分块即可

let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH); // 首先拆分切片
chunks.forEach(chunk=>{
 let fd = new FormData();
 fd.append("file", chunk);
 post('/mkblk.php', fd)
})

服务器接收到这些切片后,再将他们拼接起来就可以了,下面是PHP拼接切片的示例代码

上面这种方式来存在一些问题

  • 无法识别一个切片是属于哪一个切片的,当同时发生多个请求时,追加的文件内容会出错
  • 切片上传接口是异步的,无法保证服务器接收到的切片是按照请求顺序拼接的

因此接下来我们来看看应该如何在服务端还原切片。

还原切片

在后端需要将多个相同文件的切片还原成一个文件,上面这种处理切片的做法存在下面几个问题

  • 如何识别多个切片是来自于同一个文件的,这个可以在每个切片请求上传递一个相同文件的context参数
  • 如何将多个切片还原成一个文件
  • 确认所有切片都已上传,这个可以通过客户端在切片全部上传后调用mkfile接口来通知服务端进行拼接
  • 找到同一个context下的所有切片,确认每个切片的顺序,这个可以在每个切片上标记一个位置索引值
  • 按顺序拼接切片,还原成文件

上面有一个重要的参数,即context,我们需要获取为一个文件的唯一标识,可以通过下面两种方式获取

  • 根据文件名、文件长度等基本信息进行拼接,为了避免多个用户上传相同的文件,可以再额外拼接用户信息如uid等保证唯一性
  • 根据文件的二进制内容计算文件的hash,这样只要文件内容不一样,则标识也会不一样,缺点在于计算量比较大

修改上传代码,增加相关参数

// 获取context,同一个文件会返回相同的值
function createContext(file) {
     return file.name + file.length
}
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);
// 获取对于同一个文件,获取其的context
let context = createContext(file);
let tasks = [];
chunks.forEach((chunk, index) => {
 let fd = new FormData();
 fd.append("file", chunk);
 // 传递context
 fd.append("context", context);
 // 传递切片索引值
 fd.append("chunk", index + 1);

 tasks.push(post("/mkblk.php", fd));
});
// 所有切片上传完毕后,调用mkfile接口
Promise.all(tasks).then(res => {
 let fd = new FormData();
 fd.append("context", context);
 fd.append("chunks", chunks.length);
 post("/mkfile.php", fd).then(res => {
 console.log(res);
 });
});

断点续传

即使将大文件拆分成切片上传,我们仍需等待所有切片上传完毕,在等待过程中,可能发生一系列导致部分切片上传失败的情形,如网络故障、页面关闭等。由于切片未全部上传,因此无法通知服务端合成文件。这种情况下可以通过断点续传来进行处理。

断点续传指的是:可以从已经上传部分开始继续上传未完成的部分,而没有必要从头开始上传,节省上传时间。

由于整个上传过程是按切片维度进行的,且mkfile接口是在所有切片上传完成后由客户端主动调用的,因此断点续传的实现也十分简单:

  • 在切片上传成功后,保存已上传的切片信息
  • 当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
  • 所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并

因此问题就落在了如何保存已上传切片的信息了,保存一般有两种策略

  • 可以通过locaStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
  • 服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件context查询已上传切片的接口,在上传文件前调用该文件的历史上传记录

下面通过在本地保存已上传切片记录,来实现断点上传的功能

// 获取已上传切片记录
function getUploadSliceRecord(context){
 let record = localStorage.getItem(context)
 if(!record){
 return []
 }else {
 try{
 return JSON.parse(record)
 }catch(e){}
 }
}
// 保存已上传切片
function saveUploadSliceRecord(context, sliceIndex){
 let list = getUploadSliceRecord(context)
 list.push(sliceIndex)
 localStorage.setItem(context, JSON.stringify(list))
}

然后对上传逻辑稍作修改,主要是增加上传前检测是已经上传、上传后保存记录的逻辑

let context = createContext(file);
// 获取上传记录
let record = getUploadSliceRecord(context);
let tasks = [];
chunks.forEach((chunk, index) => {
 // 已上传的切片则不再重新上传
 if(record.includes(index)){
 return
 }

 let fd = new FormData();
 fd.append("file", chunk);
 fd.append("context", context);
 fd.append("chunk", index + 1);
 let task = post("/mkblk.php", fd).then(res=>{
 // 上传成功后保存已上传切片记录
 saveUploadSliceRecord(context, index)
 record.push(index)
 })
 tasks.push(task);
});

此时上传时刷新页面或者关闭浏览器,再次上传相同文件时,之前已经上传成功的切片就不会再重新上传了。

服务端实现断点续传的逻辑基本相似,只要在getUploadSliceRecord内部调用服务端的查询接口获取已上传切片的记录即可,因此这里不再展开。

此外断点续传还需要考虑切片过期的情况:如果调用了mkfile接口,则磁盘上的切片内容就可以清除掉了,如果客户端一直不调用mkfile的接口,放任这些切片一直保存在磁盘显然是不可靠的,一般情况下,切片上传都有一段时间的有效期,超过该有效期,就会被清除掉。基于上述原因,断点续传也必须同步切片过期的实现逻辑。

上传进度和暂停

通过xhr.upload中的progress方法可以实现监控每一个切片上传进度。

上传暂停的实现也比较简单,通过xhr.abort可以取消当前未完成上传切片的上传,实现上传暂停的效果,恢复上传就跟断点续传类似,先获取已上传的切片列表,然后重新发送未上传的切片。

标签:文件,file,切片,let,context,上传
From: https://www.cnblogs.com/ggonekim/p/17568474.html

相关文章

  • java rmi上传下载512字节OutputStream
    /*JADXINFO:Accessmodifierschangedfrom:protected*/publicfinalvoiddownloadFile(Parameterparameter,OutputStreamout)throwsXException{if(!this.session.isEffective()){thrownewXException(10000000,"连接会话无效&......
  • linux环境中,一个目录下,除了特定的目录和这个目录里面的内容,删除所有其他的目录和文件
    有个tomcat目录,如下: [[email protected]]#ls-ltotal148drwxr-x---2rootroot4096Jul2014:37bin-rw-r-----1rootroot19992May421:04BUILDING.txtdrwx------2rootroot4096May421:04conf-rw-r-----1rootroot62......
  • 用Power Automate获取SharePoint文件内容
    1.根据文件夹路径获取路径下所有文件内容使用SharePoint连接器的“获取文件(仅属性)”操作,站点地址选SharePoint的默认站点,库名选择对应文档库,我这里把SharePoint集成到Dynamics365的实体附件,库名就是对应的实体名。仅限文件夹条目为 /entityname/relativeurl,entityname为实体名,r......
  • uniapp小程序(原生微信小程序也可以使用),获取接口二进制流数据上传文件到服务器
    需求:通过接口返回的二进制流数据,这个流数据他是一个xlsx文档,需要给到用户一个文档线上连接。下面是具体代码,注意只针对二进制的文件数据,如果图片上传直接调用uploadFile就可以,并且兼容原生微信小程序。exportfunctionexportExcel1(query){ uni.showLoading({ title:'正......
  • java如何生成一个.vm文件
    如何使用Java生成一个.vm文件在开发过程中,我们有时需要使用Velocity模板引擎生成.vm文件,以便在后续的操作中使用。本文将介绍如何使用Java生成一个.vm文件,并提供一个实际的示例。Velocity模板引擎简介Velocity是一款开源的模板引擎,它使用简单且功能强大,广泛应用于Java开发中。Ve......
  • jquery读取文件
    jQuery读取文件在前端开发中,我们经常需要读取文件的内容并将其显示在网页上,而使用jQuery可以方便地完成这个任务。jQuery是一个快速、简洁的JavaScript库,它简化了HTML文档的遍历、事件处理、动画等操作。下面我们将介绍如何使用jQuery读取文件的内容。使用jQuery的ajax方法读取文......
  • php读取txt文件内容
    1、如何使用PHP读取文本文件内容2、php如何获取txt文本指定行的指定数据?3、php读取txt文件指定行的内容并显示4、php获取文件夹里面多个txt文件中的随机一行?5、PHP+TXT读取文本内容并输出如何使用PHP读取文本文件内容1、如果直接使用file_get_contents来读取文件,那么在......
  • C#文件夹复制
    两种方式本质是一样的,代码也差不多。1、文件夹拷贝代码一:源文件夹名为AAA,目标文件夹为BBB。拷贝完成后,文件夹名字更改为BBB。比如AAA/1.jpng,会变成BBB.jpg。//如果前面是\Android,后面是com.xx,则复制之后的Android名字更改为com.xxpublicstaticintCopyFolder(str......
  • go通过docker sdk将容器中的文件或者文件夹复制到本地
    第一步:需要创建docker客户端第二步:将文件复制出来,但是复制出来的文件无论文件还是文件夹都会是tar存档的形式第三步:解包packagemainimport( "archive/tar" "context" "fmt" "io" "os" "path/filepath" "github.com/docker/docker/client&quo......
  • 信创环境下,使用国产服务器如何进行文件安全可靠传输?
    信创,即信息技术应用创新,2018年以来,受“华为、中兴事件”影响,国家将信创产业纳入国家战略,并提出了“2+8+n”发展体系。从产业链角度,信创产业生态体系较为庞大,主要包括基础硬件、基础软件、应用软件、信息安全4部分构成,其中芯片、服务器、操作系统、数据库等是最重要的产业链环节。......