如何實現一個FormData
- 2020 年 10 月 26 日
- 筆記
- javascript
一、前言
最近項目中遇到一個問題,我們需要在cocos項目里去上傳音頻文件,而cocos原生環境和平時我們開發所在的瀏覽器環境和Node環境有很多差異,而cocos環境只提供了基礎類,沒有提供FormData這種封裝類。
所以問題來了?如何實現一個FormData,以及怎麼去使用它?
二、瀏覽器中的FormData
這裡我列一個最簡單的例子,我們來看看FormData到底是什麼。
function App() {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const [file, setFile] = useState<File | null>()
const submit = () => {
console.log(name, age);
console.log(file);
var fd = new FormData()
fd.append('name', name)
fd.append('age', age.toString())
fd.append('file', file as Blob)
$.ajax({
type: "POST",
url: "www.happy.com",
data: fd,
processData: false,//重要
contentType: 'multipart/form-data',//重要
success: function (data: any) {
}
})
}
return (
<div className="App">
<form action="form_action.asp" method="get">
<p>name: <input type="text" name="name" value={name} onChange={e => setName(e.currentTarget.value)} /></p>
<p>age: <input type="number" name="age" value={age} onChange={e => setAge(Number(e.currentTarget.value))} /></p>
<p>file:<input type="file" name="file" onChange={e => setFile(e.target.files && e.target.files[0])}/>
</p>
<input type="button" name="b1" value="submit" onClick={() => submit()} />
</form>
</div>
);
}
FormData:FormData 介面提供了一種表示表單數據的鍵值對 key-value 的構造方式,並且可以輕鬆的將數據通過XMLHttpRequest.send() 方法發送出去,本介面和此方法都相當簡單直接。如果送出時的編碼類型被設為 “multipart/form-data”,它會使用和表單一樣的格式。(摘自MDN)
大多數文章里,只給了這樣的一種描述或者說是概念,它是一個介面類,用來做上傳用,我們來看它在數據形式上體現的是什麼。
下面是chrome devtool request payload里的樣子。
------WebKitFormBoundaryuhGsgTdqAAltAXy7 // 分隔/邊界符
Content-Disposition: form-data; name="name" // 內聯形式
hackftz // value
------WebKitFormBoundaryuhGsgTdqAAltAXy7
Content-Disposition: form-data; name="age"
22
------WebKitFormBoundaryuhGsgTdqAAltAXy7
Content-Disposition: form-data; name="file"; filename="Minstrel - eyecatch!.mp3"
Content-Type: audio/mpeg
------WebKitFormBoundaryuhGsgTdqAAltAXy7-- // 這裡是end_boundary,結尾分隔/邊界符,必需!
三、我在實現FormData時遇到了哪些坑?
先貼程式碼,然後說說我遇到了哪些坑。
export default class MyFormData {
// 將隨機數傳入構造函數
constructor(stamp) {
this._boundary_key = stamp; // 隨機數,分隔符和結尾分隔符必需。
this._boundary = '--' + this._boundary_key;
this._end_boundary = this._boundary + '--';
this._result = [];
}
// 上傳普通鍵值對——字元串、數字
append(key, value) {
this._result.push(this._boundary + '\r\n');
this._result.push('Content-Disposition: form-data; name="' + key + '"' + '\r\n\r\n');
this._result.push(value);
this._result.push('\r\n');
}
// 上傳複雜數據——文件
appendFile(name, data, ext){
let type = "audio/mpeg";
let filename = "upload."+ext;
this._result.push(`${this._boundary}\r\n`);
this._result.push(`Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n`); // 上傳文件定義
this._result.push(`Content-Type: ${type}\r\n`);
this._result.push("\r\n");
this._result.push(data);
this._result.push("\r\n");
}
// 獲取二進位數據 get
get arrayBuffer() {
this._result.push('\r\n' + this._end_boundary); // 結尾分隔符
let charArr = [];
// 處理charCode
for (let i = 0; i < this._result.length; i++) { // 取出文本的charCode(10進位
let item = this._result[i];
if( typeof(item) === 'string'){
for (let s = 0; s < item.length; s++){
charArr.push(item.charCodeAt(s));
}
} else if(typeof(item) === 'number') {
let numstr = item.toString()
for (let s = 0; s < numstr.length; s++){
charArr.push(numstr.charCodeAt(s));
}
} else{
for (let j = 0; j < item.length; j++){
charArr.push(item[j]);
}
}
}
let array = new Uint8Array(charArr);
return array.buffer;
}
}
踩坑記錄:
- 首先,我需要定義一個boundary_key,它當前環境提供給FormData的隨機數,chrome v87.0.4270.0提供給FormData的是”WebKitFormBoundary” + “xxxxxxxxxxxxxx” 隨機數。我在項目里使用的是timestamp,這裡只要提供一個隨機數即可。
- appendFile方法的實現,要根據具體上傳類型,文件類型,作content-type定義,比如我這裡上傳的是音頻文件,所以設置的是”audio/mpeg”。
- 普通鍵值對和複雜鍵值對的區分,如果value是字元串,直接分解成字元再處理;如果是number,這裡有個坑,那就是直接添加到FormData會失敗,所以需要先把number值轉為string,再像處理string值一樣處理。
- 再看arrayBuffer實現方法,我們可以得知FormData最終要給api data的值是一個由具體blob值,分解為單個字元,存儲到一個字元數組中,再創建一個參數為字元數組的新的Uint8Array數組,最終可以將這樣一個arrayBuffer數據(通用的、固定長度的原始二進位數據緩衝區。)提供給伺服器去解析。
以上是封裝FormData中我遇到的問題,再來看怎麼去使用這樣一個我們自定義的FormData。
四、MyFormData的使用
話不多說,先貼程式碼,再談問題:
const stamp = Date.now() // 生成隨機數,這裡使用了時間戳
const fd = new MyFormData(stamp)
for (const key in data) {
if (data.hasOwnProperty(key)) {
fd.append(key, data[key])
}
}
fd.appendFile('file', blob, data.fileExtName); // 添加要上傳的文件,這裡記得第三個參數要傳入文件後綴名。
const config = {
headers: {
'Content-Type': `multipart/form-data; boundary=${stamp}` // 分隔符
},
};
axios({
url,
data: fd.arrayBuffer,
method: 'POST',
headers
})
.then(response => {
if (response.status === 200) {
const { data } = response;
console.log("fun -> JSON.stringify(data)", JSON.stringify(data))
}
})
.catch(err => {
console.log(err);
});
踩坑記錄:
- 因為我們上傳的是二進位流數據,appendFile函數添加後綴名,在formdata數據里定義好Content-Disposition里的文件名,方便和後端開發人員識別是什麼樣的文件,是有必要的。
- request headers里注意
multipart/form-data; boundary=${stamp}
這裡一定要把隨機數寫到boundary=
後面,否則後端服務會報錯’no multipart boundary was found’