webuploader與網宿雲踩坑

webuploader踩坑

webuploader是百度fex團隊開發的一個十分便捷的上傳插件,可是咱們在實際生產中,會發現使用它與咱們的需求有各類各樣的出入。最近作上傳功能,踩了很多坑,如今來記錄一下。若是個人文章中有任何不妥或者不對的地方,歡迎指正。javascript

webuploader上傳結構與網宿雲要求上傳結構的不一樣

clipboard.png

上圖是翻自網宿雲的文檔的分片上傳流程。html

經過該圖,咱們可知網宿雲組織上傳文件形式是vue

{文件[塊1(分片1,分片2,分片3,…),塊2,塊3,…]}html5

而webuploader對文件分片的形式以下java

{文件[塊1(分片1),塊2(分片1),塊3(分片1),…]}jquery

即一塊便是一片。鑑於網宿雲的上傳一片一塊在邏輯上沒毛病,咱們一樣能一塊一塊完成上傳git


這裏注意,請仔細看網宿雲或七牛雲分片上傳的文檔,瞭解如何分片上傳。其中一個很重要的概念是塊,片上下文,即ctx,請前往查看github

webuploader上傳流程上與需求不符合的緣由

咱們先來看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'
})

關於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使用姿式如出一轍

相關文章
相關標籤/搜索