NodeJS - 如何在没有框架的情况下解析多部分表单数据?

14
我是一名有帮助的助手,可以为您翻译文本。

我正在尝试做一件基本的事情:使用FormData API发送表单并在NodeJS中解析它。

在搜索了一个小时的SO后,我只找到了使用ExpressJS和其他框架的答案,因此我认为这值得提出自己的问题:

我有这个HTML:

<form action="http://foobar/message" method="POST">
  <label for="message">Message to send:</label>
  <input type="text" id="message" name="message">
  <button>Send message</button>
</form>

JS:

var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://foobar/message');
xhr.send(new FormData(form));

在NodeJS中,我正在执行以下操作:
var qs = require('querystring');

var requestBody = '';
request.on('data', function (chunk) {
  requestBody += chunk;
});
request.on('end', function () {
  var data = qs.parse(requestBody);
  console.log(data.message);
});

但是在data.message中,我收到了Webkit Boundary的东西(来自多部分表单数据格式),而不是预期的消息。 是否有另一个内置库用于解析多部分POST数据,而不是querystring?如果没有,那么如何手动执行(高级别,而不需要阅读Express的源代码)?

1
@SLaks,我特别询问了内置/手动方法,不使用第三方工具。 - krulik
2
没有简单的内置方法来完成这个任务,你可以自己编写,但是没有必要,解析数据、获取文件等可能会很复杂,而简单的 bodyParser 支持 iconv,可以解析原始数据、urlencoded、json、二进制等等,当你写完类似的代码时,你已经浪费了几天甚至几周的时间,而且它仍然不如你可以在五秒钟内安装的 bodyParser 好。 - adeneo
1
顺便提一下,bodyParser 的文档中说:“它不处理多部分体”,因此即使在这种简单情况下也没有帮助。 - krulik
2
那么你将需要阅读规范,并根据规则创建自己的解析器等。不同的部分由边界分隔,因此您必须首先识别它,然后拆分和解析每个部分等。祝你好运。 - adeneo
2
有多种理解框架的方法,这是一种比盲目使用现有框架更好的心态。然而,你提出的方法是重新发明轮子——这将容易出错且耗时,但会给你最多的知识。另一种方法是阅读并理解 Express 源代码。考虑到你已经遇到了障碍(不得不发布到 SO),这可能是你前进最具生产力的方式。 - skav
显示剩余3条评论
4个回答

5
你可以使用Buffer.from将POST请求的body转换为字符串,然后通过请求头中提供的boundary值来进行.split操作。这样可以将body分割成一个数组。
现在,你可以处理这些数组元素,以确定哪些是文件,哪些是键值对。下面的代码示例说明了这一点。

NodeJS / 服务器端示例

