webuploader是百度fex團隊開發的一個十分便捷的上傳插件,可是咱們在實際生產中,會發現使用它與咱們的需求有各類各樣的出入。最近作上傳功能,踩了很多坑,如今來記錄一下。若是個人文章中有任何不妥或者不對的地方,歡迎指正。javascript
上圖是翻自網宿雲的文檔的分片上傳流程。html
經過該圖,咱們可知網宿雲組織上傳文件形式是vue
{文件[塊1(分片1,分片2,分片3,…),塊2,塊3,…]}html5
而webuploader對文件分片的形式以下java
{文件[塊1(分片1),塊2(分片1),塊3(分片1),…]}jquery
即一塊便是一片。鑑於網宿雲的上傳一片一塊在邏輯上沒毛病,咱們一樣能一塊一塊完成上傳git
這裏注意,請仔細看網宿雲或七牛雲分片上傳的文檔,瞭解如何分片上傳。其中一個很重要的概念是塊,片上下文,即ctx,請前往查看github
咱們先來看webuploader一個文件上傳流程中,觸發的鉤子和事件web
一個文件的上傳只觸發三個實際使用的鉤子api
1. before-send-file 上傳文件前 2. before-send 上傳塊前 3. after-send-file 上傳文件結束
觸發多個事件
1. uploadStart 開始上傳前 2. uploadAccept 驗證上傳是否合法的事件,取ctx只能在這一步進行,比較悲慘 3. uploadBeforeSend 上傳文件前,對應before-send-file 4. uploadProgress 文件上傳進度事件 5. uploadSkip 跳過當前文件上傳事件,當出現該事件,uploader內部標記該文件已經上傳成功 6. stopUpload 暫停當前文件上傳時觸發 7. startUpload 恢復上傳當前文件觸發,或開始上傳也會觸發 8. uploadSuccess 文件上傳成功觸發 9. uploadError 文件上傳失敗觸發
經過比對網宿雲的分片上傳流程,咱們會發現他遠遠不知足咱們當下需求,缺乏上傳分片前的鉤子,缺乏上傳分片後的鉤子,這是不一樣的分片姿式決定的,目前來講除非咱們本身修改widgets/upload模塊,要不沒什麼好的方式解決他
因此下面是修改該模塊的內容
// 負責將文件切片。 function CuteFile( file, chunkSize ) { ... // 七牛雲,網宿雲規定的最大的塊的大小,chunkSize不能大於它 var blockSize = 4 * 1024 * 1024 while ( index < chunks ) { len = Math.min( chunkSize, total - start ); let block = { file: file, start: start, end: chunkSize ? (start + len) : total, total: total, chunks: chunks, chunk: index, cuted: api } // 增長塊id block.blockIndex = Math.floor(block.start / blockSize); // 增長塊內片偏移量標識 block.offset = block.start % blockSize; // 增長塊內最後一片標識(網宿雲要求在組合文件的時候,須要用每塊最後一片上傳成功的ctx做爲參數來組合文件) block.lastChunk = block.end % blockSize === 0 || block.end === total; if (block.start % blockSize === 0) { // 增長塊頭標識 block.mkblk = true; // 計算總塊數 let blocks = Math.ceil( total / opts.blockSize ); // 增長塊大小標識 block.size = (block.blockIndex + 1) === blocks ? (total - block.start) : blockSize; } pending.push(block); index++; start += len; } file.blocks = pending.concat(); file.remaning = pending.length; return api; }
這樣改事後有一個毛病,那就是因爲片上傳是順序上傳,片上傳是沒法併發的~這樣改的結果就是,一個文件只能順序上傳全部片了。。~本修改只是一個示例,若是真的要徹底支持塊併發,片順序上傳,必需要修改block的結構,讓block存儲該塊中全部片內容。其結構應該是
block: { ... file: 父節點的引用 cutes: [ 片1, 片2, 片3 ], percents: x, remaning: cutes.length }
除此以外,把實施上傳的主體變動爲片,並實現或觸發一些支持分片上傳的自定義事件,這樣就能夠以塊爲單位,併發上傳,塊中片順序上傳了。
經過網上大量的例子,以下:
uploader.register({ 'before-send-file': 'bsf', 'before-send': 'bbs', 'after-send-file': 'afs' }, { 'bsf': function () { ... }, 'bbs': function (block) { var server = ''; var D = webUploader.Deferred() if (block.chunk === 1) { uploader.options.server = 'xxxx' } else { uploader.options.server = 'xxxxx' } setTimeout(function () { D.resolve() }, 200) return D.promise() }, 'afs': function () { ... } })
從例子看,彷佛webuploader只有一個通用的options來配置服務器地址,formData, headers信息等,因爲before-send-file, before-send, after-send-file三個鉤子是異步執行的,因此在併發上傳時,修改分片上傳或mkblk操做所需的服務配置可能會給咱們帶來困擾。按照這個思路,一個解決方案是實現一個uploadTaskManager,使用worker來進行多實例併發上傳操做。
然而近期,經過讀webuploader/widgets/upload.js的源代碼,咱們發現如下內容:
_doSend: function( block ) { var me = this, owner = me.owner, // 可喜可賀 opts = $.extend({}, me.options, block.options), file = block.file, tr = new Transport( opts ), data = $.extend({}, opts.formData ), headers = $.extend({}, opts.headers ), requestAccept, ret; ...
可喜可賀,咱們徹底能夠經過直接給block增長options來保證before-send鉤子執行時不擾亂總體options配置
// appendWidget不用管,是我添加用於追加註冊一個掛件的方法。 // 因爲register方法是在webuploader實例化的時候纔將註冊的掛件掛載上,因此纔有了這個方法 this.$uploader.appendWidget({ 'before-send-file': 'bsf', 'before-send': 'bbs', 'after-send-file': 'afs', 'name': 'progress' }, { bsf: (file) => { // 這個也不用管,是我爲vue增長的插件,每次響應get操做都返回一個webuploader.Deferred() let deferred = this.$deferred // 爲webuploader增長的sha1hash計算方法 this.$uploader.sha1File(file) .progress((e) => { // console.log(file.name, e) }) .then((sha1Hash) => { file.sha1Hash = sha1Hash api.path.upload({ name: file.name, pid: file.pid, hash: file.sha1Hash }) .then((res) => { let data = res.body if (data.msg === 'file already exists') { this.$uploader.skipFile(file) } else { file.token = data.token file.server = data.url } deferred.resolve() }) }) return deferred.promise() }, bbs: (block) => { let deferred = this.$deferred if (!block.options) { let file = block.file // 直接設置options來達到修改server,headers配置的目的 block.options = { headers: { 'Content-Type': 'application/octet-stream', 'Authorization': file.token, 'UploadBatch': file.source.uid } } // webuploader切出的block上沒有mkblk, blockIndex, size, offset屬性等,這是我爲了支持分片上傳作的修改,請注意 if (block.mkblk) { block.options.server = file.server + '/mkblk/' + block.size + '/' + block.blockIndex } else { // 尋找當前片在整個塊中的偏移 block.options.server = file.server + '/bput/' + file.ctxs[block.chunk - 1] + '/' + block.offset } } deferred.resolve() return deferred.promise() }, afs: (file) => { let deferred = this.$deferred if (file.skipped) { deferred.resolve() } else { let server = file.server + '/mkfile/' + file.size this.$http.post(server, file.mkblkctxs.join(','), { headers: { Authorization: file.token, 'Content-Type': 'text/plain', UploadBatch: file.source.uid } }) .then(res => { if (res.body.code) { deferred.reject(res.body.message) } else { deferred.resolve() } }) } return deferred.promise() }, 'name': 'progress' })
這裏用html5無依賴版本進行說明
1.html5版本沒有提供md5File的具體實現,而是以鉤子的形式給你了,若是真的須要聚合md5計算方法,能夠按照全量版本里的模塊註冊形式,依次引入md5計算輔助庫,引入全量包裏的lib/md5, runtime/html5/md5, widgets/md5三個模塊,並在preset模塊中引入widgets/md5, runtime/html5/md5兩個模塊,完成模塊組合。若是不須要在內部聚合,能夠直接使用register註冊一個匿名掛件,並把md5-file這個命令鉤子所對應的函數實現便可。 2.無依賴版本的內建jquery還不徹底,這致使了無依賴版本沒法運行,請自行爲dollar-builtin模塊增長$.param, $.inArray兩個方法,並將weuploader中用到了$.map方法的地方改成$.each(內建的jquery不支持$.map) 3.刪除全部與dom相關的依賴,只保留無dom操做相關的純邏輯模塊(其實不刪除也能夠,只要不配置dom相關掛件便可) 4.將webuploader實現爲vue的插件,能夠直接爲Vue.prototype添加一個uploader的實例
如下是一個內聚實現七牛雲qeTag hash的代碼,因爲是臨時測試修改,沒有在乎語法和模塊引入,見諒。
修改uploader模塊,爲webuploader添加sha1File方法的命令
// 批量添加純命令式方法。 $.each({ upload: 'start-upload', stop: 'stop-upload', getFile: 'get-file', getFiles: 'get-files', addFile: 'add-file', addFiles: 'add-file', sort: 'sort-files', removeFile: 'remove-file', cancelFile: 'cancel-file', skipFile: 'skip-file', retry: 'retry', isInProgress: 'is-in-progress', makeThumb: 'make-thumb', md5File: 'md5-file', sha1File: 'sha1-file', // 這裏添加~ getDimension: 'get-dimension', addButton: 'add-btn', predictRuntimeType: 'predict-runtime-type', refresh: 'refresh', disable: 'disable', enable: 'enable', reset: 'reset' }, function( fn, command ) { Uploader.prototype[ fn ] = function() { return this.request( command, arguments ); }; });
加入一個sha1的依賴,這裏我使用的是js-sha1
實現/widgets/sha1,實現sha1File接口
/** * @fileOverview sha1計算 */ import Base from '../base' import Uploader from '../uploader' import Sha1 from '../lib/sha1' import Blob from '../lib/blob' export default Uploader.register({ name: 'sha1', /** * 計算文件 sha1_hash 值,返回一個 promise 對象,能夠監聽 progress 進度。 * * * @method sha1File * @grammar sha1File( file[, start[, end]] ) => promise * @for Uploader * @example * * uploader.on( 'fileQueued', function( file ) { * var $li = ...; * * uploader.sha1File( file ) * * // 及時顯示進度 * .progress(function(percentage) { * console.log('Percentage:', percentage); * }) * * // 完成 * .then(function(val) { * console.log('sha1 result:', val); * }); * * }); */ sha1File: function( file, start, end ) { var sha1 = new Sha1(), deferred = Base.Deferred(), blob = (file instanceof Blob) ? file : this.request( 'get-file', file ).source; sha1.on( 'progress load', function( e ) { e = e || {}; deferred.notify( e.total ? e.loaded / e.total : 1 ); }); sha1.on( 'complete', function() { deferred.resolve( sha1.getResult() ); }); sha1.on( 'error', function( reason ) { deferred.reject( reason ); }); if ( arguments.length > 1 ) { start = start || 0; end = end || 0; start < 0 && (start = blob.size + start); end < 0 && (end = blob.size + end); end = Math.min( end, blob.size ); blob = blob.slice( start, end ); } sha1.loadFromBlob( blob ); return deferred.promise(); } });
實現/lib/sha1,鏈接運行時sha1庫的封裝
/** * @fileOverview sha1 */ import RuntimeClient from '../runtime/client' import Mediator from '../mediator' function Sha1() { RuntimeClient.call( this, 'Sha1' ); } // 讓 Sha1 具有事件功能。 Mediator.installTo( Sha1.prototype ); Sha1.prototype.loadFromBlob = function( blob ) { var me = this; if ( me.getRuid() ) { me.disconnectRuntime(); } // 鏈接到blob歸屬的同一個runtime. me.connectRuntime( blob.ruid, function() { me.exec('init'); me.exec( 'loadFromBlob', blob ); }); }; Sha1.prototype.getResult = function() { return this.exec('getResult'); }; export default Sha1;
建立一個運行時庫/runtime/html5/sha1,這裏使用了Crypto-JS v2.5.1進行輔助計算
/** * @fileOverview Transport flash實現 */ import Html5Runtime from './runtime' import Sha1 from '@/plugins/sha1' import Uploader from '../../uploader' import Crypto from '@/libs/Crypto' export default Html5Runtime.register( 'Sha1', { init: function() { // do nothing. }, loadFromBlob: function( file ) { var blob = file.getSource(), chunkSize = 4 * 1024 * 1024, chunks = Math.ceil( blob.size / chunkSize ), chunk = 0, owner = this.owner, me = this, blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, loadNext, fr; var hashs = [], ret = ''; fr = new FileReader(); loadNext = function() { var start, end; start = chunk * chunkSize; end = Math.min( start + chunkSize, blob.size ); fr.onload = function( e ) { // var block = Tool.Crypto.util.bytesToWords( new Uint8Array(e.target.result)); var sha1 = Sha1.create(); var hash = sha1.update(e.target.result).digest(); hashs = hashs.concat(hash); if (end === file.size) { var perfex = 0x16; if (chunks > 1) { perfex = 0x96 sha1 = Sha1.create(); hash = sha1.update(hashs).digest() hashs = hash } hashs.unshift(perfex) ret = Crypto.util.bytesToBase64(hashs); } owner.trigger( 'progress', { total: file.size, loaded: end }); }; fr.onloadend = function() { fr.onloadend = fr.onload = null; if ( ++chunk < chunks ) { setTimeout( loadNext, 1 ); } else { setTimeout(function(){ owner.trigger('load'); // 導出的是urlsafe的base64 me.result = ret.replace(/\//g,'_').replace(/\+/g,'-'); loadNext = file = blob = hashs = null; owner.trigger('complete'); }, 50 ); } }; fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); }; loadNext(); }, getResult: function() { return this.result; } });
爲preset/html5only掛載依賴
/** * @fileOverview 只有html5實現的文件版本。 */ import Base from '../base' import '../widgets/widget' import '../widgets/queue' import '../widgets/runtime' import '../widgets/upload' import '../widgets/validator' import '../widgets/md5' import '../widgets/sha1' import '../runtime/html5/blob' import '../runtime/html5/transport' import '../runtime/html5/md5' import '../runtime/html5/sha1' export default Base;
如何使用?和md5File使用姿式如出一轍