iT邦幫忙

0

AJAX 一次上傳多個檔案與檔案說明 (JSON Array 包含file欄位與非file欄位)

  • 分享至 

  • xImage

工作上遇到一個情境:

他們希望在一個分類上,Call一次AJAX上傳所有的圖片檔案與圖片檔案的說明文字

Request的格式如下:

{
 "id": "分類id",
 "title": "分類標題",
 "Files":[
   {
    "FileName":"檔案1",
    "ImageIndex": 1,
    "ImageFile":"../image/5.jpg", /* input=file 欄位 */
    "ImageRemark":"備註說明",
   },
    {
     "FileName":"檔案2",
     "ImageIndex": 2,
     "ImageFile":"../image/5.jpg", /* input=file 欄位 */
     "ImageRemark":"備註說明",
    },
 ]
}

因為有檔案上傳的部分,所以必須需要使用FormData
但是請求裡面有JsonArray,而且JsonArray混入input file的欄位和非input file的欄位
想請問該怎麼從Client端透過JS實作AJAX?或者如果不可行的話,是否該跟開規格的溝通其他方式?

感謝大大的協助。

結論

FormData混Json 和 File 雖然不是不可行
可以根據淺水員大大的回答實現

但是在設計上,根據 fillano 大大說的
規格的原設計會有問題。

看更多先前的討論...收起先前的討論...
淺水員 iT邦大師 6 級 ‧ 2020-06-05 13:16:14 檢舉
ImageData 跟 ImageFile 可以說明一下是什麼嗎?
雖然我會覺得 ImageData 就是圖檔的真實資料(轉成base64),但這樣一來 ImageFile 好像就沒有作用了。
通靈亡 iT邦高手 1 級 ‧ 2020-06-05 13:22:57 檢舉
淺水員:
ImageData 開規格那邊原本是希望丟image 的Url encode
後來開規格的決定改用圖片上傳的方式
所以ImageData 和 ImageMedia 都不需要
感謝提醒,我修正問題的Request格式

所以Files格式的部分主要是FileName、ImageIndex、ImageFile、ImageRemark
咖咖拉 iT邦好手 1 級 ‧ 2020-06-05 13:41:01 檢舉
先做成list 在整理list轉成JSON 不行嗎?
通靈亡 iT邦高手 1 級 ‧ 2020-06-05 13:47:39 檢舉
咖冰拉:
如果要轉成JSON就是像淺水員和fillano提到的轉Data Url先處理掉
為了要讓後端處理File,變成要用FormData包input file的資料
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 個回答

2
fillano
iT邦超人 1 級 ‧ 2020-06-05 13:36:46
最佳解答

一個可能性:

https://developer.mozilla.org/zh-TW/docs/Web/API/FileReader

然後把檔案內容用DataUrl字串或TypedArray的方式讀取放進JSON,不過後端需要針對這個內容做一次解碼。

看更多先前的回應...收起先前的回應...
通靈亡 iT邦高手 1 級 ‧ 2020-06-05 13:43:40 檢舉

fillano
如果我沒理解錯的話
這樣子的做法是不是在淺水員大大提問前的Request
input file 的欄位選完檔案之後轉成Data Url的字串

這樣子的好處是不需要FormData包住AJAX的Request
不過開規格的希望可以丟File,圖片的檔案相關處理讓後端來處理

但是這樣的作法讓我疑惑的是,FormData 怎麼處理像這樣子的Request格式?

通靈亡
你說的處理 是指這樣嗎

var formdata = new FormData();
var blobData = Blob([/* {uploadfile} */], {type: "image/jpeg"})
formdata.append('file', blobdata)
通靈亡 iT邦高手 1 級 ‧ 2020-06-05 14:05:08 檢舉

listennn08
如果是前端轉成Data Url
可以用FileReader轉換後存到JsonArray

不過如果要丟到後端接type=file的檔案欄位,可以用Blob處理後存到JsonArray嗎?

fillano iT邦超人 1 級 ‧ 2020-06-05 14:06:54 檢舉

post json時,request的body是json字串,並不是multipart form,這兩個不能混在一起用。非要混用不可的話,可以考慮把json字串改成當做form的一欄。

