參考資料
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 passtruecd(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()); // 注意應轉換成字符串
})
}
})