const SERVER = http.createServer(async function(request, response) {
  let statusCode = 200;
  if(request.url === '/app') {
    let contentTypeHeader = request.headers["content-type"];
    let boundary = "--" + contentTypeHeader.split("; ")[1].replace("boundary=","");
    if (request.method == 'POST') {
      let body = [];
      request.on('data', chunk => {
        body.push(chunk)
      });
      request.on('end', async () => {
        body = Buffer.concat(body).toString();
        let bodyParts = body.split(boundary);
        let result = [];
        bodyParts.forEach(function(val,index){
          val = val.replace("Content-Disposition: form-data; ","").split(/[\r\n]+/);
          if(isFile(val)){
            result.push(returnFileEntry(val))
          }
          if(isProperty(val)){
            result.push(returnPropertyEntry(val))
          }
        })
        console.log(result)
      });  
      response.end();  
    }
    response.end();

然后是处理函数

function returnPropertyEntry(arr){
  if (!Array.isArray(arr)) {return false};
  let propertyName = '';
  let propertyVal = undefined;
  arr.forEach(function(val,index){
    if(val.includes("name=")){
      propertyName = arr[index].split("name=")[1];
      propertyVal = arr[index + 1]
    }
  })
  return [propertyName,propertyVal];
}

function returnFileEntry(arr){
  if (!Array.isArray(arr)) {return false};
  let fileName = '';
  let file = undefined;
  arr.forEach(function(val,index){
    if(val.includes("filename=")){
      fileName = arr[index].split("filename=")[1];
    }
    if(val.toLowerCase().includes("content-type")){
      file = arr[index + 1];
    }
  })
  return [fileName,file];
}
function isFile(part){
  if(!Array.isArray(part)){return false};
  let filenameFound = false;
  let contentTypeFound = false;
  part.forEach(function(val,index){
    if (val.includes("filename=")){
      filenameFound = true;
    }
    if (val.toLowerCase().includes("content-type")){
      contentTypeFound = true;
    }
  });
  part.forEach(function(val,index){
    if (!val.length){
      part.splice(index,1)
    }
  });
  if(filenameFound && contentTypeFound){
    return part;
  } else {
    return false;
  }
}
function isProperty(part){
  if(!Array.isArray(part)){return false};
  let propertyNameFound = false;
  let filenameFound = false;
  part.forEach(function(val,index){
    if (val.includes("name=")){
      propertyNameFound = true;
    }
  });
  part.forEach(function(val,index){
    if (val.includes("filename=")){
      filenameFound = true;
    }
  });
  part.forEach(function(val,index){
    if (!val.length){
      part.splice(index,1)
    }
  });
  if(propertyNameFound && !filenameFound){
    return part;
  } else {
    return false;
  }
}

enter image description here


2
我遇到了同样的问题;因为我使用的Web服务器是用C++编写的,带有一个Javascript API(虽然符合标准,但与Node.js不同)。所以我必须自己造轮子。
然后我发现了这个npm模块parse-multipart-data。它很有效,并且你可以阅读源代码,它只有一个文件,作者非常清楚地解释了它是什么以及如何使用它。
附言:当你越往上走时,你需要往下走。有经验的程序员会知道我在说什么 :)

1
我来到这里是为了寻找一个能够替代在使用fetch(req)时可用的req.formData方法的函数,当无法使用fetch时。就像原来的问题所述,我只对返回的键值字符串感兴趣。不过,与原问题不同的是,我希望结果与使用.formData转换为Object.fromEntries()的格式相同。这里的被接受的答案以及https://www.section.io/engineering-education/a-raw-nodejs-rest-api-without-frameworks-such-as-express/上的getReqData(req)函数帮助我创建了这个(typescript)函数,这个函数对我很有用。
function getFormData(request:any) {
  return new Promise<{[key:string]:string}>((resolve, reject) => {
    try {
      const contentTypeHeader = request.headers["content-type"];
      const boundary = "--" + contentTypeHeader.split("; ")[1].replace("boundary=","");
      const body = [] as any;
      request.on('data', (chunk:any) => { body.push(chunk) });
      request.on('end', () => {
        const formDataSubmitted: {[key:string]:string} = {};
        const bodyParts = Buffer.concat(body).toString().split(boundary);
        bodyParts.forEach((val:string) => {
          // After name=.. there are 2 \r\n before the value - that's the only split I want
          // So, the regex below splits at the first occurance of \r\n\r\n, and that's it
          // This way, newlines inside texarea inputs are preserved
          const formDataEntry = returnKeyValObj(
            val.replace("Content-Disposition: form-data; ","").split(/\r\n\r\n(.*)/s)
          );
          if (formDataEntry) Object.assign(formDataSubmitted, formDataEntry);
        })
        if (Object.keys(formDataSubmitted).length) resolve(formDataSubmitted);
      });
    } catch (error) {
      reject(error);
    }
  });
}

function returnKeyValObj(arr:Array<string>){
  if (!Array.isArray(arr) || arr.length < 2) return false;
  let propKey = '';
  const formDataEntries: {[key:string]:string} = {};
  const [pKey, ...pValArray] = arr;
  // pValArray[0] ends with \r\n (2 characters total)
  const propVal = pValArray[0].slice(0,-2)
  // pKey looks like '\r\nname=\"key\"', where \r and \n and \" count as one character each
  // So, need to remove 8 from start of pKey and 1 from end of pKey
  if (pKey && pKey.includes('name=\"')) propKey = pKey.slice(8).slice(0,-1);
  if (propKey) formDataEntries[propKey] = propVal;
  if (Object.keys(formDataEntries).length) return formDataEntries;
  return false;
}


所以,不是
        const data = await req.formData();
        const dataEntries = Object.fromEntries(data.entries());

我可以使用
      const dataEntries = await getFormData(req);

而且,我所有其他的代码都保持不变。

0
嗨,我看到你正在尝试使用HTTP请求将前端的表单数据传输到后端。让我们以一种更简单和高效的方式来做:你可以使用packetgun-frontend和packetgun-backend的npm包,像这样使用(我自己做的,别看它们的文档,很糟糕):
在前端项目文件夹中运行以下命令:
npm install packetgun-frontend

然后在您的代码中写入以下内容(将ip替换为您的服务器ip,将port替换为您正在监听的端口)。

Html

<!DOCTYPE html>
<html>
    <head>
        <title>Cool form chat website</title>
    </head>
    <body>
        <div id="form">
            <h1 id="title">Enter a message:</h1>
            <input type="text" id="message" placeholder="Hello i am a cool dev">
            <button id="send">Send</button>
        </div>
    </body>
</html>
<!--The path may be a bit different sometimes-->
<script src="node_modules/packetgun-frontend/packetgun-frontend.js"></script>
<script src="./app.js"></script>

Javascript [app.js]

//Get textbox
var message = document.getElementById("message");
//Get button
var send = document.getElementById("send");
var open = function(){
    packetgun.recommended("127.0.0.1:1234", function (client) {
        send.onclick = function () {
            //On button click send form data...
            client.send({
                "exit_code": 0,
                "data": JSON.stringify({
                    "message": message.value
                })
            })
            //Empty textbox
            message.value = "";
        }
        message.onkeydown = function (event) {
            //If pressed enter key while in the textbox
            if (event.keyCode === 13) {
                //Click the button
                send.click();
            }
        }
        client.on("serverDisconnect", function(){
            //On client close restart the process
            open();
        })
    })
}
open()

在你的node.js项目文件夹中运行以下命令。
npm install packetgun-backend

然后在你的代码中写入以下内容(将端口替换为你想要使用的端口):
//Require packetgun
var packetgun = require("packetgun-backend");

//Init packetgun
packetgun.init()
//Listen with recommended method
packetgun.listen.recommended(port, function(client){
    //When receiving data from client
    client.on("data", function(data){
        //Ignore exit_code and client_exit_code just get client data
        data = data.data;
        //Check if data requires preprocessing
        if (typeof data === "string"){
            try{
                //Preprocess data
                data = JSON.parse(data)
            }
            catch (error){
                //Client data is corrupted
                console.error("Client data is corrupted :(");
                client.close();
            }
        }
        //Close client to mimic an http request (disconnect client)
        client.close();
        //data is the JSON object you sent from the frontend side containing the form data, use that data as you whish
        //For exemple let's stringify and log the data
        console.log(JSON.stringify(data, null, 4));
    })
})

我测试了这段代码,它运行正常!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接