参考资料
Node.js 文件系统 | 菜鸟教程
HTML DOM FileUpload 对象 | W3school
HTML <form> 标签 | W3school
HTML <input> 标签 | W3school
HTML <input> 标签的 accept 属性
multer - npm
multer模块的使用 +文件上传+ 评论 | 维克多噗噗的博客
1. fs 模块
该模块主要执行文件操作,操作的方法均有同步和异步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。其余参数根据不同的方法有所差异。
比起同步,异步方法性能更高,速度更快,而且没有阻塞。在此仅记录我在 express 上传文件操作时所用到的readFile
方法、writeFile
方法、stat
方法和unlink
方法,对其余方法仅作简单描述,详细使用方法和实例参照Node.js文件系统 | 菜鸟教程。
1.1 读取文件fs.readFile
fs.readFile(filename,[,options], callback(err, data));
回调函数的参数:
err
- 错误信息;data
- buffer数据流对象,可用data.toString()
转换成字符串;
var fs = require('fs');
// 异步读取
fs.readFile('./input.txt', function (err, data) {
if (err) {
return console.error(err);
}
console.log("异步读取:" + data.toString());
});
1.2 写入文件fs.writeFile
fs.writeFile(file, data[, options], callback(err))
writeFile 直接打开文件默认是w
模式,所以如果文件存在,该方法写入的内容会覆盖旧的文件内容。
参数使用说明如下:
file
- 文件名或文件描述符。data
- 要写入文件的数据,可以是String(字符串)或Buffer(流)对象。options
- 该参数是一个对象,内容如下:- encoding - 编码,默认值为
utf8
; - mode - 模式(权限),默认值为
0666
(可读、可写); - flag - 文件打开行为,默认值为
'w'
;
- encoding - 编码,默认值为
callback
- 回调函数,回调函数只包含错误信息参数(err),在写入失败时返回。
常见的打开文件的模式(mode)有以下几种:
Flag | 描述 |
---|---|
r | 以读取模式打开文件。如果文件不存在抛出异常。 |
r+ | 以读写模式打开文件。如果文件不存在抛出异常。 |
w | 以写入模式打开文件,如果文件不存在则创建。 |
w+ | 以读写模式打开文件,如果文件不存在则创建。 |
1.3 获取文件信息fs.stat
fs.stat(path, callback(err, stats))
文件的状态信息包含在回调函数的参数stats中,这是一个fs.Stats对象,其内容如下:
atime: Wed Jul 25 2018 21:11:59 GMT+0800 (GMT+08:00) {}
atimeMs: 1532524319921.0476
birthtime: Wed Jul 25 2018 21:11:59 GMT+0800 (GMT+08:00) {}
birthtimeMs: 1532524319921.0476
blksize: undefined
blocks: undefined
ctime: Wed Jul 25 2018 21:11:59 GMT+0800 (GMT+08:00) {}
ctimeMs: 1532524319922.0476
dev: 6533005
gid: 0
ino: 1407374884234476
mode: 33206
mtime: Wed Jul 25 2018 21:11:59 GMT+0800 (GMT+08:00) {}
其中有四个时间值得我们关注:
- atime - 访问时间(access time);
- birthtime - 创建时间;
- ctime - 状态修改时间(change time),显示的是文件的权限、拥有者、所属的组、链接数发生改变时的时间;
- mtime - 修改时间(modify time),显示的是文件内容被修改的最后时间。
每一个时间都是一个JavaScript Date()对象的实例,因此有些方法是可以通用的,例如获取日期、月份、年份:
stats.birthtime.getDate()
25
stats.birthtime.getMonth() // js的月份从0开始算
6
stats.birthtime.getFullYear()
2018
1.4 删除文件fs.unlink
fs.unlink(path, callback(err))
直接删除path
对应的文件,若文件不存在会通过err
报错。
其他方法
方法 | 作用 |
---|---|
fs.open(path, flags[, mode], callback(err, fd)) | 打开文件 |
fs.read(fd, buffer, offset, length, position, callback) | 读取文件 |
fs.close(fd, callback) | 关闭文件 |
fs.ftruncate(fd, len, callback) | 截取文件 |
fs.mkdir(path[, mode], callback) | 创建目录 |
fs.rmdir(path, callback) | 删除目录 |
1.5 读取目录fs.readdir
fs.readdir(path, callback(err, files))
参数说明:
path
- 文件目录名(若目录不存在或者不是目录,则会将错误信息写入err);files
- 目录下的文件数组列表,其包含如下字段:- length - 数组大小,记录了该目录下文件和子文件夹的总数;
- [0, 1, 2, 3, ...] - files[i]依次记录了文件名(以及子文件夹名);
例如我们使用如下方法输出当前目录下的所有文件及子文件夹名:
fs.readdir('./', function (err, files) { // 获取目录下所有文件名
if (err) {
return console.error(err);
}
for (var i=0; i<files.length; i++) {
console.log(files[i].toString());
}
})
得到的结果(按照文件名排序了):
file.js
input.txt
node_modules
package-lock.json
package.json
reference.css
table.html
我们还可以配合fs.unlink方法来清空一个文件夹:4.4 缓存管理
2. 关于 HTTP 文件传输和 multer 控制文件上传的几个问题(写在前面)
2.1 文件选择后(未提交前)放在哪里?
哪都没放,还在原先的磁盘上,只是根据选择文件信息填充了HTML DOM FileUpload的属性。
2.2 文件提交后的路径是什么?
由服务器设定,在express中由multer({dest: ''})
指定。
2.3 文件传输在HTTP协议中是如何进行的?
将文件编码后存储在请求体中,且一旦发送请求(包含请求体和请求头),就向服务器指定接收文件的位置发送一个编码文件(存放在multer({dest: ''})
指定的路径中);
服务器可以根据请求头的信息,对编码文件进行操作(解析、读取等);
若直接修改编码文件的后缀名,可以直接获得原始文件,例如我发送一个png图片,在服务器收到了一个名称为7d5931b2f95ce2cb93e647c6d64f5326
的文件,将其后缀名修改为.png,打开,完美还原。
2.4 multer([options])中有哪些键?分别有什么用??
dest
:指定接收编码文件的路径;(用的最多)fileFilter
:控制接收的文件类型;(偶尔用用,在控制文件类型时用到)limits
:Limits of the uploaded data;(基本没用)preservePath
:Keep the full path of files instead of just the base name;(基本没用)
2.5 multer.array()有什么用??
array()的作用是规定接受的一系列文件共有的字段名(类似于将文件分类)。其可以使用app.use()命名为一个全局中间件,但这并不理想,因为在一个脚本文件中可能需要响应不同类型的文件上传,有图片、文档、XML、JSON等。
所以更理想的方式是在全局先创建一个multer实例:
var upload = multer({ dest: './tmp/'});
然后在对每个不同的POST请求响应中,将upload.array('')
作为第二个参数写入:
// 响应请求
app.post('/image_upload', upload.array('image'), function (req, res) {
// code...
})
一般来说,一个app.post()只能响应一个表单元素的提交,因此对提交的不同类型的表单元素数据设置不同的字段名(fieldname),是最理想的选择。
2.6 使用不同浏览器传输文件会有什么不同效果??
没有不同效果,都可以成功传输文件,并且都能将编码文件存储到指定文件夹中。只是请求头的user-agent
信息会有不同。
3. HTML 用于上传文件的元素
3.1 HTML < form > 标签的 enctype 属性
enctype 属性规定在发送到服务器之前应该如何对表单数据进行编码;
默认地,表单数据会编码为 application/x-www-form-urlencoded
。就是说,在发送到服务器之前,所有字符都会进行编码(空格转换为 "+" 加号,特殊符号转换为 ASCII HEX 值);
当我们使用文件上传功能时,enctype的值必须设置为multipart/form-data
。
语法
<form enctype="value">
属性值
值 | 描述 |
---|---|
application/x-www-form-urlencoded | 在发送前编码所有字符(默认) |
multipart/form-data |
不对字符编码。 在使用包含文件上传控件的表单时,必须使用该值。 |
text/plain | 空格转换为 "+" 加号,但不对特殊字符编码。 |
3.2 HTML DOM FileUpload 对象
在 HTML 文档中 标签每出现一次,一个 FileUpload 对象就会被创建。我们可以通过使用document.getElementById()
来访问 FileUpload 对象:
.html:
<form id="uplodaFile" action="/file_upload" method="POST" enctype="multipart/form-data">
<input type="file" name="image" size="50"><br>
<input type="submit" value="上传图片">
</form>
.js(获取 FileUpload 对象):
var element = document.getElementById("uplodaFile");
FileUpload 对象的属性:
属性 | 描述 |
---|---|
accept |
设置或返回指示文件传输的 MIME 类型的列表(逗号分隔)。 |
accessKey | 设置或返回访问 FileUpload 对象的快捷键。 |
alt | 设置或返回不支持 <input type="file"> 时显示的替代文字。 |
defaultValue | 设置或返回 FileUpload 对象的初始值。 |
disabled | 设置或返回是否禁用 FileUpload 对象。 |
form | 返回对包含 FileUpload 对象的表单的引用。 |
id | 设置或返回 FileUpload 对象的 id。 |
name | 设置或返回 FileUpload 对象的名称。 |
tabIndex | 设置或返回定义 FileUpload 对象的 tab 键控制次序的索引号。 |
type | 返回表单元素的类型。对于 FileUpload ,则是 "file" 。 |
value | 返回由用户输入设置的文本后,FileUpload 对象的文件名。 |
4. express 文件上传
4.1 文件上传的实例
upload.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Page</title>
</head>
<body>
<h2>UPLOAD IMAGE FILE</h2><br>
<form id="uploadImg" action="/image_upload" method="POST" enctype="multipart/form-data" >
<input type="file" name="image" accept="image/*"><br>
<input type="submit" value="上传图片">
</form>
<script type="text/javascript">
var element = document.getElementById("uploadImg");
</script>
</body>
</html>
upload.js:
/**
* 上传图片文件测试脚本
*/
// 依赖
var express = require('express');
var app = express();
var fs = require('fs');
var bodyParser = require('body-parser');
var multer = require('multer');
// 中间件
app.use(express.static('./uploads/'));
app.use(bodyParser.urlencoded({ extended: false }));
// 控制允许接收的文件类型(4.3.3)
function fileFilter (req, file, cd) {
if (file.mimetype == "image/png" || file.mimetype == "image/jpeg"){
cd(null, true);
}else{
req.error = "不允许上传" + file.mimetype + "类型的文件!";
cd(null, false);
}
}
// 设置缓存路径和文件过滤器(4.3)
var upload = multer({ dest: './uploadFiles/tmp/', fileFilter: fileFilter});
// 首页
app.get('/', function (req, res) {
res.sendFile(__dirname + "/" + "upload.html");
})
// 响应请求
app.post('/image_upload', upload.array('image'), function (req, res) {
// 文件信息
if(!req.files[0]){
console.log(req.error);
res.send(req.error);
return;
}else{
console.log(req.files[0]);
}
// 存储并响应客户端
var des_file = __dirname + "/uploadFiles/" + req.files[0].fieldname + "/" + req.files[0].originalname;
fs.readFile(req.files[0].path, function (err, data) {
fs.writeFile(des_file, data, function (err) {
if(err){
console.log(err);
}else{
var response = {
message: 'File uploaded successfully',
filename: req.files[0].originalname
};
console.log(response);
res.json(response);
}
});
});
})
// 监听
var server = app.listen(3333, function () {
var host = server.address().address;
var port = server.address().port;
console.log("应用实例,访问地址为:http://%s:%s", host, port);
})
4.2 multer模块是什么?
Multer是一个专门用于处理multipart/form-data
编码类型数据流的node.js中间件,在进行文件上传操作时常用到。
需要注意的是,当表单元素的编码类型不是multipart/form-data
时,Multer不会对请求进行解析。
我们一般通过如下方法使用multer模块:
var multer = require('multer');
var upload = multer({ dest: './tmp/'});
// 响应请求
app.post('/image_upload', upload.array('image'), function (req, res) {
// code...
})
4.3 multer如何控制文件传输?
4.3.1 控制编码文件的位置
multer({ dest: './uploadFiles/tmp/' });
4.3.2 给不同的文件响应规定字段名
multer.array('image');
multer.array('myType');
4.3.3 控制接收文件的类型
IMME文件类型:Content-Type
前端控制
为表单元素<input type="file">
设置属性accept
,限定文件选择对话框中允许选择的文件类型(多种类型用逗号分隔):
<input type="file" name="image" accept="image/png, application/pdf"><br>
服务端控制
在服务端控制接收文件的类型,主要依靠multer([options])
中的fileFilter
键(multer的键值)。fileFilter
键的使用方法是:创建一个函数fileFilter(req, file, cd){}
,来对请求进行解析,进而通过参数cd
决定是否接收发送的文件。
错误的使用:
// 不能直接规定fileFilter的键值
var upload = multer({ dest: '.upload', fileFilter: 'image/png, image/jpeg'});
正确的使用:
// 控制允许接收的文件类型
function fileFilter (req, file, cd) {
if (file.mimetype == "image/png" || file.mimetype == "image/jpeg"){
cd(null, true); // 同意接收文件
}else{
req.error = "不允许上传" + file.mimetype + "类型的文件!";
cd(null, false); // 拒绝接收文件
}
}
var upload = multer({ dest: './uploadFiles/tmp/', fileFilter: fileFilter});
file
包含以下字段encoding
:"7bit";fieldname
:"image";(字段名:由upload.array('image')
定义的)mimetype
:"image/jpeg";originalname
:"540ff7cddc29e.jpg";
cd
的用法cd(null, true)
- To accept the file passtrue
cd(null, false)
- To reject this file passfalse
4.3 设置本地存储路径(通过 req 对象的属性)
在文件目录下创建uploadFiles文件夹,同时根据upload.array()中规定的字段名创建文件夹(一定不能创建出错,不然会提示无法打开相应的文件夹);
例如upload.array('image')
,创建uploadFiles/image,使用下面方法可以将文件存入到image文件夹中:
var des_file = __dirname + "/uploadFiles/" + req.files[0].fieldname + "/" + req.files[0].originalname;
fs.readFile(req.files[0].path, function (err, data) {
fs.writeFile(des_file, data, function (err) {
//callback...
});
});
4.4 缓存管理
在接收文件时,服务器将受到大量的编码文件,当完成文件接收后,这些编码的文件仍然存放在服务器主机磁盘上。这些文件的存在有利于数据的恢复,但当其数量达到一定规模时,会对磁盘空间造成较大的压力,因此,应该采取合适的手段进行编码文件的数量控制,来保证磁盘空间的可用性。
// 删除传输文件时的临时文件
var fs = require('fs');
var desDir = "D:/nodejs/my-sql/uploadFiles/tmp/";
// 先获取该文件夹下所有文件名
fs.readdir(desDir, function (err, files) {
if (err) {
return console.error(err);
}
for (var i=0; i<files.length; i++) {
// 使用 unlink 删除
fs.unlink(desDir + files[i], function (err){
if (err) {
return console.error(err);
}
console.log("Successfully delete file " + files[i].toString()); // 注意应转换成字符串
})
}
})