說清楚文件上傳
dir:接收上傳內容的目錄
public:存放前端測試頁面的目錄
依賴:用到了express和formidable,分別用來搭建本地服務和提供/upload上傳介面
版本1:
對應HTML代碼
<!-- action="/upload":指定後端上傳介面 enctype="multipart/form-data":將文件統一轉成二進位的形式上傳 method:以POST形式進行上傳--><form action="/upload" enctype="multipart/form-data" method="post"> <!-- name屬性是必須的,給後端用的 --> <!-- multiple屬性代表支持多選上傳 --> <input type="file" name="fileInput" multiple> <br> <input type="submit" value="上傳"></form>
此時選擇文件後再點擊上傳按鈕,上傳成功,如下圖所示:
dir目錄也會多出文件,文件名字是後端可定義的,如下圖所示:
以上我們沒有用到任何JS代碼,就實現了最簡單的文件上傳。後端/upload介面的實現也很簡單,先不用關心,後面我會給出源碼展示並加以說明。
版本2:
版本1存在的問題:上傳成功後當前頁面「不見了」,我們希望的是上傳成功後在當前頁面也能拿到後端返回的信息,實際開發中也往往如此。我們對版本1的HTML代碼進行改進,實際上只是增加了iframe標籤,並把form的target屬性值指向iframe的name值即可。
對應HTML代碼
<iframe src="" frameborder="1" name="iframe"></iframe><!-- target 屬性規定在何處打開 action URL --><form action="/upload" enctype="multipart/form-data" method="post" target="iframe"> <input type="file" name="fileInput" multiple> <br> <input type="submit" value="上傳"></form>
我們選擇文件並點擊上傳按鈕後如下圖所示,可見後端返回的內容已經能在當前頁面接收了:
版本3:
這個版本要做的工作是:
- 隱藏iframe框,我們頁面並不需要它
- 把iframe框中數據提取出來
- 簡單封裝下代碼,假如頁面有N個上傳按鈕時方便使用
我們的想法是,點擊上圖中的上傳按鈕直接彈出系統文件選擇框,如下所示:
選中文件後,點擊打開按鈕直接開始上傳操作。大致的操作思路如下圖:
其中步驟3,我們用到了oFile.click()方法,IE8及以下瀏覽器是不支持的,也就是用戶必須手動點擊<input type="file">框才能進行文件選擇操作,解決辦法是:一開始在按鈕上覆蓋一個透明的<input type="file">框即可,不過我並不打算進行相關兼容方法的處理。上面思路換成代碼實現如下:
let fileUpload = (function () { function createIframe(opt) { let oIframe = document.createElement(iframe); oIframe.name = opt.iframeName; oIframe.style.display = "none";// none document.body.appendChild(oIframe); return oIframe; } function createForm(opt) { let oForm = document.createElement("form"); oForm.action = opt.action; oForm.target = opt.iframeName; oForm.enctype = "multipart/form-data"; oForm.method = "post"; oForm.style.display = "none";// none // 創建表單內的File框 let oFile = createFile(opt); oForm.appendChild(oFile); document.body.appendChild(oForm); return { oForm, oFile }; } function createFile(opt) { let oFile = document.createElement("input"); oFile.type = "file"; oFile.name = "fileInput"; oFile.multiple = true; oFile.accept = opt.type.join(,); return oFile; } function handleUpload(opt) { // 創建iframe let oIframe = createIframe(opt); // 創建表單 let obj = createForm(opt); // 插入完畢後執行點擊 obj.oFile.click(); // 監聽change,並執行submit提交 obj.oFile.addEventListener("change", () => obj.oForm.submit()); oIframe.addEventListener("load", opt.endFn); oIframe.addEventListener("load", function() { // 我們可不希望每點擊一次按鈕頁面都多出上面創建的標籤,所以拿到數據後把創建的標籤刪除 oIframe.remove(); obj.oForm.remove(); }); } function init(opt) { opt = opt || {}; // 可以設置下opt的默認值,做下容錯處理 handleUpload(opt); } return { init: init }})();let oBtn1 = document.querySelector("#btn1"), oBtn2 = document.querySelector("#btn2");let uploadOpt = { iframeName: iframe, // iframe name and form target action: /upload, // 上傳介面 type: [image/jpeg, image/png, video/mp4], // 接收上傳文件類型 endFn: function () { // 回調函數,接收上傳成功後端返回的數據 let oPre = this.contentDocument.querySelector("pre"); let data = JSON.parse(oPre.innerHTML) // 後端返回的數據data console.log(data); }};// 點擊上傳按鈕oBtn1.addEventListener("click", () => fileUpload.init(uploadOpt));oBtn2.addEventListener("click", () => fileUpload.init(uploadOpt));
版本4:
版本3點擊按鈕選擇文件打開後直接進行上傳操作,但對於上傳進度我們是不清楚的,在這個版本我們需要加上相關進度的展示。展示進度常用一下方式來實現:
- Flash
- 基於WebSocket長連接方式去實現
- 輪訓(前端不斷向後端請求上傳進度)
- 用AJAX實現文件上傳,AJAX提供有相關進度的介面(版本5中會用此方法進行改進)
以上方案各有優缺點,下面我用輪訓的方式實現下。簡單來說就是後端提供介面,前端每次請求實時返迴文件的上傳進度。就像下面的代碼,version_04.html中也有完整的代碼展示:
setInterval(lx, 500);function lx() { let xhr = new XMLHttpRequest; xhr.open(post, /progress); xhr.send(); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { oProgress.value = parseInt(xhr.responseText); (xhr.responseText === "100") && clearInterval(timer); } }}
版本5:
其實上面4個版本我們都是在利用form表單,然後submit幫我們自動上傳,這個版本我們嘗試用FormData的形式獲取上傳數據(關於什麼是FormData,參考MDN的說明,使用非常簡單),並結合AJAX的形式把數據傳給後台,這樣進度展示也可以利用AJAX提供的API在前端獲取。
代碼展示
function UploadFile() { this.oWrap = this.createEle("div"); this.oBtn = this.createEle("span");}// 創建標籤的函數UploadFile.prototype.createEle = function(ele) { return document.createElement(ele)};// 初始化頁面上的上傳按鈕UploadFile.prototype.initBtn = function(opt) { // btn this.oBtn.className = "btn"; this.oBtn.innerHTML = "上傳文件"; opt.cls && (this.oWrap.className = opt.cls); this.oWrap.appendChild(this.oBtn); document.body.appendChild(this.oWrap);};// 創建FILE上傳框和進度條UploadFile.prototype.initHtml = function(opt) { this.oFile = this.createEle("input"); this.oProgressWrap = this.createEle("div"); this.oProgressCon = this.createEle("span"); // file this.oFile.type = "file"; this.oFile.style.display = "none"; opt.multiple && (this.oFile.multiple = opt.multiple); // progress this.oProgressWrap.className = "progress-wrap"; this.oProgressCon.className = "progress-con"; this.oProgressWrap.appendChild(this.oProgressCon); opt.top && (this.oProgressCon.style.top = opt.top + "px"); this.oWrap.appendChild(this.oFile); this.oWrap.appendChild(this.oProgressWrap);};// 用AJAX把數據傳給後台UploadFile.prototype.ajax = function(opt) { let xhr = new XMLHttpRequest, _this = this; xhr.open(post, /upload); xhr.upload.addEventListener("progress", (ev) => { let percent = (ev.loaded / ev.total) * 100 + "%"; this.oProgressCon.style.width = percent; }); xhr.send(opt["formData"]); xhr.addEventListener("readystatechange", () => { if (xhr.readyState === 4 && xhr.status === 200) { opt.cb && opt.cb(JSON.parse(xhr.responseText)); } });};UploadFile.prototype.handle = function(opt) { let _this = this; this.oBtn.addEventListener("click", () => { // 下次點擊的時候把上次創建的刪除 this.oFile && this.oFile.remove(); this.oProgressWrap && this.oProgressWrap.remove(); // 每次點擊按鈕的時候把該創建的創建了,也可以在初始化的時候一次性把事辦齊 this.initHtml(opt); // 主動點擊,彈出系統上傳框 this.oFile.click(); // 監聽上傳框 this.oFile.addEventListener("change", () => { let formData = new FormData(); // 通過FormData對象可以組裝一組用 XMLHttpRequest發送請求的鍵/值對 formData.append(upload, _this.oFile.files[0]); opt["formData"] = formData; this.ajax(opt); }); });};UploadFile.prototype.init = function(opt) { opt = opt || {};// 這裡可以搞一下默認參數的配置 this.initBtn(opt); this.handle(opt);};/* 參數說明: top: 進度條距離窗口頂部距離 cls: 外包裹class名字 multiple: 是否支持多選 cb: 上傳成功的回調 accept: 支持上傳的文件格式(未做) 還有一些細節要處理: 例如上一個還沒傳完,我又點擊了相同的上傳按鈕這時候前端進度該如何處理 當碰到很大的文件時,我們是否考慮切割上傳 前端是不是最好也把上傳預覽做一下 先到這裡,有空再來完善...*/let btn1 = new UploadFile();let btn2 = new UploadFile();btn1.init({ top: 0, cls: "wrap", multiple: true, cb: function(data) { console.log(data); }});btn2.init({ top: "20"});
查看源碼
Photo by Ian Stauffer
推薦閱讀: