wangpin34 / blog

个人博客, 博文写在 Issues 里

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

文件上传二三事

wangpin34 opened this issue · comments

commented

引子

其实很早就开始酝酿这一篇了,无奈总是发现有缺漏的地方,遂努力恶补前端+后端+底层相关知识。今天终于可以发表了。

--跟生孩子一样啊。

选择文件

谈到文件上传,不得不提 form,中文名叫表单。它可以包含一个用来选择文件的东东,叫做 file。

<form name="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
file:<input type="file" name="anyname"/>
</form>

action 表示表单的数据发送的目标地址,method 表示发送表单所使用的 http 方法(get / post),enctype表示数据的编码方式,对于文件上传,必须为 _multipart/form-data_。

具体的定义参见 form

下面是对应的页面,可以看到,有一个提示选择文件的按钮

file-upload

点击按钮,就可以选择文件啦。

choose-file

  • 小贴士:文件选择好之后,可以通过 FileReader 进行预览,或者简单的编辑。

如何上传

简单的上传,只需要提交对应的 form 就可以了。是不是很简单,O(∩_∩)O哈哈哈~。

增强实现

上面介绍的都太简单粗暴肤浅了,实际项目中老板,客户100%会投反对票。因为实在是太简陋了。

美化选择按钮

浏览器提供的原生控件实在是丑的不忍心看,可以自己画一个好看的按钮。

.chooseFile{
    min-width: 30px;
    min-height: 15px;
    width: 106px;
    height: 29px;
    background-color: #B6E2C9;
    color: black;
    font-family: monospace;
    font-weight: 400;
    border-color: white;
    border-radius: 17px;
    padding: 5px;
    text-align: center;
    vertical-align: middle;
    cursor: pointer;
}

记得把原来的form隐藏掉。

接下来你需要做的是给这个按钮绑定 click listener ,当它被点击时,触发 form 中的 file 的 click 事件。

custom-choose

不想刷新页面

有些时候,希望上传时不刷新当前页面。但是使用 form 是避免不了页面刷新的。怎么办?

第一个想出这个办法的肯定是个头脑灵活的家伙--使用隐藏的 iframe 上传。

原理是,在当前页面(父页面)中添加 iframe,iframe 的页面(子页面)中包含 form 和相关的函数(验证,预处理等等)。当用户在父页面点击选择文件的按钮时,去触发子页面中 file 控件的 click 事件。

当用户提交时,提交子页面中的 form。这时,子页面跳转,而父页面没有刷新。

这个方案有个缺点,就是需要前后端协同工作。

当需要使用回调函数来处理上传完成后后端返回的数据时,需要和后端预先达成约定,如,回调函数名,参数列表,等等。这对前后端完全分离的开发场景(比如,你只是开发前端UI)是一个挑战。(出现全栈工程师的原因,是不是就是因为前端工程师想把这些依赖但是却又无法完全控制的工作给**_抢**_过来?)

比如,父页面须定义回调函数

function uploadSuccess (result){
...
}

后端须对action(上面form中定义的/upload)返回html,html包含对回调函数的调用,以及制定参数。

<html>
...
<script>
window.uploadSuccess('xxxxxx');
</sript>
...
</html>

当然,如果你是**全栈工程师**,这都不算事。自己一个人搞,还约定个啥。

FormData,ajax文件上传

你说文件上传这么常见的功能,咋就不用直接用 js 搞定呢? 非要牵扯什么 form,什么 iframe,烦?

客官,看来你需要的是 FormData

FormData 允许通过 js 构造 form ,然后通过 ajax 方式上传。为了方便,这里使用 jquery 的 ajax。

var data = new FormData();
data.append('file', fileObj);

$.ajax({
    url: '/upload',
    type: 'POST',
    data: data,
    cache: false,
    dataType: 'json',
    processData: false, // Don't process the files
    contentType: false, // Set content type to false as jQuery will tell the server its a query string request
    success: function(data, textStatus, jqXHR) {
        console.log(JSON.stringify(data, null, 4));
    },
    error: function(jqXHR, textStatus, errorThrown) {
          //jqXHR may have no responseJSON in old jquery
        console.log(JSON.stringify(jqXHR.responseJSON, null, 4));
    }
});

需要注意的是,processData 必须指定为false,否则,jquery 会尝试格式化formData,这会引起一些错误。

一些低版本的浏览器可能对 FormData 没有提供支持,所以实际项目中要谨慎使用哦。

文件验证

有时候,我们需要对文件进行譬如大小,类型(通过扩展名),名称的验证,只有符合预期的才允许上传。

前端

前端获取这三个属性非常简单。

var file = uploadForm.anyname.files[0];
console.log(file.name);
console.log(file.size);
console.log(file.type);

更详细的介绍 file api

后端

相对前端来说,由于涉及到 http 报文的细节,所以稍微复杂一点(意思就是说,我讲的很有可能是片面的,错误的)。

http 报文,也就是你从浏览器的 network 调试窗口看到的 request 信息,它主要包括 header 和 body 两部分。header 中包含 content-length,也就是发送数据的长度,一般可以依次作为对文件大小的判断。如果后端检测到它大于预设的最大限制,则返回错误给前端。

http 的 body 部分会为上传文件的数据的开始和结尾插入边界,例如,chrome

------WebKitFormBoundarycKtZKQMmA6QfpeMW
Content-Disposition: form-data; name="file"; filename="bt.jpg"
Content-Type: image/jpeg


------WebKitFormBoundarycKtZKQMmA6QfpeMW--

并且,在文件内容之前,是文件的元数据,例如名词,类型,还有大小。

后端可以根据边界的检验,识别上传的文件,读取元数据中的文件属性,从而为验证提供数据。

有很多文件上传框架会将文件写入临时文件夹后,再做验证。其实是非常没有必要的。完全可以在 http 数据开头的一部分(数据并不是一起传送,而是类似于流的方式)抵达服务器时就完成验证,从而尽早的返回错误,避免不必要的数据操作(所谓优化--能不做,尽量不做。)。

为什么叫二三事

也许叫xxx大全会好一点,不过本人孤傲的不愿意拾人牙慧,只要叫做 二三事 了。所谓 _二三_,其实是一堆事。有叙述,有感叹,有建议。当然,也有吐槽。

后记

补充

  • 除了 file 表单,file对象还可以从拖拽事件中获取。
e.dataTransfer.files
  • http body中,上传文件的边界可以由程序指定
var boundary = 'fdfrefdrerefdfd';
xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary="+boundary); // simulate a file MIME POST request.  
xhr.setRequestHeader("Content-Length", fileSize);  

var body = '';  
body += "--" + boundary + "\r\n";  
body += "Content-Disposition: form-data; name=\""+dropbox.getAttribute('name')+"\"; filename=\"" + fileName + "\"\r\n";  
body += "Content-Type: "+fileType+"\r\n\r\n";  
body += fileData + "\r\n";  
body += "--" + boundary + "--\r\n";  

xhr.sendAsBinary(body);