zoukankan      html  css  js  c++  java
  • 如何实现一个FormData

    一、前言

    最近项目中遇到一个问题,我们需要在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 + '
    ');
            this._result.push('Content-Disposition: form-data; name="' + key + '"' + '
    
    ');
            this._result.push(value);
            this._result.push('
    ');
    	}
    	// 上传复杂数据——文件
    	appendFile(name, data, ext){
    		let type = "audio/mpeg";
    		let filename =  "upload."+ext;
    
    		this._result.push(`${this._boundary}
    `);
    		this._result.push(`Content-Disposition: form-data; name="${name}"; filename="${filename}"
    `); // 上传文件定义
    		this._result.push(`Content-Type: ${type}
    `);
    		this._result.push("
    ");
    		this._result.push(data);
    		this._result.push("
    ");
    	}
    	// 获取二进制数据 get
    	get arrayBuffer() {
    		this._result.push('
    ' + this._end_boundary); // 结尾分隔符
    		let charArr = [];
            
            // 处理charCode
    		for (let i = 0; i < this._result.length; i++) { // 取出文本的charCode
    			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。

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

    踩坑记录:

    1. 因为我们上传的是二进制流数据,appendFile函数添加后缀名,在formdata数据里定义好Content-Disposition里的文件名,方便和后端开发人员识别是什么样的文件,是有必要的。
    2. request headers里注意multipart/form-data; boundary=${stamp}这里一定要把随机数写到boundary=后面,否则后端服务会报错'no multipart boundary was found'

    五、如果觉得有帮助的话,还请给个赞,谢谢!

  • 相关阅读:
    easyui源码翻译1.32--Droppable(放置)
    easyui源码翻译1.32--Draggable(拖动)
    easyui源码翻译1.32--Dialog(对话框窗口)
    easyui源码翻译1.32--DateTimeBox(日期时间输入框)
    easyui源码翻译1.32--DateBox(日期输入框)
    easyui源码翻译1.32--ComboTree(树形下拉框)
    easyui源码翻译1.32--ComboGrid(数据表格下拉框)
    我不曾忘记的初心-大厂小厂
    我不曾忘记的初心-屌丝逆袭
    我不曾忘记的初心-愿天堂没有代码
  • 原文地址:https://www.cnblogs.com/hackftz/p/13882180.html
Copyright © 2011-2022 走看看