如何實現一個FormData

1、前言

最近項目中遇到一個問題,咱們須要在cocos項目裏去上傳音頻文件,而cocos原生環境和平時咱們開發所在的瀏覽器環境和Node環境有不少差別,而cocos環境只提供了基礎類,沒有提供FormData這種封裝類。ios

因此問題來了?如何實現一個FormData,以及怎麼去使用它?ajax

2、瀏覽器中的FormData

這裏我列一個最簡單的例子,咱們來看看FormData究竟是什麼。chrome

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)axios

大多數文章裏,只給了這樣的一種描述或者說是概念,它是一個接口類,用來作上傳用,咱們來看它在數據形式上體現的是什麼。後端

下面是chrome devtool request payload裏的樣子。api

------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,結尾分隔/邊界符,必需!

3、我在實現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;
	}
}

踩坑記錄:瀏覽器

  1. 首先,我須要定義一個boundary_key,它當前環境提供給FormData的隨機數,chrome v87.0.4270.0提供給FormData的是"WebKitFormBoundary" + "xxxxxxxxxxxxxx" 隨機數。我在項目裏使用的是timestamp,這裏只要提供一個隨機數便可。
  2. appendFile方法的實現,要根據具體上傳類型,文件類型,做content-type定義,好比我這裏上傳的是音頻文件,因此設置的是"audio/mpeg"。
  3. 普通鍵值對和複雜鍵值對的區分,若是value是字符串,直接分解成字符再處理;若是是number,這裏有個坑,那就是直接添加到FormData會失敗,因此須要先把number值轉爲string,再像處理string值同樣處理。
  4. 再看arrayBuffer實現方法,咱們能夠得知FormData最終要給api data的值是一個由具體blob值,分解爲單個字符,存儲到一個字符數組中,再建立一個參數爲字符數組的新的Uint8Array數組,最終能夠將這樣一個arrayBuffer數據(通用的、固定長度的原始二進制數據緩衝區。)提供給服務器去解析。

以上是封裝FormData中我遇到的問題,再來看怎麼去使用這樣一個咱們自定義的FormData。服務器

4、MyFormData的使用

話很少說,先貼代碼,再談問題:app

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);
	});

踩坑記錄:

  1. 由於咱們上傳的是二進制流數據,appendFile函數添加後綴名,在formdata數據裏定義好Content-Disposition裏的文件名,方便和後端開發人員識別是什麼樣的文件,是有必要的。
  2. request headers裏注意multipart/form-data; boundary=${stamp}這裏必定要把隨機數寫到boundary=後面,不然後端服務會報錯'no multipart boundary was found'

5、若是以爲有幫助的話,還請給個贊,謝謝!

相關文章
相關標籤/搜索