解析 form-data 數據,實現 formidable 函數的功能


首先,"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 的幾點說明:

  1. 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'
    }
  ]
}
  1. 文件在寫入時,需要設置編碼格式為 "binary" ,因為 binaryStream 中存的是二進制的字符串。

  2. 在函數中,我是先這樣處理 Buffer 類型的數據的:

    let formStr = Buffer.concat(body).toString('binary');
    

    這樣的話,會造成中文的字符串亂碼,所以文件名,參數值在后面都要轉換成 utf8 格式,當然,這個在函數內部實現了。

  3. 如果先將 Buffer 類型的數據轉換成 utf8 格式的字符串,再將文件轉換回 binary 編碼,純文本文件沒什么問題,但是圖片文件就會讀不出來,具體原因不知道,可能這種 轉換不可逆吧

  4. 我在一些博客上看見這樣的處理方式:在讀取請求體數據之前,設置

    req.setEncoding('binary');
    

    因為我對 node 還不是很了解,到 node.js 官網去查了,了解到這里的 req 是回調函數中的參數,是一個 IncomingMessage 對象,它繼承了 stream.Readable 類,所以設置讀取時的數據編碼為 binary。

  5. 還有一個小細節,分隔數據的字符串是 "--"+boundary,結尾也會有兩個”-“:

關於 formParse 的代碼 https://github.com/Arduka/ajax-formParser


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM