摘要:上传结构与网宿云要求上传结构的不同上图是翻自网宿云的文档的分片上传流程。鉴于网宿云的上传一片一块在逻辑上没毛病,我们同样能一块一块完成上传这里注意请仔细看网宿云或七牛云分片上传的文档,了解如何分片上传。
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使用姿势一模一样
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88811.html
摘要:简介是由团队开发的一个简单的以为主,为辅的现代文件上传组件。采用大文件分片并发上传,极大的提高了文件上传效率。另外分片传输能够更加实时的跟踪上传进度。选择文件的按钮。 简介:WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的...
摘要:简介是由团队开发的一个简单的以为主,为辅的现代文件上传组件。采用大文件分片并发上传,极大的提高了文件上传效率。另外分片传输能够更加实时的跟踪上传进度。选择文件的按钮。 简介:WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的...
摘要:简介是由团队开发的一个简单的以为主,为辅的现代文件上传组件。采用大文件分片并发上传,极大的提高了文件上传效率。另外分片传输能够更加实时的跟踪上传进度。选择文件的按钮。 简介:WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的...
摘要:否则强制转换成指定的类型。是否要分片处理大文件上传还有其他配置项上传事件选择需要上传的文件后,文件就会加入文件队列,并触发事件上传进度回调事件,在文件上传中,多次调用此事件当文件上传成功时触发当文件上传出错时触发。 WebUploader简述 具有两套运行时支持:HTML5与FLASH 分片、并发 预览、压缩 多途径添加文件 MD5验证 引入文件 虽然官方没说必须要引入JQuery...
阅读 3217·2021-11-22 12:07
阅读 1832·2021-10-12 10:11
阅读 1021·2019-08-30 15:44
阅读 2906·2019-08-30 12:45
阅读 2135·2019-08-29 16:41
阅读 1556·2019-08-29 16:35
阅读 2597·2019-08-29 12:57
阅读 1128·2019-08-26 13:51