談?wù)劵趇frame、FormData、FileReader三種無刷新上傳文件的方法
發(fā)請求有兩種方式,一種是用ajax,另一種是用form提交,默認(rèn)的form提交如果不做處理的話,會(huì)使頁面重定向。以一個(gè)簡單的demo做說明:

html如下所示,請求的路徑action為"upload",其它的不做任何處理:
<form method="POST" action="upload" enctype="multipart/form-data"> 名字 <input type="text" name="user"></input> 頭像 <input type="file" name="file"></input> <input type="submit" id="_submit" value="提交"></input> </form>
服務(wù)端(node)response直接返回: "Recieved form data",演示如下:

可以看到默認(rèn)情況下,form請求upload的同時(shí)重定向到upload。但是很多情況下是希望form請求像ajax一樣,不會(huì)重定向或者刷新頁面。像上面的場景,當(dāng)上傳完成之后,將用戶選擇的頭像顯示在當(dāng)前頁面。
解決辦法第一種是使用html5的FormData,將form里面的數(shù)據(jù)封裝到FormData對象里,然后再以POST的方式send出去。如下面代碼所示,對提交按鈕的單擊事件做一個(gè)響應(yīng),代碼第6行獲取到form的DOM對象,然后第8行構(gòu)造一個(gè)FormData的實(shí)例,第18行,將form數(shù)據(jù)發(fā)送出去。
document.getElementById("_submit").onclick = function(event){
//取消掉默認(rèn)的form提交方式
if(event.preventDefault) event.preventDefault();
else event.returnValue = false; //對于IE的取消方式
var formDOM = document.getElementsByTagName("form")[];
//將form的DOM對象當(dāng)作FormData的構(gòu)造函數(shù)
var formData = new FormData(formDOM);
var req = new XMLHttpRequest();
req.open("POST", "upload");
//請求完成
req.onload = function(){
if(this.status === ){
//對請求成功的處理
}
}
//將form數(shù)據(jù)發(fā)送出去
req.send(formData);
//避免內(nèi)存泄漏
req = null;
}
上傳成功后,服務(wù)將返回圖片的訪問地址,補(bǔ)充14行對請求成功的處理:在submit按鈕的上方位置顯示上傳的圖片:
var img = document.createElement("img");
img.src = JSON.parse(this.responseText).path;
formDOM.insertBefore(img, document.getElementById("_submit"));
示例:

