webbrowser實現input tab事件_如何合理構造一個Uploader工具類(設計到實現)

作者:Chaser (本文來自作者投稿)? ? ?

原文地址:https://juejin.im/post/5e5badce51882549652d55c2

源碼地址:https://github.com/impeiran/Blog/tree/master/uploader

前言

本文將帶你基于ES6的面向對象,脫離框架使用原生JS,從設計到代碼實現一個Uploader基礎類,再到實際投入使用。通過本文,你可以了解到一般情況下根據需求是如何合理構造出一個工具類lib。

需求描述

相信很多人都用過/寫過上傳的邏輯,無非就是創建input[type=file]標簽,監聽onchange事件,添加到FormData發起請求。

但是,想引入開源的工具時覺得增加了許多體積且定制性不滿足,每次寫上傳邏輯又會寫很多冗余性代碼。在不同的toC業務上,還要重新編寫自己的上傳組件樣式。

此時編寫一個Uploader基礎類,供于業務組件二次封裝,就顯得很有必要。

下面我們來分析下使用場景與功能:

  • 選擇文件后可根據配置,自動/手動上傳,定制化傳參數據,接收返回。
  • 可對選擇的文件進行控制,如:文件個數,格式不符,超出大小限制等等。
  • 操作已有文件,如:二次添加、失敗重傳、刪除等等。
  • 提供上傳狀態反饋,如:上傳中的進度、上傳成功/失敗。
  • 可用于拓展更多功能,如:拖拽上傳、圖片預覽、大文件分片等。

然后,我們可以根據需求,大概設計出想要的API效果,再根據API推導出內部實現。

可通過配置實例化

const uploader = new Uploader({
url: '',
// 用于自動添加input標簽的容器
wrapper: null,

// 配置化的功能,多選、接受文件類型、自動上傳等等
multiple: true,
accept: '*',
limit: -1, // 文件個數
autoUpload: false

// xhr配置
header: {}, // 適用于JWT校驗
data: {} // 添加額外參數
withCredentials: false
});

狀態/事件監聽

// 鏈式調用更優雅
uploader
.on('choose', files => {
// 用于接受選擇的文件,根據業務規則過濾
})
.on('change', files => {
// 添加、刪除文件時的觸發鉤子,用于更新視圖
// 發起請求后狀態改變也會觸發
})
.on('progress', e => {
// 回傳上傳進度
})
.on('success', ret => {/*...*/})
.on('error', ret => {/*...*/})

外部調用方法

這里主要暴露一些可能通過交互才觸發的功能,如選擇文件、手動上傳等

uploader.chooseFile();

// 獨立出添加文件函數,方便拓展
// 可傳入slice大文件后的數組、拖拽添加文件
uploader.loadFiles(files);

// 相關操作
uploader.removeFile(file);
uploader.clearFiles()

// 凡是涉及到動態添加dom,事件綁定
// 應該提供銷毀API
uploader.destroy();

至此,可以大概設計完我們想要的uploader的大致效果,接著根據API進行內部實現。

內部實現

使用ES6的class構建uploader類,把功能進行內部方法拆分,使用下劃線開頭標識內部方法。

然后可以給出以下大概的內部接口:

class Uploader {
// 構造器,new的時候,合并默認配置
constructor (option = {}) {}
// 根據配置初始化,綁定事件
_init () {}

// 綁定鉤子與觸發
on (evt) {}
_callHook (evt) {}

// 交互方法
chooseFile () {}
loadFiles (files) {}
removeFile (file) {}
clear () {}

// 上傳處理
upload (file) {}
// 核心ajax發起請求
_post (file) {}
}

構造器 - constructor

代碼比較簡單,這里目標主要是定義默認參數,進行參數合并,然后調用初始化函數

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

這里初始化做了幾件事:維護一個內部文件數組uploadFiles,構建input標簽,綁定input標簽的事件,掛載dom。

為什么需要用一個數組去維護文件,因為從需求上看,我們的每個文件需要一個狀態去追蹤,所以我們選擇內部維護一個數組,而不是直接將文件對象交給上層邏輯。

由于邏輯比較混雜,分多了一個函數_initInputElement進行初始化input的屬性。

class Uploader {
// ...

_init () {
this.uploadFiles = [];
this.input = this._initInputElement(this.setting);
// input的onchange事件處理函數
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;
}
}

看完上面的實現,有兩點需要說明一下:

  1. 為了考慮到destroy()的實現,我們需要在this屬性上暫存input標簽與綁定的事件。后續方便直接取來,解綁事件與去除dom。
  2. 其實把input事件函數changeHandler單獨抽離出去也可以,更方便維護。但是會有this指向問題,因為handler里我們希望將this指向本身實例,若抽離出去就需要使用bind綁定一下當前上下文。

上文中的changeHanler,來單獨分析實現,這里我們要讀取文件,響應實例choose事件,將文件列表作為參數傳遞給loadFiles

為了更加貼合業務需求,可以通過事件返回結果來判斷是中斷,還是進入下一流程。

this.changeHandler = e => {
const files = e.target.files;
const ret = this._callHook('choose', files);
if (ret !== false) {
this.loadFiles(ret || e.target.files);
}
};

通過這樣的實現,如果顯式返回false,我們則不響應下一流程,否則拿返回結果||文件列表。這樣我們就將判斷格式不符,超出大小限制等等這樣的邏輯交給上層實現,響應樣式控制。如以下例子:

uploader.on('choose', files => {
const overSize = [].some.call(files, item => item.size > 1024 * 1024 * 10)
if (overSize) {
setTips('有文件超出大小限制')
return false;
}
return files;
});

狀態事件綁定與響應

簡單實現上文提到的_callHook,將事件掛載在實例屬性上。因為要涉及到單個choose事件結果控制。沒有按照標準的發布/訂閱模式的事件中心來做,有興趣的同學可以看看tiny-emitter的實現。

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

傳進來文件列表參數,判斷個數響應事件,其次就是要封裝出內部列表的數據格式,方便追蹤狀態和對應對象,這里我們要用一個外部變量生成id,再根據autoUpload參數選擇是否自動上傳。

let uid = 1

class 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;
}
// 構建約定的數據格式
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

這里可根據傳進來的參數,判斷是上傳當前列表,還是單獨重傳一個,建議是每一個文件單獨走一次接口(有助于失敗時的文件追蹤)。

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函數,我們往下再單獨實現。

交互方法

這里都是些供給外部操作的方法,實現比較簡單就直接上代碼了。

