首先,"multipart/form-data" 編碼格式的數據是不能用 querystring 或者 body-parser 來解析的,需要借助 formidable 或者其他的模塊來解析。再學習這些第三方模塊時我就在想,它們是怎么實現解析數據的呢?
如果是 "application/x-www-form-urlencoded" 格式的數據很好解決,請求體中的數據都是以 ”&“ 作為連接,特殊字符轉換為ASCII HEX
值,比如參數值中的 "&" 編碼成 "%26" ,空格用 ”+“ 表示,但是 "multipart/form-data" 數據就有些麻煩了,因為它的請求體是這樣的:
這里我將請求體 Buffer 類型的數據轉換成了 utf-8 格式的字符串,數據以 boundary 作為分隔,文件以二進制數據傳輸,為了解析這段數據,聲明函數 "function formParse( )" ,函數需要傳入兩個參數,請求體中的 data,和請求頭中的 boundary 。代碼如下:
module.exports = function formParse(body, boundary) {
//將Buffer類型的數據轉化成binary編碼格式的字符串
let formStr = Buffer.concat(body).toString('binary');
let formarr = formStr.split(boundary);
//去掉首尾兩端的無用字符
formarr.shift();
formarr.pop();
//存儲普通key-value
let filed = {};
//存儲文件信息
let file = {};
for (let item of formarr) {
//去除首尾兩端的非信息字符
item = item.slice(0, -2).trim();
//value存儲input輸入的值
let value = '';
//不同操作系統換行符不同,用變量a聲明特殊分割點位的下標
let a;
if ((a = item.indexOf('\r\n\r\n')) != -1) {
value = item.slice(a + 4);
} else if ((a = item.indexOf('\r\r')) != -1) {
value = item.slice(a + 2);
} else if ((a = item.indexOf('\n\n')) != -1) {
value = item.slice(a + 2);
}
//正則匹配,組中內容
let key = item.match(/name="([^"]+)"/)[1];
if (item.indexOf('filename') == -1) {
if (!(key in filed)) {
//將二進制字符串轉化成utf8格式的字符串
filed[key] = Buffer.from(value,'binary').toString('utf8');
} else {
//將復選框的數據放入一個數組中
let arr = [];
filed[key] = arr.concat(filed[key], value);
}
} else {
let filename_b = item.match(/filename="([^"]*)"/)[1];
//解決中文文件名亂碼的問題
let filename = Buffer.from(filename_b,'binary').toString();
let contentType = item.slice(item.indexOf('Content-Type:'), a);
let obj = {};
obj.filename = filename;
obj.contentType = contentType;
obj.binaryStream = value;//文件的二進制數據
let arr = [];
if (!(key in file)) {
arr.push(obj);
file[key] = arr;
} else {
//用於多文件上傳
file[key] = arr.concat(file[key], obj);
}
}
}
return { filed, file };
}
然后,再使用這個函數,
const http = require('http');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const formParse = require('./formParse');
const server = http.createServer();
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
server.on('request', async (req, res) => {
if (req.url == '/ajax') {
if (req.method == 'GET') {
let pathName = path.join(__dirname, 'ajax.html');
let data = await readFile(pathName, 'utf8');
res.end(data);
}
if (req.method == 'POST') {
// req.setEncoding('binary');
let body = [];
let boundary = req.headers['content-type'].split('boundary=')[1];
//console.log(boundary);
req.on('data', (chunk) => {
body.push(chunk);
}).on('end', async () => {
let { filed, file } = formParse(body, boundary);
console.log(filed);
console.log(file);
try {
//文件輸入框的name="files"
let fileArr = file.files;
for(let f of fileArr){
await writeFile(f.filename, f.binaryStream, 'binary');
console.log(`文件\"${f.filename}\"寫入成功`);
}
} catch (error) {
console.log(error);
res.statusCode = 500;
res.end();
}
res.end('請求成功');
})
}
} else {
res.end('not found');
}
});
server.listen(8080);
console.log('服務器啟動成功');
關於函數 formParse 的幾點說明:
- formParse 將非文件的參數存入再 "field" 對象中,文件參數存在 "file" 對象中,文件以二進制字符串的形式存在,這樣的話,文件存在哪,以什么名字或者格式存儲,你可以自己設置。在命令行窗口打印 filed 和 file 對象,格式如下:
{ username: '張三', password: '123', book: [ 'book1', 'book2' ] }
{
files: [
{
filename: '測試文檔.txt',
contentType: 'Content-Type: text/plain',
binaryStream: 'test text --En\r\n䏿å符串 --zh-cn'
}
]
}
-
文件在寫入時,需要設置編碼格式為 "binary" ,因為 binaryStream 中存的是二進制的字符串。
-
在函數中,我是先這樣處理 Buffer 類型的數據的:
let formStr = Buffer.concat(body).toString('binary');
這樣的話,會造成中文的字符串亂碼,所以文件名,參數值在后面都要轉換成 utf8 格式,當然,這個在函數內部實現了。
-
如果先將 Buffer 類型的數據轉換成 utf8 格式的字符串,再將文件轉換回 binary 編碼,純文本文件沒什么問題,但是圖片文件就會讀不出來,具體原因不知道,可能這種 轉換不可逆吧
-
我在一些博客上看見這樣的處理方式:在讀取請求體數據之前,設置
req.setEncoding('binary');
因為我對 node 還不是很了解,到 node.js 官網去查了,了解到這里的 req 是回調函數中的參數,是一個 IncomingMessage 對象,它繼承了 stream.Readable 類,所以設置讀取時的數據編碼為 binary。
-
還有一個小細節,分隔數據的字符串是 "--"+boundary,結尾也會有兩個”-“:
關於 formParse 的代碼 https://github.com/Arduka/ajax-formParser