一、引入
随着web应用的发展,越来越多的web应用变得丰富起来,文件操作成为了软件的刚需。如何使用浏览器来进行多种方式的文件上传,并通过nodejs来处理上传的文件呢?下面我们就文件上传展开分析。
二、选择文件
首先要想上传一个文件,最起码得让浏览器知道,上传的文件究竟是哪一个。
两种选择文件的方式
1.input文件选择
在浏览器的input标签中当type属性为file时,当点击触发了这个标签后,浏览器会打开系统的文件选择器,这时就可以进行文件选择。
<input type="file" id="file" multiple="multiple">
file = document.getElementById('file');
file.onclick = function(e){
e.target.value=""
}
file.onchange = function(e){
console.log(e.target.files)
console.log(e.target.value)
}
- 一般通过设置input标签的change事件来监控文件变化,只要input所选文件变化之后,就会重新触发该事件。
- 在事件回调中,
e.target
记录了触发的元素,即input标签。标签的value属性记录了所选文件的具体位置,标签的files属相记录了文件的名称大小类型等信息。 - 一般使用文件选择的时候,默认只允许选择一个文件,如果涉及到了多个文件,可以在input的html中添加multiple=“multiple”。表示允许选择器可以进行多个文件的选择。
- 需要注意的是:在选择多个文件时,files数组中存储的内容,为多个文件的数组。但是value属性只限制按照字符规则排序的第一个文件。
- 在文件选择的时候只能选择同一路径下的文件,不能跨文件夹选择文件。
- 还存在一些问题,当我们选择同一个文件两次的时候,会因为文件没有被改变,导致change事件根本没有被触发。或是点击取消按钮,value值置空了,事件异常触发了。
- 这时我们可以绑定点击事件,当文件点击的时候清除它的value属性。
- 之后当打开文件选择器之后,不论选择什么文件之前有没有选择过都会触发事件。
- 之后点击取消文按钮,由于value之前就被置空了,也不会触发change更新。
2.js文件拖拽选择
在新版w3c规范中,浏览器支持了原生的拖拽事件,这样我们就可以使用拖拽功能来选择文件。
要想使用拖拽功能,我们首先得了解一下,有哪些拖拽事件:
- 被拖对象:dragstart事件,被拖动的元素,开始拖放触发
- 被拖对象:drag事件,被拖放的元素,拖放过程中
- 被拖对象:dragend事件,拖放的对象元素,拖放操作结束
- 经过对象:dragenter事件,拖放过程中鼠标经过的元素,被拖放的元素“开始”进入其它元素范围内(刚进入)
- 经过对象:dragover事件,拖放过程中鼠标经过的元素,被拖放的元素正在本元素范围内移动(一直)
- 经过对象:dragleave事件,拖放过程中鼠标经过的元素,被拖放的元素离开本元素范围
- 目标地点:drop事件,拖放的目标元素,其他元素被拖放到本元素中
var oBox = document.getElementById('box');
//需要放入的盒子
oBox.ondragenter = function() {
oBox.innerHTML = '请释放鼠标';
};
oBox.ondragleave = function() {
oBox.innerHTML = '请将文件拖拽到此区域';
};
oBox.ondrop = function(ev) {
console.log(ev.dataTransfer.files);
};
- 注意:当ondrop事件触发的时候,
ev.dataTransfer.files
中会包含选择的文件,但是由于事件回调参数为引用类型,只有在回调函数中才可以拿到相关的文件信息,之后通过引用指向访问到的文件列表会为空。 - 如果没有将文件拖拽到指定的位置,这时会触发浏览器的默认事件,导致浏览器打开新的页面,直接执行程序,而不是上传文件。这时需要同时给document添加ondragover,ondrop事件,并禁止掉他们的默认事件。这样就可以保证浏览器,不会打开新的页面。
三、文件上传
经过了之前的文件选择,确定了上传的目标之后,就可以进行文件的具体上传了。
文件提交
ajax文件的上传需要使用FormData这个浏览器原生对象,通过对象的原型方法append,将文件内容转化为一个模拟表单。之后通过ajax的post方法向后台进行提交。
var form = new FormData()
form.append('files', oFile);//这里的oFile就是上一章节选择文件数组的具体一项
console.log(form)//打印看不出来有文件被添加了
url = 'http://localhost:1949/upload', //服务器上传地址
var xhr = new XMLHttpRequest();
xhr.open("post", url, true);
xhr.send(form); //开始上传
注意:生成的form的实例,控制台打印看不出来有文件被添加了,看上去就是一个空对象。这是因为绑定的文件是以二进制来书写的,控制台无法进行数据解析,所以表面看上起就是一个空数组。
post抓包分析
这里需要注意的是,在使用post请求发送之前,首先会进行一次options连接,当确定连接可以建立,才会发起真正的post请求。下面是Fiddler抓取的本地测试的post数据包。
POST http://localhost:1949/upload HTTP/1.1
Host: localhost:1949
Connection: keep-alive
Content-Length: 432100
Pragma: no-cache
Cache-Control: no-cache
Sec-Fetch-Mode: cors
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4X2oHxnVyC3gbFyk
Accept: */*
Sec-Fetch-Site: cross-site
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
------WebKitFormBoundary4X2oHxnVyC3gbFyk
Content-Disposition: form-data; name="file"; filename="Global方法.png"
Content-Type: image/png
具体文件数据信息...
*** FIDDLER: RawDisplay truncated at 262144 characters. Right-click to disable truncation. ***
-
其中
Content-Type:
部分的内容 multipart/form-data; boundary=----WebKitFormBoundary4X2oHxnVyC3gbFyk这里的:multipart/form-data是指表单数据有多部分构成,既有文本数据,又有文件等二进制数据的意思。boundary字符串来分割请求头与请求体的
-
二进制数据的最后面有字符的长度信息
-
当遇到多个文件的上传问题时boundary会分为若干部分,如下所示。
Content-Type: multipart/form-data; boundary=${boundary}
--${boundary}
具体文件数据信息...
--${boundary}--
--${boundary}
具体文件数据信息...
--${boundary}--
监控进度
可以使用xhr.upload添加progress
事件,在回调的参数中,result.loaded / result.total分别表示已上传大小和全部大小。可以通过设计定时器,计算上传速率,可视化数据上传进度。
xhr.upload.addEventListener("progress", function(result) {
console.log(result)
console.log(result.loaded / result.total)
}, false);
四、后台处理
文件发送到了后台,紧接着就要进行文件的接收了。这里我们使用express和multer第三方模块处理网络请求,和解析文件。
const multer = require('multer')
var objMulter = multer({dest: './www/upload/'});//设置文件所在位置
server.use(objMulter.any());//使用中间件
//中间件处理的req.files表示解析的文件
经过中间件处理,文件会接收为二进制存储在指定的目录,文件会生成一个不会重复的散列值作为文件名,防止多文件上传中出现文件的冲突。但是我们为了保证我们文件具有可访问性,需要使用fs模块,对文件名称的后缀进行修改。可以使用 path.parse功能分析req.files的文件格式。
server.post('/upload', function(req, res) {
console.log(pathLib.parse(req.files[0].originalname))
var newName = req.files[0].path + pathLib.parse(req.files[0].originalname).ext;
// 使用fs模块的rename重命名方法重名字保存的文件,才能正常使用
//rename('旧文件名,新文件, 回调 ')
fs.rename(req.files[0].path, newName, function(err) {
if (err) {
res.send('上传失败')
} else {
res.send('上传成功')
}
res.end();
})
})
五、文件的再访问
我们将文件上传到了服务器后,可能也需要进行文件的再次访问。可以将文件的服务器路径在文件上传成功后,返回给前端。同时在服务器配置nginx或其他文件资源服务器,保证对应路径内容可以被访问到。