通靈亡 iT邦高手 1 級 ‧ 2020-06-05 14:19:38 檢舉

fillano
我看到也覺得Request格式JsonArray裡面混 File 跟一般的文字內容的設計真的讓人很困擾

我印象中是不是可以用一個陣列分開獨立放所有的 File
例如:

formData.append('pic[]', myFileInput1.files[0], 'chris1.jpg');
formData.append('pic[]', myFileInput2.files[0], 'chris2.jpg');

如果是的話,我可能必須跟開規格的溝通改設計了...

fillano iT邦超人 1 級 ‧ 2020-06-05 14:45:45 檢舉

如果你熟各種form的body格式就知道,他的要求真的有問題XD

multi part格式的request,會在標頭先定義一個part的分隔字串,然後在request body中,分隔字串之間,會有各個part的header跟body。欄位名稱、檔案名稱等會放在part的header,欄位值或檔案內容會放在part body。這裡面不可能空降一個json字串,還有辦法各自剖析。但是當作欄位的值是沒問題的。

content-type是application/json的request,request body就只有json字串,加上其他東西剖析就會失敗(不合法的json),不可能用form的方式再來上傳檔案。

通靈亡 iT邦高手 1 級 ‧ 2020-06-05 14:56:17 檢舉

感謝 fillano 大大 協助
今天一整個早上都在檢視這個需求 /images/emoticon/emoticon04.gif

2
淺水員
iT邦大師 6 級 ‧ 2020-06-05 13:41:18

如果要混一般資料跟 File 一起發送 AJAX,FormData + Fetch可以做到
下面是範例:

html

<input type='file' id='fileupload'>
<div id='result'></div>
<script src="main.js"></script>

javascript

document.querySelector('#fileupload').addEventListener('change', function(evt){
    const files=evt.target.files;
    let info={
        "id": "分類id",
        "title": "分類標題",
        "Files": [
            {
                "FileName":"檔案1",
                "ImageIndex": 1,
                "ImageFile":"../image/5.jpg", /* input=file 欄位 */
                "ImageRemark":"備註說明",
            }
        ]
    };
    let theFormData=new FormData();
    theFormData.append('info',JSON.stringify(info));
    theFormData.append('../image/5.jpg', files[0]);
    fetch('test.php',{
        method: 'post',
        body: theFormData
    }).then((response)=>{
        return response.text();
    }).then((html)=>{
        document.querySelector('#result').innerHTML=html;
    });
});

test.php

$infoData=json_decode($_POST['info'], true);
echo '<pre>'.print_r($infoData, true).'</pre>';
echo '<pre>'.print_r($_FILES, true).'</pre>';

PS. File 的 key 好像會把 .. 自動轉成底線,這部分可能要注意一下。

看更多先前的回應...收起先前的回應...
通靈亡 iT邦高手 1 級 ‧ 2020-06-05 13:59:58 檢舉

先謝謝淺水員大大

我補充實際的操作情境:

新增一個分類,分類名稱叫分類1
分類1底下可以讓使用者,新增多組圖片,圖片1、圖片2、圖片3
每個圖片可輸入各自的說明文字
上傳幾張的檔案選擇欄位、說明文字欄位會由JS動態產生

當我按下送出的時候,包成Request,傳到後端
讓後端新增分類1,並同時上傳分類1的所有圖片檔案以及圖片說明文字

因此跟你程式碼轉成stringfy又不太一樣
Files會長下面這個樣子:

[
    {
        "FileName":"檔案1",
        "ImageIndex": 1,
        "ImageFile": 檔案1 input file的內容, /* input=file 欄位 */
        "ImageRemark":"圖片說明1",
    },
    {
         "FileName":"檔案2",
         "ImageIndex": 2,
         "ImageFile": 檔案2 input file的內容, /* input=file 欄位 */
         "ImageRemark":"圖片說明2",
    }
]

其中Files裡面的數量,會根據上傳的圖片數量而改變

{
    "FileName":"檔案1",
    "ImageIndex": 1,
    "ImageFile": 檔案1 input file的內容, /* input=file 欄位 */
    "ImageRemark":"圖片說明1",
 }
淺水員 iT邦大師 6 級 ‧ 2020-06-05 14:28:25 檢舉

