Nodejs学习笔记,4 文件操作 fs 及 express 上传


目录


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'
  • callback - 回调函数,回调函数只包含错误信息参数(err),在写入失败时返回。

 常见的打开文件的模式(mode)有以下几种:

Flag描述
r以读取模式打开文件。如果文件不存在抛出异常。
r+以读写模式打开文件。如果文件不存在抛出异常。
w以写入模式打开文件,如果文件不存在则创建。
w+以读写模式打开文件,如果文件不存在则创建。

获取文件信息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 属性

HTML <form> 标签 | W3school

HTML <input> 标签 | W3school

 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 DOM FileUpload 对象 | W3school

 在 HTML 文档中 标签每出现一次,一个 FileUpload 对象就会被创建。我们可以通过使用document.getElementById()来访问 FileUpload 对象:

.html:

<form >
    <input type="file" name="image" size="50"><br>
    <input type="submit" value="上传图片">
</form>

.js(获取 FileUpload 对象):

var element = document.getElementById("uplodaFile");

FileUpload 对象的属性:

属性描述
accept

设置或返回指示文件传输的 MIME 类型的列表(逗号分隔)。

Content-Type

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 >
<head>
    <meta charset="UTF-8">
    <title>Upload Page</title>
</head>
<body>
    <h2>UPLOAD IMAGE FILE</h2><br>
    <form  >
        <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 - npm

 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如何控制文件传输?

multer - npm


4.3.1 控制编码文件的位置

multer({ dest: './uploadFiles/tmp/' });

4.3.2 给不同的文件响应规定字段名

multer.array('image');
multer.array('myType');

4.3.3 控制接收文件的类型

IMME文件类型:Content-Type


前端控制

HTML <input> 标签的 accept 属性

 为表单元素<input type="file">设置属性accept,限定文件选择对话框中允许选择的文件类型(多种类型用逗号分隔):

<input type="file" name="image" accept="image/png, application/pdf"><br>

服务端控制

multer模块的使用 +文件上传+ 评论 | 维克多噗噗的博客

multer(fileFilter) - npm

 在服务端控制接收文件的类型,主要依靠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 pass true
    • cd(null, false) - To reject this file pass false

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()); // 注意应转换成字符串
        })
    }
})