webbrowser实现input tab事件_如何合理构造一个Uploader工具类(设计到实现)
作者:Chaser (本文來自作者投稿)? ? ?
原文地址:https://juejin.im/post/5e5badce51882549652d55c2
源碼地址:https://github.com/impeiran/Blog/tree/master/uploader
前言
本文將帶你基于ES6的面向對象,脫離框架使用原生JS,從設計到代碼實現(xiàn)一個Uploader基礎類,再到實際投入使用。通過本文,你可以了解到一般情況下根據(jù)需求是如何合理構造出一個工具類lib。
需求描述
相信很多人都用過/寫過上傳的邏輯,無非就是創(chuàng)建input[type=file]標簽,監(jiān)聽onchange事件,添加到FormData發(fā)起請求。
但是,想引入開源的工具時覺得增加了許多體積且定制性不滿足,每次寫上傳邏輯又會寫很多冗余性代碼。在不同的toC業(yè)務上,還要重新編寫自己的上傳組件樣式。
此時編寫一個Uploader基礎類,供于業(yè)務組件二次封裝,就顯得很有必要。
下面我們來分析下使用場景與功能:
- 選擇文件后可根據(jù)配置,自動/手動上傳,定制化傳參數(shù)據(jù),接收返回。
- 可對選擇的文件進行控制,如:文件個數(shù),格式不符,超出大小限制等等。
- 操作已有文件,如:二次添加、失敗重傳、刪除等等。
- 提供上傳狀態(tài)反饋,如:上傳中的進度、上傳成功/失敗。
- 可用于拓展更多功能,如:拖拽上傳、圖片預覽、大文件分片等。
然后,我們可以根據(jù)需求,大概設計出想要的API效果,再根據(jù)API推導出內部實現(xiàn)。
可通過配置實例化
const uploader = new Uploader({url: '',
// 用于自動添加input標簽的容器
wrapper: null,
// 配置化的功能,多選、接受文件類型、自動上傳等等
multiple: true,
accept: '*',
limit: -1, // 文件個數(shù)
autoUpload: false
// xhr配置
header: {}, // 適用于JWT校驗
data: {} // 添加額外參數(shù)
withCredentials: false
});
狀態(tài)/事件監(jiān)聽
// 鏈式調用更優(yōu)雅uploader
.on('choose', files => {
// 用于接受選擇的文件,根據(jù)業(yè)務規(guī)則過濾
})
.on('change', files => {
// 添加、刪除文件時的觸發(fā)鉤子,用于更新視圖
// 發(fā)起請求后狀態(tài)改變也會觸發(fā)
})
.on('progress', e => {
// 回傳上傳進度
})
.on('success', ret => {/*...*/})
.on('error', ret => {/*...*/})
外部調用方法
這里主要暴露一些可能通過交互才觸發(fā)的功能,如選擇文件、手動上傳等
uploader.chooseFile();// 獨立出添加文件函數(shù),方便拓展
// 可傳入slice大文件后的數(shù)組、拖拽添加文件
uploader.loadFiles(files);
// 相關操作
uploader.removeFile(file);
uploader.clearFiles()
// 凡是涉及到動態(tài)添加dom,事件綁定
// 應該提供銷毀API
uploader.destroy();
至此,可以大概設計完我們想要的uploader的大致效果,接著根據(jù)API進行內部實現(xiàn)。
內部實現(xiàn)
使用ES6的class構建uploader類,把功能進行內部方法拆分,使用下劃線開頭標識內部方法。
然后可以給出以下大概的內部接口:
class Uploader {// 構造器,new的時候,合并默認配置
constructor (option = {}) {}
// 根據(jù)配置初始化,綁定事件
_init () {}
// 綁定鉤子與觸發(fā)
on (evt) {}
_callHook (evt) {}
// 交互方法
chooseFile () {}
loadFiles (files) {}
removeFile (file) {}
clear () {}
// 上傳處理
upload (file) {}
// 核心ajax發(fā)起請求
_post (file) {}
}
構造器 - constructor
代碼比較簡單,這里目標主要是定義默認參數(shù),進行參數(shù)合并,然后調用初始化函數(shù)
class Uploader {constructor (option = {}) {
const defaultOption = {
url: '',
// 若無聲明wrapper, 默認為body元素
wrapper: document.body,
multiple: false,
limit: -1,
autoUpload: true,
accept: '*',
headers: {},
data: {},
withCredentials: false
}
this.setting = Object.assign(defaultOption, option)
this._init()
}
}
初始化 - _init
這里初始化做了幾件事:維護一個內部文件數(shù)組uploadFiles,構建input標簽,綁定input標簽的事件,掛載dom。
為什么需要用一個數(shù)組去維護文件,因為從需求上看,我們的每個文件需要一個狀態(tài)去追蹤,所以我們選擇內部維護一個數(shù)組,而不是直接將文件對象交給上層邏輯。
由于邏輯比較混雜,分多了一個函數(shù)_initInputElement進行初始化input的屬性。
class Uploader {// ...
_init () {
this.uploadFiles = [];
this.input = this._initInputElement(this.setting);
// input的onchange事件處理函數(shù)
this.changeHandler = e => {
// ...
};
this.input.addEventListener('change', this.changeHandler);
this.setting.wrapper.appendChild(this.input);
}
_initInputElement (setting) {
const el = document.createElement('input');
Object.entries({
type: 'file',
accept: setting.accept,
multiple: setting.multiple,
hidden: true
}).forEach(([key, value]) => {
el[key] = value;
})''
return el;
}
}
看完上面的實現(xiàn),有兩點需要說明一下:
上文中的changeHanler,來單獨分析實現(xiàn),這里我們要讀取文件,響應實例choose事件,將文件列表作為參數(shù)傳遞給loadFiles。
為了更加貼合業(yè)務需求,可以通過事件返回結果來判斷是中斷,還是進入下一流程。
this.changeHandler = e => {const files = e.target.files;
const ret = this._callHook('choose', files);
if (ret !== false) {
this.loadFiles(ret || e.target.files);
}
};
通過這樣的實現(xiàn),如果顯式返回false,我們則不響應下一流程,否則拿返回結果||文件列表。這樣我們就將判斷格式不符,超出大小限制等等這樣的邏輯交給上層實現(xiàn),響應樣式控制。如以下例子:
uploader.on('choose', files => {const overSize = [].some.call(files, item => item.size > 1024 * 1024 * 10)
if (overSize) {
setTips('有文件超出大小限制')
return false;
}
return files;
});
狀態(tài)事件綁定與響應
簡單實現(xiàn)上文提到的_callHook,將事件掛載在實例屬性上。因為要涉及到單個choose事件結果控制。沒有按照標準的發(fā)布/訂閱模式的事件中心來做,有興趣的同學可以看看tiny-emitter的實現(xiàn)。
class Uploader {// ...
on (evt, cb) {
if (evt && typeof cb === 'function') {
this['on' + evt] = cb;
}
return this;
}
_callHook (evt, ...args) {
if (evt && this['on' + evt]) {
return this['on' + evt].apply(this, args);
}
return;
}
}
裝載文件列表 - loadFiles
傳進來文件列表參數(shù),判斷個數(shù)響應事件,其次就是要封裝出內部列表的數(shù)據(jù)格式,方便追蹤狀態(tài)和對應對象,這里我們要用一個外部變量生成id,再根據(jù)autoUpload參數(shù)選擇是否自動上傳。
let uid = 1class Uploader {
// ...
loadFiles (files) {
if (!files) return false;
if (this.limit !== -1 &&
files.length &&
files.length + this.uploadFiles.length > this.limit
) {
this._callHook('exceed', files);
return false;
}
// 構建約定的數(shù)據(jù)格式
this.uploadFiles = this.uploadFiles.concat([].map.call(files, file => {
return {
uid: uid++,
rawFile: file,
fileName: file.name,
size: file.size,
status: 'ready'
}
}))
this._callHook('change', this.uploadFiles);
this.setting.autoUpload && this.upload()
return true
}
}
到這里其實還沒完善,因為loadFiles可以用于別的場景下添加文件,我們再增加些許類型判斷代碼。
class Uploader {// ...
loadFiles (files) {
if (!files) return false;
+ const type = Object.prototype.toString.call(files)
+ if (type === '[object FileList]') {
+ files = [].slice.call(files)
+ } else if (type === '[object Object]' || type === '[object File]') {
+ files = [files]
+ }
if (this.limit !== -1 &&
files.length &&
files.length + this.uploadFiles.length > this.limit
) {
this._callHook('exceed', files);
return false;
}
+ this.uploadFiles = this.uploadFiles.concat(files.map(file => {
+ if (file.uid && file.rawFile) {
+ return file
+ } else {
return {
uid: uid++,
rawFile: file,
fileName: file.name,
size: file.size,
status: 'ready'
}
}
}))
this._callHook('change', this.uploadFiles);
this.setting.autoUpload && this.upload()
return true
}
}
上傳文件列表 - upload
這里可根據(jù)傳進來的參數(shù),判斷是上傳當前列表,還是單獨重傳一個,建議是每一個文件單獨走一次接口(有助于失敗時的文件追蹤)。
upload (file) {if (!this.uploadFiles.length && !file) return;
if (file) {
const target = this.uploadFiles.find(
item => item.uid === file.uid || item.uid === file
)
target && target.status !== 'success' && this._post(target)
} else {
this.uploadFiles.forEach(file => {
file.status === 'ready' && this._post(file)
})
}
}
當中涉及到的_post函數(shù),我們往下再單獨實現(xiàn)。
交互方法
這里都是些供給外部操作的方法,實現(xiàn)比較簡單就直接上代碼了。
class Uploader {// ...
chooseFile () {
// 每次都需要清空value,否則同一文件不觸發(fā)change
this.input.value = ''
this.input.click()
}
removeFile (file) {
const id = file.id || file
const index = this.uploadFiles.findIndex(item => item.id === id)
if (index > -1) {
this.uploadFiles.splice(index, 1)
this._callHook('change', this.uploadFiles);
}
}
clear () {
this.uploadFiles = []
this._callHook('change', this.uploadFiles);
}
destroy () {
this.input.removeEventHandler('change', this.changeHandler)
this.setting.wrapper.removeChild(this.input)
}
// ...
}
有一點要注意的是,主動調用chooseFile,需要在用戶交互之下才會觸發(fā)選擇文件框,就是說要在某個按鈕點擊事件回調里,進行調用chooseFile。否則會出現(xiàn)以下這樣的提示:
寫到這里,我們可以根據(jù)已有代碼嘗試一下,打印upload時的內部uploadList,結果正確。
發(fā)起請求 - _post
這個是比較關鍵的函數(shù),我們用原生XHR實現(xiàn),因為fetch并不支持progress事件。簡單描述下要做的事:
- onload事件:處理響應的狀態(tài),返回數(shù)據(jù)并改寫文件列表中的狀態(tài),響應外部change等相關狀態(tài)事件。
- onerror事件:處理錯誤狀態(tài),改寫文件列表,拋出錯誤,響應外部error事件
- onprogress事件:根據(jù)返回的事件,計算好百分比,響應外部onprogress事件
if (!file.rawFile) return
const { headers, data, withCredentials } = this.setting
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append('file', file.rawFile, file.fileName)
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key])
})
file.status = 'uploading'
xhr.withCredentials = !!withCredentials
xhr.onload = () => {
/* 處理響應 */
if (xhr.status < 200 || xhr.status >= 300) {
file.status = 'error'
this._callHook('error', parseError(xhr), file, this.uploadFiles)
} else {
file.status = 'success'
this._callHook('success', parseSuccess(xhr), file, this.uploadFiles)
}
}
xhr.onerror = e => {
/* 處理失敗 */
file.status = 'error'
this._callHook('error', parseError(xhr), file, this.uploadFiles)
}
xhr.upload.onprogress = e => {
/* 處理上傳進度 */
const { total, loaded } = e
e.percent = total > 0 ? loaded / total * 100 : 0
this._callHook('progress', e, file, this.uploadFiles)
}
xhr.open('post', this.setting.url, true)
xhr.send(formData)
}
parseSuccess
將響應體嘗試JSON反序列化,失敗的話再返回原樣文本
const parseSuccess = xhr => {let response = xhr.responseText
if (response) {
try {
return JSON.parse(response)
} catch (error) {}
}
return response
}
parseError
同樣的,JSON反序列化,此處還要拋出個錯誤,記錄錯誤信息。
const parseError = xhr => {let msg = ''
let { responseText, responseType, status, statusText } = xhr
if (!responseText && responseType === 'text') {
try {
msg = JSON.parse(responseText)
} catch (error) {
msg = responseText
}
} else {
msg = `${status} ${statusText}`
}
const err = new Error(msg)
err.status = status
return err
}
至此,一個完整的Upload類已經(jīng)構造完成,整合下來大概200行代碼多點,由于篇幅問題,完整的代碼已放在個人github里。
測試與實踐
寫好一個類,當然是上手實踐一下,由于測試代碼并不是本文關鍵,所以采用截圖的方式呈現(xiàn)。為了呈現(xiàn)良好的效果,把chrome里的network調成自定義降速,并在測試失敗重傳時,關閉網(wǎng)絡。
服務端
這里用node搭建了一個小的http服務器,用multiparty處理文件接收。
客戶端
簡單的用html結合vue實現(xiàn)了一下,會發(fā)現(xiàn)將業(yè)務代碼跟基礎代碼分開實現(xiàn)后,簡潔明了不少
拓展拖拽上傳
拖拽上傳注意兩個事情就是
更改客戶端代碼如下:
效果圖GIF
優(yōu)化與總結
本文涉及的全部源代碼以及測試代碼均已上傳到github倉庫中,有興趣的同學可自行查閱。
代碼當中還存在不少需要的優(yōu)化項以及爭論項,等待各位讀者去斟酌改良:
- 文件大小判斷是否應該結合到類里面?看需求,因為有時候可能會有根據(jù).zip壓縮包的文件,可以允許更大的體積。
- 是否應該提供可重寫ajax函數(shù)的配置項?
- 參數(shù)是否應該可傳入一個函數(shù)動態(tài)確定?
- ...
源碼地址:https://github.com/impeiran/Blog/tree/master/uploader
?? 看完三件事
大家好,我是 koala,如果你覺得這篇內容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個【在看】,或者分享轉發(fā),讓更多的人也能看到這篇內容
關注公眾號【程序員成長指北】,不定期分享原創(chuàng)&精品技術文章。
添加微信【?coder_qi?】。加入程序員成長指北公眾號交流群。
“在看轉發(fā)”是最大的支持
總結
以上是生活随笔為你收集整理的webbrowser实现input tab事件_如何合理构造一个Uploader工具类(设计到实现)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 虚拟局域网(VLAN)的管理
- 下一篇: 产品发布系统_【产品发布】第3期|阀门遥