在AJAX中发送嵌套的FormData

19

我需要使用ajax和FormData发送一些数据,因为我想要发送一个文件和一些其他参数。我通常发送数据的方式是:

$.ajax({
    type:       'POST',
    url:        'some_url',
    dataType:   'json',
    processData:false,
    contentType:false,
    data:{
        Lvl_1-1: 'something',
        Lvl_1-2: 'something',
        Lvl_1-3: {
            Lvl_1-3-1: "something",
            Lvl_1-3-2: "something",
            Lvl_1-3-3: "something",
        },
    },
    ...
});
如果我不使用FormData(),就没有问题,但是当使用FormData()时,只有Lvl1上的数据是正常的,但任何嵌套的内容都显示为字符串,如下所示。
<b>array</b> <i>(size=3)</i>
    'Lvl1-1' <font color='#888a85'>=&gt;</font> <small>string</small> 
        <font color='#cc0000'>'Something'</font> 
        <i>(length=23)</i>
    'Lvl1-2' <font color='#888a85'>=&gt;</font> <small>string</small> 
        <font color='#cc0000'>''Something''</font> <i>(length=3)</i>
    'Lvl1-3' <font color='#888a85'>=&gt;</font> <small>string</small> 
        <font color='#cc0000'>'[object Object]'</font> <i>(length=17)</i>

如果我使用FormData()来编码Lvl1-3内部的数据,那么我得到的将不再是[object Object],而是[object FormData]

如何才能在Lvl1-3上获得数组而不是字符串?

注意:如果文件在顶层(Lvl_1),我可以使用FormData()发送文件而没有任何问题。我没有写附加文件的代码,因为那不是问题,嵌套数据才是问题。我只提到了文件,因为这就是我使用FormData()的原因。


请查看 https://github.com/foo123/serialiser.js,以将复杂/嵌套的表单字段序列化为formData、对象、json、url编码数据(作者)。 - Nikos M.
3个回答

23

URL编码的表单数据没有原生方法表达复杂数据结构,它仅支持简单的键值对。

?foo=1&bar=2

大多数表单数据解析库允许使用相同名称的键来表示数据数组

?foo=1&foo=2

PHP在该格式的基础上添加了自己的语法:

?foo[]=1&foo[]=2

这使得关联数组中可以使用命名键:

?foo[bar]=1&foo[baz]=2

以及嵌套的数组:

?foo[bar][level2a]=1&foo[bar][level2b]=2
由于PHP的普及,当您将JavaScript对象传递给"data"时,jQuery采用了该语法来生成表单数据。
如果要使用"FormData",则jQuery不会为您重新处理它。
您看到的效果是因为您尝试将一个对象(我猜测是一个FormData实例,但您没有展示代码的那部分)作为第二个参数放入"append"中,而只有字符串才被期望。
您需要自己使用PHP的语法来生成键名。
form_data_instance.append("Lvl_1-3[Lvl_1-3-1]", "something");
form_data_instance.append("Lvl_1-3[Lvl_1-3-2]", "something");
form_data_instance.append("Lvl_1-3[Lvl_1-3-3]", "something");

谢谢!这就是我一直在寻找的。现在我对这个主题有了更好的理解。 - kunde

8

我会将我的嵌套参数转化为字符串并在另一端进行解析。

例如,如果我想传递:

{"sthing":
  {"sthing":"sthing"},
  {"picture":
    {"legend":"great legend"},
    {"file":"great_picture.jpg"}
  }
}

接着我执行:

// On the client side
const nestedParams = {"sthing":
                       {"sthing":"sthing"},
                       {"picture":
                         {"legend":"great legend"}
                       }
                     };
const pictureFile = document.querySelector('input[type="file"]')[0];
const formDataInstance = new FormData;
formDataInstance.append("nested_params": JSON.stringify(nested_params);
formDataInstance.append("file": document.querySelector('input[type="file"]')[0]);


// On the server side
params["nested_params"] = JSON.parse(params["nested_params"]);
params["nested_params"]["sthing"]["picture"]["file"] = params["file"];

这只适用于处理JS对象(您可以使用类型“application/json”发送)。如果您需要发送二进制文件,则不能使用该方法,这也是使用FormData而不是JSON的主要原因。 - Thierry J.
@ThierryJ. 为什么这个不行呢?Freddo基本上建议使用formdata来发送二进制文件,就像你建议的那样,并且还要将JSON作为字符串一起发送... - GrowinMan
@GrowinMan 这是一个非常好的问题 :) 老实说,我不记得为什么会这样说了。也许我只读了答案的开头而没有阅读代码。在当前答案的状态下,它应该确实按要求工作。 - Thierry J.

1
为了补充Quentin的回答,我有一些PHP Laravel代码如下:
    $tags = [];
    foreach ($request->input('tags') as $tag) {
        if (!is_array($tag)) {
            $new_tag = Tag::generate($tag);
            array_push($tags, $new_tag->id);
        } else {
            array_push($tags, $tag['id']);
        }
    }

你可以看到,我从一个空数组开始,然后用$request->input('tags')中的值填充它。该请求输入是一个多维数组,因此它遇到了这个问题所描述的问题。仅在使用FormData和multipart/form-data表单编码类型时才会出现问题。
我能够通过Quentin的答案以及这里的客户端JavaScript代码来解决它:
this.example.tags.forEach((tag, i) => {
    if (Object.prototype.toString.call(tag) === '[object String]') {
        payload.append(`tags[${i}]`, tag);
    } else {
        Object.keys(tag).forEach(field => payload.append(`tags[${i}][${field}]`, tag[field]));
    }
});

这段代码首先检查要添加到FormData的标签是否为字符串。如果是,则表示这是一个新的、不存在的标签,然后按其索引号将其添加。如果该标签已经存在,则只发送客户端拥有的所有值。在我的情况下,服务器只关心名称和ID(用于与该行ID的SQL关系),所以这很好,但它是使用语法如arr[index][field]的练习。

如果您学习Quentin和我给出的答案,应该能够看到模式。我的例子也有一定的复杂性,因此在我看来检查它会很好。

为了完全理解,以下是有效载荷的样子:

  'tags' => 
  array (
    0 => 
    array (
      'id' => '2',
      'name' => 'React JS',
    ),
    1 => 
    array (
      'id' => '5',
      'name' => 'JSON Web Tokens',
    ),
    2 => 'Sandwiches',
  ),

你可以看到有3个标签。前两个已经存在。第三个在我的数据库中不存在,因此它以字符串值而不是对象的形式出现。Laravel/PHP 不喜欢嵌套对象 "existing tags",所以我必须修改 FormData(其中包含图片,因此需要多部分编码类型)。
为了提供背景,这是我的整个 submitForm 函数:
const payload = new FormData();

Object.keys(this.example).forEach(field => payload.append(field, this.example[field]));

this.example.tags.forEach((tag, i) => {
    if (Object.prototype.toString.call(tag) === '[object String]') {
        payload.append(`tags[${i}]`, tag);
    } else {
        Object.keys(tag).forEach(field => payload.append(`tags[${i}][${field}]`, tag[field]));
    }
});

this.example.images.forEach(image => payload.append('images[]', image));

const response = await axios.post(route('admin.examples.create'), payload);

参考资料: JavaScript中如何检查一个变量是否为字符串


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