開發中會常常涉及到文件上傳的需求,根據業務不一樣的需求,有不一樣的文件上傳狀況。javascript
有簡單的單文件上傳,有多文件上傳,因瀏覽器原生的文件上傳樣式及功能的支持度不算過高,不少時候咱們會對樣式進行美化,對功能進行完善。php
本文根據一個例子,對多文件的上傳樣式作了一些簡單的美化(其實也沒怎麼美化。。),同時支持選擇文件後自定義刪除相關的文件,最後再上傳css
文章篇幅較長,先簡單看看圖示:html
目錄前端
最簡單的文件上傳,是單文件上傳,form標籤中加入enctype="multipart/form-data",form表單中有一個input[type="file"]項java
<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data"> <input type="text" name="user" id="user" placeholder="請輸入暱稱"> <input type="file" name="userImage" id="userImage"> <input type="submit" name="sub" value="提交"> </form>
<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data"> <input type="text" name="user" id="user" placeholder="請輸入暱稱"> <input type="file" name="userImage1" id="userImage1"> <input type="file" name="userImage2" id="userImage2"> <input type="file" name="userImage3" id="userImage3"> <input type="submit" name="sub" value="提交"> </form>
<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data"> <input type="text" name="user" id="user" placeholder="請輸入暱稱"> <input type="file" name="userImage" id="userImage" multiple> <input type="submit" name="sub" value="提交"> </form>
要注意的是,對於multiple這個新屬性,在IE9及如下版本中不被支持,在移動端安卓平臺下會忽略,也就是隻能選擇一個文件web
看了上面幾個圖片,能夠知道原生的文件選擇項樣式是最基本的,主要體如今三個點:ajax
基於幾個問題,能夠按需對其進行美化json
第一點能夠直接添加邊框的樣式bootstrap
第二點須要增添其餘元素,能夠新增一個按鈕(自行按需美化),將原始文件框隱藏,用JS事件綁定,點擊按鈕後模擬文件框的點擊
<input type="file" name="userImage" id="userImage" style="display: none;"> <input type="button" id="" value="選擇文件" onclick="document.getElementById('userImage').click()">
第三點與第二點相似,也得添加新的元素,選擇文件後,經過JS獲取選擇的文件信息,並在新的元素中顯示出來
想着很簡單,但隨之而來的問題就是,若是選中的文件數量不少,新元素佔空間的多少就是個問題,能夠默認顯示幾個文件,再經過「查看更多文件」查看到更多的信息
隨之另外的想法是,一次性選中的文件不少,想取消某個文件時,又得從新選擇。這未免太繁瑣,因此須要提供即時刪除某個選中文件的操做
要提供選中文件後可刪除的操做,就必然須要提供相關入口及腳本操做,下面圍繞這點來作些解析
選擇文件後,咱們能夠經過刪除按鈕刪除選中的文件,由於會出現多文件的狀況,因此須要一個信息模版
<!-- 當前選擇的文件列表 文件信息模版 --> <script type="text/template" id="file-temp-item-tpl"> <span class="file-temp-item" style="{{style}}"> <span class="file-temp-name">{{name}}</span> <span class="file-temp-btn">×</span> </span> </script>
選中的文件一多,就得再增添一個下拉框作輔助,最多顯示5個文件信息,而後經過下拉按鈕展開下拉框(按鈕樣式自行設定)
這裏5個文件間的位置計算的不是很到位,主要是這段代碼,能夠自行設定
// 計算每一項座標left、佔寬width left = i === 0 ? 2 : 2 + i * (100 / fileTempLen); width = 100 / fileTempLen - 2;
下拉列表裏面的每一項也是一個模版
<!-- 查看更多文件 文件信息模版 --> <script type="text/template" id="file-more-item-tpl"> <li> <span class="file-item-more-name">{{name}}</span> <span class="file-item-more-btn">×</span> </li> </script>
如下爲初始的HTML結構
<form name="form" id="form" method="post" action="fileTest.php" enctype="multipart/form-data"> <!-- <input type="number" name="numberTest" value="100"> --> <input type="file" name="fileTest[]" id="fileTest" multiple> <!-- 當前選擇的文件列表(最多顯示5條) --> <span class="file-temp"> </span> <!-- 查看更多文件 --> <ul class="item-more"> </ul> <input type="button" class="btn btn-success" id="uploadBtn" value="上傳"> <p class="upload-tip">文件上傳成功</p> </form>
如下爲所有CSS樣式
1 <link rel="stylesheet" type="text/css" href="bootstrap.min.css"> 2 <style type="text/css"> 3 html { 4 font-family: Arial; 5 } 6 7 form { 8 margin: 50px auto; 9 width: 400px; 10 } 11 12 input { 13 width: 300px; 14 padding: 4px; 15 } 16 17 #uploadBtn { 18 margin-top: -3px; 19 margin-left: 5px; 20 width: 60px; 21 height: 30px; 22 font-weight: bold; 23 font-size: 12px; 24 } 25 26 #fileTest { 27 display: inline-block; 28 border: 1px solid #ccc; 29 border-radius: 3px; 30 } 31 32 .file-temp { 33 position: relative; 34 display: none; 35 width: 300px; 36 height: 31px; 37 } 38 39 .file-temp-item { 40 position: absolute; 41 top: 4px; 42 height: 24px; 43 } 44 45 .item-more-btn { 46 display: inline-block; 47 position: absolute; 48 top: 18px; 49 right: 0.5%; 50 width: 10px; 51 height: 10px; 52 color: #777; 53 cursor: pointer; 54 } 55 56 .item-more-btn:hover { 57 border-top-color: #aaa; 58 } 59 60 .file-temp-name { 61 display: inline-block; 62 overflow: hidden; 63 width: 90%; 64 height: 26px; 65 padding: 2px 15px 2px 5px; 66 border-radius: 2px; 67 background-color: #eaeaf3; 68 text-overflow: ellipsis; 69 white-space: nowrap; 70 } 71 .file-temp-btn { 72 position: absolute; 73 display: inline-block; 74 top: 4px; 75 right: 11%; 76 width: 18px; 77 height: 18px; 78 line-height: 18px; 79 text-align: center; 80 border: 1px solid #ddd; 81 background-color: #ccc; 82 border-radius: 50%; 83 color: #fff; 84 font-size: 18px; 85 cursor: pointer; 86 } 87 88 .item-more { 89 position: absolute; 90 overflow-y: auto; 91 display: none; 92 padding-left: 0; 93 width: 300px; 94 max-height: 150px; 95 list-style: none; 96 } 97 98 .item-more li { 99 position: relative; 100 padding: 5px; 101 border: 1px solid #ccc; 102 border-top: none; 103 } 104 .item-more li:hover { 105 background-color: #f5f5f9; 106 } 107 108 .file-item-more-name { 109 display: inline-block; 110 width: 90%; 111 overflow: hidden; 112 text-overflow: ellipsis; 113 white-space: nowrap; 114 } 115 .file-item-more-btn { 116 position: absolute; 117 display: inline-block; 118 top: 8px; 119 right: 2%; 120 width: 18px; 121 height: 18px; 122 line-height: 18px; 123 text-align: center; 124 border: 1px solid #ddd; 125 background-color: #ddd; 126 border-radius: 50%; 127 color: #fff; 128 font-size: 18px; 129 cursor: pointer; 130 } 131 .file-item-more-btn:hover { 132 background-color: #ccc; 133 } 134 135 .upload-tip { 136 display: none; 137 margin: 50px auto; 138 text-align: center; 139 font-size: 12px; 140 } 141 </style>
下面,着重介紹JS腳本的處理
要獲取到選中文件的信息,天然想到用value屬性,但經過文件項的value只能獲取到一個文件路徑(第一個),不管有沒有multiple
無multiple
<input type="file" onchange="console.log(this.value);">
有multiple
<input type="file" multiple onchange="console.log(this.value);">
既然直接經過value獲取不到全部選中的文件信息,只能尋求其餘途徑。
獲取選中的文件信息,還能夠用FileList對象,這是在HTML5中新增的,每一個表單文件項都有個files屬性,裏邊存儲這選中的文件的一些信息
<input type="file" multiple onchange="console.log(this.files);">
選中兩個文件後,查看文件信息
FileList對象看起來是個類數組,有length屬性。因此咱們應該能夠經過修改或刪除相關的項來自定義咱們選擇的文件(注意其實這是不能修改的,且繼續看下去)
假如我選擇了兩個文件,想刪除第二項目,使用splice刪除,則
<input type="file" multiple onchange="console.log(Array.prototype.splice.call(this.files, 1, 1));">
報錯,由此可知FileList的length屬性是隻讀的,那直接修改成可寫可配置呢
Object.defineProperty(FileList.prototype, 'length', { writable: true, configurable: true });
配置以後length能修改了,乍一看還覺得splice生效了,然而輸出一看,FileList對象內容不變,仍爲兩項
查閱了一些資料後,瞭解到瀏覽器爲了安全性的考慮,把FileList對象的內容設爲了避免可更改,只能夠手動置空,但不能修改內容
因此,解決辦法是,新增一個數組,初始複製FileList對象的文件內容,以後的修改操做則經過這個可更改的數組進行
// 存儲更新所選文件 var curFiles = []; ... // 選中文件後 var files = this.files; if (files && files.length) { // 原始FileList對象不可更改,因此將其賦予curFiles提供接下來的修改 Array.prototype.push.apply(curFiles, files); }
假如點擊了刪除叉叉,能夠直接更新文件信息數組
var name = $(this).prev().text(); // 去除該文件 curFiles = curFiles.filter(function(file) { return file.name !== name; });
這樣一來,更新文件信息的問題獲得解決,而後就能夠進行文件的上傳了
點擊文件上傳,若是直接調用$form.submit(); 則上傳的文件信息依然是初始的FileList對象,達不到咱們自定義的要求,因此須要用Ajax提交
那麼,該怎麼想後臺提供一個文件對象呢?
HTML5引入了表單的新對象FormData, 它能夠生成一個表單對象,咱們能夠向其中獲取/設置鍵值對信息,再一併提交給後臺
引用MDN的FormData使用方法,咱們能夠添加各類類型的數據,使用ajax提交
var oMyForm = new FormData(); oMyForm.append("username", "Groucho"); oMyForm.append("accountnum", 123456); // 數字123456被當即轉換成字符串"123456" // fileInputElement中已經包含了用戶所選擇的文件 oMyForm.append("userfile", fileInputElement.files[0]); var oFileBody = '<a id="a"><b id="b">hey!</b></a>'; // Blob對象包含的文件內容 var oBlob = new Blob([oFileBody], { type: "text/xml"}); oMyForm.append("webmasterfile", oBlob); var oReq = new XMLHttpRequest(); oReq.open("POST", "http://foo.com/submitform.php"); oReq.send(oMyForm);
也可以使用JQ的封裝的ajax,不過要注意設置processData和contentType屬性爲false,防止JQ胡亂解析文件格式
var fd = new FormData(document.getElementById("fileinfo")); // 使用某個表單做爲初始項 fd.append("CustomField", "This is some extra data"); $.ajax({ url: "stash.php", type: "POST", data: fd, processData: false, // 告訴jQuery不要去處理髮送的數據 contentType: false // 告訴jQuery不要去設置Content-Type請求頭 });
這裏有幾個要注意的點:
1)FormData中的屬性值接受的是單個文件信息,不能是複合性的對象。可能表意不明,且看
var fd = new FormData($('#form')[0]); fd.append('myFileTest', curFiles);
$files = $_REQUEST['myFileTest']; var_dump($files);
用PHP接收傳過來的數據,數據卻被直接轉換成字符串了,非文件對象
curFiles是文件對象,那PHP端是否是應該用$_FILES來接收信息呢,試試換成$files = $_FILES['myFileTest'];
直接出問題了,說明不能這樣處理,須要將curFiles內容一項一項拆開,即單個文件信息
var fd = new FormData($('#form')[0]); for (var i = 0, j = curFiles.length; i < j; ++i) { fd.append('myFileTest[]', curFiles[i]); }
$files = $_FILES['myFileTest']; var_dump($files);
文件接收成功,接下來就能夠按需進行文件的操做了
2)後端獲取文件信息的時候,是直接經過原始$_FILES獲取的,其餘通常的信息才用$_REQUEST獲取
換成$files = $_REQUEST['myFileTest'];試試,直接就是出現找不到myFileTest的問題
試試添加通常的文件再提交
var fd = new FormData($('#form')[0]); for (var i = 0, j = curFiles.length; i < j; ++i) { fd.append('myFileTest[]', curFiles[i]); } fd.append('myTest', [1, 2, 3]);
$files = $_FILES['myFileTest']; $test = $_REQUEST['myTest']; var_dump($test); var_dump($files);
3)若是須要multiple的多文件上傳,則須要在文件項的文件後添加[]號,表示這是一個多文件的數組,以供後端處理解析
fd.append('myFileTest[]', curFiles[i]);
若是沒有後面的[],則連續的append會直接覆蓋原來的,最後後端獲取到的只是最後append進去的項
4)不要直接在JQ的ajax中實例化出一個FormData對象,會出問題
直接在data屬性中生成FormData對象,會被JQ忽略,因此後端什麼信息也拿不到
混合表單項簡單的例子:
在表單處理中,不少時候咱們會進行文件上傳和其餘基礎項的提交,簡單地多加一個input項目,看看是否處理成功
<input type="number" name="numberTest" value="100">
<?php $files = $_FILES['myFileTest']; $test = $_REQUEST['numberTest']; echo json_encode(array( 'len' => count($files['name']), 'num' => $test )); ?>
如下爲所有的JS腳本:
1 <script type="text/javascript"> 2 /** 3 * 向文件列表元素中添加相應的文件項 4 * @param {Array} files 當前的文件列表數組對象 5 */ 6 function addItem(files) { 7 var fileTempItemTpl = $('#file-temp-item-tpl').html(), 8 fileMoreItemTpl = $('#file-more-item-tpl').html() 9 htmlTemp = [], 10 htmlMoreTemp = [], 11 // 文件列表中各文件座標位置及所佔空間 12 left = 2, 13 width = 100, 14 // 最多取前5個文件 15 fileTempLen = files.length > 5 ? 5 : files.length; 16 17 for (var i = 0, j = files.length; i < j; ++i) { 18 // 當i > 4,即第6個文件開始 19 if (i > 4) { 20 htmlMoreTemp.push(fileMoreItemTpl.replace('{{name}}', files[i].name)); 21 continue; 22 } 23 24 // 計算每一項座標left、佔寬width 25 left = i === 0 ? 2 : 2 + i * (100 / fileTempLen); 26 width = 100 / fileTempLen - 2; 27 28 htmlTemp.push(fileTempItemTpl 29 .replace('{{style}}', 'left: ' + left + '%;width: ' + width + '%;') 30 .replace('{{name}}', files[i].name) 31 ); 32 } 33 34 // 渲染相關元素內容 35 $('.file-temp').html('' 36 + '<input type="text" style="background-color:#fff;" class="form-control" id="fileTemp" readonly>' 37 + htmlTemp.join('') 38 + (files.length > 5 39 ? '<span class="item-more-btn" title="查看更多">=</span>' 40 : '' 41 ) 42 ); 43 44 $('.item-more').html(htmlMoreTemp.join('')); 45 } 46 47 // 保存當前選擇的(更新後)文件列表 48 var curFiles = []; 49 50 // 初始選擇文件時觸發 51 $('#fileTest').change(function() { 52 var $this = $(this), 53 $temp = $('.file-temp'), 54 files = this.files; 55 56 if (files && files.length) { 57 // 原始FileList對象不可更改,因此將其賦予curFiles提供接下來的修改 58 Array.prototype.push.apply(curFiles, files); 59 addItem(curFiles); 60 61 $this.hide(); 62 $temp.css('display', 'inline-block'); 63 } 64 }); 65 66 $(document) 67 // 取消選擇某個文件時,在文件列表數組對象中刪除這個值,並更新列表 68 .on('click', '.file-temp-btn, .file-item-more-btn', function() { 69 $('.upload-tip').hide(); 70 var name = $(this).prev().text(); 71 // 去除該文件 72 curFiles = curFiles.filter(function(file) { 73 return file.name !== name; 74 }); 75 // 文件列表數組對象長度大於5才顯示「更多文件列表」下拉項 76 if (curFiles.length <= 5) { 77 $('.item-more').hide(); 78 } 79 // 文件列表數組被清空則重置文件選擇表單項 80 if (!curFiles.length) { 81 $('#fileTest').val('').show(); 82 $('.file-temp').css('display', 'none'); 83 } else { 84 addItem(curFiles); 85 } 86 87 console.log(curFiles) 88 }) 89 // 顯示「更多文件列表」下拉項 90 .on('click', '.item-more-btn', function() { 91 $('.upload-tip').hide(); 92 $('.item-more').show('normal'); 93 }); 94 95 // 上傳操做 96 $('#uploadBtn').click(function() { 97 $('.upload-tip').hide(); 98 99 // 構建FormData對象 100 var fd = new FormData($('#form')[0]); 101 for (var i = 0, j = curFiles.length; i < j; ++i) { 102 fd.append('myFileTest[]', curFiles[i]); 103 } 104 105 $.ajax({ 106 url: 'fileTest.php', 107 type: 'post', 108 data: fd, 109 processData: false, 110 contentType: false, 111 success: function(rs) { 112 rs = JSON.parse(rs); 113 $('.upload-tip') 114 .addClass('text-success') 115 .removeClass('text-error') 116 .text(rs.len + '個文件上傳成功, number項值爲' + rs.num) 117 .show(); 118 }, 119 error: function(err) { 120 121 } 122 }); 123 }); 124 </script>
後記:
另外,能夠考慮給文件上傳增長進度條
而當文件太大時,會由於後端支持的最大文件size不夠形成崩潰,這時能夠考慮進行斷點續傳
斷點續傳,前端能夠經過二進制流和本地存儲的結合來實現,這裏就很少說了