webuploader踩坑
webuploader是百度fex团队开辟的一个非常便利的上传插件,然则我们在现实生产中,会发明运用它与我们的需求有林林总总的相差。近来做上传功用,踩了不少坑,如今来纪录一下。假如我的文章中有任何不妥或许不对的处所,迎接斧正。
webuploader上传构造与网宿云请求上传构造的差别
上图是翻自网宿云的文档的分片上传流程。
经由过程该图,我们可知网宿云构造上传文件情势是
{文件[块1(分片1,分片2,分片3,…),块2,块3,…]}
而webuploader对文件分片的情势以下
{文件[块1(分片1),块2(分片1),块3(分片1),…]}
即一块等于一片。鉴于网宿云的上传一片一块在逻辑上没缺点,我们一样能一块一块完成上传
这里注重,请仔细看网宿云或七牛云分片上传的文档,相识怎样分片上传。个中一个很主要的观点是块,片上下文,即ctx,请前去检察
webuploader上传流程上与需求不符合的缘由
我们先来看webuploader一个文件上传流程中,触发的钩子和事宜
一个文件的上传只触发三个现实运用的钩子
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'
})
关于webuploader怎样和vue组合的探究
这里用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运用姿态如出一辙