我先確認一下,ImageFile 放的是「input=file 實際的內容」還是只是「作為一個 key ,這個 key 可以讓後端從 $File 找到檔案內容」?

另外我只放一個檔案只是範例,如果是多的檔案就:
(下面這是把 ImageFile 當作 key的做法)

let info={
    "id": "分類id",
    "title": "分類標題",
    "Files": [
        {
            "FileName":"檔案1",
            "ImageIndex": 1,
            "ImageFile":"key1", /* input=file 欄位 */
            "ImageRemark":"備註說明",
        },{
            "FileName":"檔案2",
            "ImageIndex": 2,
            "ImageFile":"key2", /* input=file 欄位 */
            "ImageRemark":"備註說明",
        }
    ]
};
let theFormData=new FormData();
theFormData.append('info',JSON.stringify(info));
theFormData.append('key1', files[0]);
theFormData.append('key2', files[1]);

依此類推你可以自己改

淺水員 iT邦大師 6 級 ‧ 2020-06-05 14:34:52 檢舉

我上面是假設你們傳的是 multipart form
如果只是單純傳 JSON ,那就要訂好 ImageFile 使用的編碼了
(這時可能還是用 base64 比較好)

通靈亡 iT邦高手 1 級 ‧ 2020-06-05 14:38:22 檢舉

是input=file 的實際的內容

這樣子是不是在FormData直接拉兩個陣列,一個專門放File,另一個放圖片文字資訊的Json會比較恰當?

{
    Files: [圖片1的input file內容, 圖片2的input file內容],
    ImagesInfo: [
        {
            "FileName":"檔案1",
            "ImageIndex": 1,
            "ImageRemark":"圖片1說明",
        },
        {
            "FileName":"檔案2",
            "ImageIndex": 2,
            "ImageRemark":"圖片2說明",
        },
    ]
}

是的話,我就必須跟開規格的溝通改設計了

淺水員 iT邦大師 6 級 ‧ 2020-06-05 16:07:43 檢舉

雖然問題好像解決了,不過我還是回一下
原先 ImageFile 欄位要放檔案內容是可以的
只是要轉成 base64 編碼
然後後端再把 base64 編碼還原後儲存起來就可以
PS. 這邊我也不用 multipart form 傳送了,直接送 JSON
html

<input type='file' id='fileupload' multiple>

javascript

function readFileAsPromise(file)
{
    return new Promise((success, error)=>{
        let frd=new FileReader();
        frd.onload=function(){
            success({
                name: file.name,
                buffer: this.result.split(';base64,')[1]
            });
        }
        frd.readAsDataURL(file);
    });
}

document.querySelector('#fileupload').addEventListener('change', function(evt){
    let readFiles=Array.from(evt.target.files).map(f=>readFileAsPromise(f));
    Promise.all(readFiles).then((readerArray)=>{
        let filesData=readerArray.reduce((arr,rd,idx)=>{
            arr.push({
                FileName: rd.name,
                ImageIndex: idx,
                ImageFile: rd.buffer,
                ImageRemark: '備註說明'+idx
            });
            return arr;
        },[]);
        console.log(filesData);
        return fetch('reciver.php', {
            method:'post',
            body: JSON.stringify({
                time: Date.now(), //可以放一些其他資訊
                files: filesData
            })
        });
    });
});

後端我不知道你們用甚麼語言,這是 PHP 的範例

<?php
//只是示範,忽略錯誤處理跟檢查
$jsonStr=file_get_contents('php://input');
$requestData=json_decode($jsonStr, true);
$timestamp=$requestData['time'];
foreach($requestData['files'] as $fileInfo) {
    $name=$timestamp.$fileInfo['FileName'];
    $data=base64_decode($fileInfo['ImageFile']);
    file_put_contents($name, $data);
}
通靈亡 iT邦高手 1 級 ‧ 2020-06-05 17:43:05 檢舉

感謝淺水員

最後請開規格的將設計修改成開兩個陣列:
一個陣列放所有input type=file的欄位,做檔案的相關處理
另一個陣列放Json Array的字串,傳到後端轉換處理

這樣做是因為開規格的不想讓後端處理url encode

算是你提供的轉Json字串方案+我的想法。

我要發表回答

立即登入回答