如果使用jQuery,可以把formData作為ajax的data參數(shù),同時(shí)設(shè)置contentType: false和processData: false,告訴jQuery不要去處理請求頭和發(fā)送的數(shù)據(jù)。
看起來這種提交方式跟ajax一樣,但是其實(shí)并不是完全一樣,form提交的數(shù)據(jù)格式有三種,如果要上傳文件則必須為multipart/form-data,所以上面的form提交請求里的http的頭信息里面的Content-Type為multipart/form-data,而普通的ajax提交為application/json。form提交完整的Content-Type如下:
"content-type":"multipart/form-data; boundary=------WebKitFormBoundaryYOE7pWLqdFYSeBFj"
除了multipart/form-data之外,還指定了boundary,這個(gè)boundary的作用是用來區(qū)分不同的字段。由于FormData對象是不透明的,調(diào)用JSON.stringify將會(huì)返回一個(gè)空的對象{},同時(shí)FormData只提供append方法,所以無法得到FormData實(shí)際上傳的內(nèi)容,但是可以通過分析工具或者服務(wù)收到的數(shù)據(jù)進(jìn)行查看。在上面如果上傳一個(gè)文本文件,那么服務(wù)收到的POST數(shù)據(jù)的原始格式是這樣的:
------WebKitFormBoundaryYOE7pWLqdFYSeBFj
Content-Disposition: form-data; name="user"
abc
------WebKitFormBoundaryYOE7pWLqdFYSeBFj
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
這是一個(gè)文本文件的內(nèi)容。
------WebKitFormBoundaryYOE7pWLqdFYSeBFj--
從上面服務(wù)收到的數(shù)據(jù)看出FormData提交的格式,每個(gè)字段以boundary隔開,最后以--結(jié)束。而ajax請求,send出去的數(shù)據(jù)格式是自定義的,一般都是以key=value中間用&連接:
var req = new XMLHttpRequest();
var sendData = "user=abc&file=這是一個(gè)文本文件的內(nèi)內(nèi)容";
req.open("POST", "upload");
//發(fā)送的數(shù)據(jù)需要轉(zhuǎn)義,見上面提到的三種格式
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.send(sendData);
服務(wù)就會(huì)收到和send發(fā)出去的字符串一模一樣的內(nèi)容,然后再作參數(shù)解析,所以就得統(tǒng)一參數(shù)的格式:
user=abc&file=這是一個(gè)文本文件的內(nèi)容
從這里可以看出POST本質(zhì)上并不比GET安全,POST只是沒有將數(shù)據(jù)放在網(wǎng)址傳送而已。
考慮到FormData到了IE10才支持,如果要支持較低版本的IE,那么可以借助iframe。
文中一開始就說,默認(rèn)的form提交會(huì)使頁面重定向,而重定向的規(guī)則在target中指定,可以和a標(biāo)簽一樣指定為"_blank",在新窗口中打開;還可以指定為一個(gè)iframe,在該iframe中打開。所以可以弄一個(gè)隱藏的iframe,將form的target指向這個(gè)iframe,當(dāng)form請求完成時(shí),返回的數(shù)據(jù)就會(huì)由這個(gè)iframe顯示,正如上面在新頁面顯示的:"Recieved form data"。請求完成后,iframe加載完成,觸發(fā)load事件,在load事件的處理函數(shù)里,獲取該iframe的內(nèi)容,從而拿到服務(wù)返回的數(shù)據(jù)了!拿到后再把iframe刪掉。
在提交按鈕的響應(yīng)函數(shù)里,首先創(chuàng)建一個(gè)iframe,設(shè)置iframe為不可見,然后再添加到文檔里:
var iframe = document.createElement("iframe");
iframe.width = 0;
iframe.height = 0;
iframe.border = 0;
iframe.name = "form-iframe";
iframe.id = "form-iframe";
iframe.setAttribute("style", "width:0;height:0;border:none");
//放到document
this.form.appendChild(iframe);
改變form的target為iframe的name值:
this.form.target = "form-iframe";
然后再響應(yīng)iframe的load事件:
iframe.onload = function(){
var img = document.createElement("img");
//獲取iframe的內(nèi)容,即服務(wù)返回的數(shù)據(jù)
var responseData = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent;
img.src = JSON.parse(responseData).path;
f.insertBefore(img, document.getElementById("_submit"));
//刪掉iframe
setTimeout(function(){
var _frame = document.getElementById("form-iframe");
_frame.parentNode.removeChild(_frame);
}, 100);
//如果提示submit函數(shù)不存在,請注意form里面是否有id/value為submit的控件
this.form.submit();
}
第二種辦法到這里就基本可以了,但是如果看163郵箱或者QQ郵箱上傳文件的方式,會(huì)發(fā)現(xiàn)和上面的兩種方法都不太一樣。用httpfox抓取請求的數(shù)據(jù),會(huì)發(fā)現(xiàn)上傳的內(nèi)容的格式并不是上面說的用boundary隔開,而是直接把文件的內(nèi)容POST出去了,而文件名、文件大小等相關(guān)信息放在了文件的頭部。如163郵箱:
POST Data:
this is a text
Headers:
Mail-Upload-name: content.txt
Mail-Upload-size: 15
可以推測它們應(yīng)該是直接讀取了input文件的內(nèi)容,然后直接POST出去了。要實(shí)現(xiàn)這樣的功能,可以借助FileReader,讀取input文件的內(nèi)容,再保留二進(jìn)制的格式發(fā)送出去:
var req = new XMLHttpRequest();
req.open("POST", "upload");
//設(shè)置和郵箱一樣的Content-Type
req.setRequestHeader("Content-Type", "application/octet-stream");
var fr = new FileReader();
fr.onload = function(){
req.sendAsBinary(this.result);
}
req.onload = function(){
//一樣,省略
}
//讀取input文件內(nèi)容,放到fileReader的result字段里
fr.readAsBinaryString(this.form["file"].files[0]);
代碼第13行執(zhí)行讀文件,讀取完畢后觸發(fā)第6行的load響應(yīng)函數(shù),第7行以二進(jìn)制文本形式發(fā)送出去。由于sendAsBinary的支持性不是很好,可以自行實(shí)現(xiàn)一個(gè):
if(typeof XMLHttpRequest.prototype.sendAsBinary === 'undefined'){
XMLHttpRequest.prototype.sendAsBinary = function(text){
var data = new ArrayBuffer(text.length);
var uia = new UintArray(data, );
for (var i = ; i < text.length; i++){
uia[i] = (text.charCodeAt(i) & xff);
}
this.send(uia);
}
}
代碼的關(guān)鍵在于第6行,將字符串轉(zhuǎn)成8位無符號(hào)整型,還原二進(jìn)制文件的內(nèi)容。在執(zhí)行了fr.readAsBinaryString之后,二進(jìn)制文件的內(nèi)容將會(huì)以utf-8的編碼以字符串形式存放到result,上面的第6行代碼將每個(gè)unicode編碼轉(zhuǎn)成整型(&0xff或者parseInt),存放到一個(gè)8位無符號(hào)整型數(shù)組里面,第8行把這個(gè)數(shù)組發(fā)送出去。如果直接send,而不是sendAsBinary,服務(wù)收到的數(shù)據(jù)將無法正常還原成原本的文件。
上面的實(shí)現(xiàn)需要考慮文件太大,需分段上傳的問題。
關(guān)于FileReader的支持性,IE10以上支持,IE9有另外一套File API。
文章討論了3種辦法實(shí)現(xiàn)無刷新上傳文件,分別是使用iframe、FormData和FileReader,支持性最好是的iframe,但是從體驗(yàn)的效果來看FormData和FileReader更好,因?yàn)檫@兩者不用生成一個(gè)無用的DOM再刪除,其中FormData最簡單,而FileReader更加靈活。
下面給大家介紹iframe無刷新上傳文件
form.html <form enctype="multipart/form-data" method="post" target="upload" action="upload.php" > <input type="file" name="uploadfile" /> <input type="submit" /> </form> <iframe name="upload" style="display:none"></iframe>
<!--和一般的<form>標(biāo)簽相比多了一個(gè)target屬性罷了,用于指定標(biāo)簽頁在哪里打開以及提交數(shù)據(jù)。
如果沒有設(shè)置該屬性,就會(huì)像平常一樣在本頁重定向打開action中的url。
而如果設(shè)置為iframe的name值,即"upload"的話,就會(huì)在該iframe內(nèi)打開,因?yàn)镃SS設(shè)置為隱藏,因而不會(huì)有任何動(dòng)靜。若將display:none去掉,還會(huì)看到服務(wù)器的返回信息。
-->
upload.php
<?php
header("Content-type:text/html;charset=utf-");
class upload{
public $_file;
public function __construct(){
if(!isset($_FILES['uploadfile'])){
$name=key($_FILES);
}
if(!isset($_FILES['uploadfile'])){
throw new Exception("并沒有文件上傳");
}
$this->_file=$_FILES['uploadfile']; //$this->_file一維數(shù)組
var_dump($this->_file);
//判斷文件是否是通過 HTTP POST 上傳的
//如果 filename 所給出的文件是通過 HTTP POST 上傳的則返回 TRUE。這可以用來確保惡意的用戶無法欺騙腳本去訪問本不能訪問的文件,例如 /etc/passwd。
if(!is_uploaded_file($this->_file['tmp_name']))
throw new Exception("異常情況");
if($this->_file['error'] !== )
throw new Exception("錯(cuò)誤代碼:".$this->_file['error']);
}
public function moveTo($new_dir){
$real_dir=$this->checkDir($new_dir).'/';
$real_dir=str_replace("\\","/",$real_dir);
if(!move_uploaded_file($this->_file['tmp_name'],$real_dir.$this->_file['name'])){
exit('上傳失敗');
}
echo "<script type='text/javascript'>alert('上傳成功')</script>";
}
public function checkDir($dir){
if(!file_exists($dir)){
mkdir($dir,,true);
}
return realpath($dir);
}
}
$upload=new upload();
$new_dir="./a/b";
$upload->moveTo($new_dir);
- JS中使用FormData上傳文件、圖片的方法
- jQuery用FormData實(shí)現(xiàn)文件上傳的方法
- jQuery Ajax使用FormData對象上傳文件的方法
- JS FormData上傳文件的設(shè)置方法
- jQuery+formdata實(shí)現(xiàn)上傳進(jìn)度特效遇到的問題
- 通過Ajax使用FormData對象無刷新上傳文件方法
- vuejs使用FormData實(shí)現(xiàn)ajax上傳圖片文件
- FormData+Ajax實(shí)現(xiàn)上傳進(jìn)度監(jiān)控
- jQuery Ajax使用FormData上傳文件和其他數(shù)據(jù)后端web.py獲取
- 使用FormData實(shí)現(xiàn)上傳多個(gè)文件
相關(guān)文章
用JavaScript對JSON進(jìn)行模式匹配(Part 1-設(shè)計(jì))
在《從 if else 到 switch case 再到抽象》這篇文章里面說到,解決 if else 和 switch case 分支過多的一個(gè)方法,就是做一個(gè)專用的 dispatcher ,讓它來負(fù)責(zé)進(jìn)行篩選與轉(zhuǎn)發(fā)。2010-07-07
webpack自動(dòng)引入打包資源HtmlWebpackPlugin的實(shí)現(xiàn)
本文主要介紹了webpack自動(dòng)引入打包資源HtmlWebpackPlugin的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
JS實(shí)現(xiàn)簡單的todoList(記事本)效果
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)簡單的todoList(記事本)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Axios?get?post請求傳遞參數(shù)的實(shí)現(xiàn)代碼
axios是基于promise用于瀏覽器和node.js的http客戶端,支持瀏覽器和node.js,能攔截請求和響應(yīng),這篇文章主要介紹了axios?get?post請求傳遞參數(shù)的操作代碼,需要的朋友可以參考下2022-11-11
JavaScript實(shí)現(xiàn)簡單計(jì)算器功能
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)簡單計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
javascript實(shí)現(xiàn)最長公共子序列實(shí)例代碼
最長公共子序列(longest common sequence)和最長公共子串(longest common substring)不是一回事兒,下面這篇文章主要給大家介紹了關(guān)于javascript實(shí)現(xiàn)最長公共子序列的相關(guān)資料,需要的朋友可以參考下。2018-02-02
JavaScript中實(shí)現(xiàn)鍵值對應(yīng)的字典與哈希表結(jié)構(gòu)的示例
字典或者哈希表這樣的鍵值對應(yīng)結(jié)構(gòu)在其他很多語言中都有內(nèi)置,非常好用,這里我們來看一下JavaScript中實(shí)現(xiàn)鍵值對應(yīng)的字典與哈希表結(jié)構(gòu)的示例:2016-06-06