class Uploader {
// ...
chooseFile () {
// 每次都需要清空value,否則同一文件不觸發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,需要在用戶交互之下才會觸發選擇文件框,就是說要在某個按鈕點擊事件回調里,進行調用chooseFile。否則會出現以下這樣的提示:

c329e6e563c82afbb559fc92e059983f.png

寫到這里,我們可以根據已有代碼嘗試一下,打印upload時的內部uploadList,結果正確。

dd8f4695a90fbdc6bd94520f8d84cffb.png

發起請求 - _post

這個是比較關鍵的函數,我們用原生XHR實現,因為fetch并不支持progress事件。簡單描述下要做的事:

  1. 構建FormData,將文件與配置中的data進行添加。
  2. 構建xhr,設置配置中的header、withCredentials,配置相關事件
  • onload事件:處理響應的狀態,返回數據并改寫文件列表中的狀態,響應外部change等相關狀態事件。
  • onerror事件:處理錯誤狀態,改寫文件列表,拋出錯誤,響應外部error事件
  • onprogress事件:根據返回的事件,計算好百分比,響應外部onprogress事件
  1. 因為xhr的返回格式不太友好,我們需要額外編寫兩個函數處理http響應:parseSuccessparseError
_post (file) {
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類已經構造完成,整合下來大概200行代碼多點,由于篇幅問題,完整的代碼已放在個人github里。

測試與實踐

寫好一個類,當然是上手實踐一下,由于測試代碼并不是本文關鍵,所以采用截圖的方式呈現。為了呈現良好的效果,把chrome里的network調成自定義降速,并在測試失敗重傳時,關閉網絡。

447528f6f9d804fdce2d3b3f13504684.gif

服務端

這里用node搭建了一個小的http服務器,用multiparty處理文件接收。

bf1d0c10dddfe9bf75a5d2c317d87271.png

客戶端

簡單的用html結合vue實現了一下,會發現將業務代碼跟基礎代碼分開實現后,簡潔明了不少

15922e8d6c27564949af9625be1c3786.png

拓展拖拽上傳

拖拽上傳注意兩個事情就是

  1. 監聽drop事件,獲取e.dataTransfer.files
  2. 監聽dragover事件,并執行preventDefault(),防止瀏覽器彈窗。
更改客戶端代碼如下:
cdb016a3027b86a865b196566164ab91.png
效果圖GIF
8fd81fb485e6ccd12440260a5105e879.gif

優化與總結

本文涉及的全部源代碼以及測試代碼均已上傳到github倉庫中,有興趣的同學可自行查閱。

代碼當中還存在不少需要的優化項以及爭論項,等待各位讀者去斟酌改良:

  • 文件大小判斷是否應該結合到類里面?看需求,因為有時候可能會有根據.zip壓縮包的文件,可以允許更大的體積。
  • 是否應該提供可重寫ajax函數的配置項?
  • 參數是否應該可傳入一個函數動態確定?
  • ...

源碼地址:https://github.com/impeiran/Blog/tree/master/uploader

?? 看完三件事

大家好,我是 koala,如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:

  • 點個【在看】,或者分享轉發,讓更多的人也能看到這篇內容

  • 關注公眾號【程序員成長指北】,不定期分享原創&精品技術文章。

  • 添加微信【?coder_qi?】。加入程序員成長指北公眾號交流群。

41a3790709cdf9cb6ea3d844b8d34559.png

“在看轉發”是最大的支持

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/537518.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/537518.shtml
英文地址,請注明出處:http://en.pswp.cn/news/537518.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

小達人點讀筆的任我貼貼紙怎么使用?

小達人點讀筆是一款高性能、高容量點讀筆。其自身的開放性、共享性、傳播性讓可讀書籍及音頻資源真正的達到了海量且優質有用。 下面我們來認識一下標配中任我貼上的貼紙&#xff1a; 智能貼 用于已經布有二維碼的有聲圖書的封面&#xff0c;比如律動英語&#xff0c;成長一線&…

vscode必備插件_10個必備的Visual Studio Code (VS code)插件

我基本上每天都用vs code&#xff0c;我喜歡的小巧&#xff0c;開源&#xff0c;免費并且非常強大。尤其它非常多的插件可以免費使用。這里我把經常用插件介紹給大家&#xff0c;希望大家留言討論我把VS code 插件分兩類&#xff0c;一類是處理可視化的&#xff0c;比如說顏色&…

軟考官方教材:信息系統項目管理師教程(第三版)

2020年下半年信息系統項目管理師考試官方教材將繼續使用清華大學出版社出版的信息系統項目管理師教程第3版&#xff08;特別注意&#xff0c;有某些輔導資料也打著信息系統項目管理師教程第3版的旗號&#xff0c;各位考生一定要看準官方指定教材是由全國計算機專業技術資格考試…

centos掛載windows共享目錄

2019獨角獸企業重金招聘Python工程師標準>>> 在windows中創建一個共享文件夾記住這個網絡路徑&#xff0c; 在centos上新建文件夾/mnt/MyShare $> mkdir /mnt/MyShare掛載 username用戶名.password登錄密碼 $> Sudo mount -t cifs -o username用戶名,pass…

博閱likebook alita專用pdf制作

適合博閱likebook alita帶手寫的。 先上效果圖&#xff0c;左邊是正文&#xff0c;右邊有留白可以寫讀書筆記 這樣做有什么好處&#xff1f; pdf從閱讀器里復制出來&#xff0c;在電腦打開&#xff0c;筆記也會有。筆記是直接記錄在pdf上的&#xff0c;相當于pdf上的批注。 …

esc鍵沒反應_有機人名反應——Brown 硼氫化反應(Brown Hydroboration)

Brown 硼氫化反應&#xff08;Brown Hydroboration&#xff09;反應機理鏈接&#xff1a;http://chem.kingdraw.cn/Shortlink?id20200624161301Brown硼氫化反應&#xff0c;是指乙硼烷在醚類溶液中離解成的甲硼烷以B-H鍵與烯烴、炔烴的不飽和鍵加成&#xff0c;生成有機硼化合…

利用IDisposable接口構建包含非托管資源對象

托管資源與非托管資源 在.net中&#xff0c;對象使用的資源分為兩種&#xff1a;托管資源與非托管資源。托管資源由CLR進行管理&#xff0c;不需要開發人員去人工進行控制&#xff0c;.NET中托管資源主要指“對象在堆中的內存”&#xff1b;非托管資源指對象使用到的一些托管內…

修改Navicat數據庫自動備份目錄

1.右鍵連接&#xff0c;選擇“編輯連接” 2. 選擇“高級”&#xff0c;設置位置

python圖形界面編程庫_Python支持哪些圖形界面的第三方庫

Python支持哪些圖形界面的第三方庫 發布時間&#xff1a;2020-11-09 10:37:56 來源&#xff1a;億速云 閱讀&#xff1a;58 作者&#xff1a;小新 這篇文章給大家分享的是有關Python支持哪些圖形界面的第三方庫的內容。小編覺得挺實用的&#xff0c;因此分享給大家做個參考。一…

成為中國特色項目經理,走上人生巔峰

今天是秋分&#xff0c;寫在項目經理6周年的總結 落葉知秋&#xff0c;情誼如酒&#xff0c;風漸涼時有喜無憂&#xff1b; 歲月流走&#xff0c;驀然回首&#xff0c;一聲問候醇綿依舊&#xff1b; 有情相守&#xff0c;不離左右&#xff0c;含笑送出這份問候。 -----------…

python中括號的作用_Python3--中括號[]與冒號:在列表中的作用

先來定義兩個列表: liststr ["helloworld","hahahh","123456"] listnum [1,2,3,4,5,6] 這兩個列表都可以看懂吧,一個字符串組成的列表,一個數字組成的列表 中括號"[]"的作用 : 用于定義列表或引用列表、數組、字符串及元組中元素位置…

相約11月25日,開發者的嘉年華

》》廈門GDG DevFest 2018 2018 年 11 月 25 日&#xff0c;大家期待已久的廈門GDG DevFest 2018 將于廈門大學科藝中心隆重舉行&#xff01; 此次活動主題是時下最受關注的人工智能領域&#xff0c;邀請數位重量級嘉賓&#xff0c;帶來關于 TensorFlow、Android等最新技術內容…

項目經理到底要不要懂技術?

不難發現&#xff0c;高薪項目經理崗位&#xff0c;往往對項目經理有技術要求。為什么會這樣&#xff1f; 存在即合理&#xff0c;一定是現實中需要&#xff0c;項目實施過程中有必要。 想起了自己以前有次面試&#xff0c;二面是公司總經理&#xff0c;總經理說&#xff1a;…

搭建基于C#和 Appium 的 Android自動測試環境

移動端的自動化測試框架主要就是UiAutomator&#xff0c;Espresso&#xff0c;Robotium和功能最強大&#xff0c;也是比較熱門的框架Appium如果想做手機端的自動化測試&#xff0c;Appium是首選的測試框架&#xff0c;因為網上使用的人多&#xff0c;資料豐富&#xff0c;支持語…

git 可視化工具_Git的基本使用(二)

通過前文Git的基本使用(一)的學習&#xff0c;相信大家對如何將iOS項目通過Git傳到GitHub賬戶上有了一個基本的了解&#xff0c;其過程是相對繁瑣和容易出錯的。本文將告訴大家借助工具來幫助我們實現這些操作&#xff0c;并對前文進行進一步補充。一、兩個軟件Visual Studio C…

小米8配哪個版本的MIUI?

小米8之前我是忠實的魅粉&#xff0c;魅族手機用過M8&#xff0c;MX3&#xff0c;note5&#xff0c;MX6 pro。 魅族真的是被聯發科的cpu坑了&#xff0c;一核有難&#xff0c;八核圍觀。 入手小米8&#xff0c;miui是9&#xff0c;一路跟著系統自動更新&#xff0c;更新到了12。…

3D設計桌面云

榕力3D設計桌面云方案&#xff0c;為使用GPU高負載應用的設計師們提供強大支持&#xff0c;如3D建模、光線追蹤、渲染計算等領域&#xff0c;帶來極致流暢的3D設計桌面體驗&#xff0c;同時提升工作效率、可管理性和安全性。 3D設計桌面云方案滿足了工業客戶在專業設計場景中的…

PDF圖片文字識別

工具 adobe acrobat XI PRO 沒有安裝該軟件的&#xff0c;需下載安裝&#xff0c;有了此軟件&#xff0c;pdf隨意改&#xff0c;必備&#xff01; 步驟 1.打開pdf文件&#xff0c;點擊工具---文本識別---在本文件中 2.選擇“所有頁面”&#xff0c;點擊“編輯” 3.選擇語言&a…

python中import的作用_python使用import報錯是什么原因

在練習Python中package的相對導入時&#xff0c;即from . import XXX 或者from .. import XXX 有時會遇到這樣兩個錯誤&#xff1a;SystemError: Parent module not loaded, cannot perform relative import 和ValueError: attempted relative import beyond top-level packag…

win10卓越性能模式,提升電腦性能

Win10隱藏了一個電源模式“卓越性能”&#xff0c;是比“高性能”模式更強性能的電源模式。 這個模式開啟后&#xff0c;電腦到底有沒有變快呢&#xff1f;心理上是快了。 開啟步驟 1.鼠標移到開始按鈕圖標上&#xff0c;別動&#xff01;&#xff01;&#xff01;&#xff…