web worker 處理多文件并行上傳

一 web worker:

什么是web worker

Web Worker為Web內(nèi)容在后臺線程中運行腳本提供了一種簡單的方法。線程可以執(zhí)行任務(wù)而不干擾用戶界面约谈。此外,他們可以使用XMLHttpRequest執(zhí)行 I/O (盡管responseXML和通道屬性總是為空)秕脓。一旦創(chuàng)建雕旨, 一個worker 可以將消息發(fā)送到創(chuàng)建它的JavaScript代碼, 通過將消息發(fā)布到該代碼指定的事件處理程序 (反之亦然);

兼容性:

webworker 兼容性.png

worker中可用的函數(shù)和接口

你可以在web worker中使用大多數(shù)的標準javascript特性悲柱,包括

  • Navigator
  • XMLHttpRequest
  • Array,Date,Math, and String
  • WindowTimers.setTimeout`and WindowTimers.setInterval

在一個worker中最主要的你不能做的事情就是直接影響父頁面锋喜。包括操作父頁面的節(jié)點以及使用頁面中的對象。你只能間接地實現(xiàn)豌鸡,通過self.postMessage回傳消息給主腳本嘿般,然后從主腳本那里執(zhí)行操作或變化。

特性:
  1. 為 JavaScript引入真正的線程,不必再使用 setTimeout()涯冠、setInterval()炉奴、XMLHttpRequest 來模擬并行
  2. Worker 利用類似線程的消息傳遞實現(xiàn)并行。這非常適合確保對 UI 的刷新蛇更、性能以及對用戶的響應(yīng)瞻赶。
  3. Web Worker 的三大主要特征:能夠長時間運行(響應(yīng)),理想的啟動性能以及理想的內(nèi)存消耗派任。
適用場景
  1. 使用專用線程進行數(shù)學(xué)運算
    Web Worker最簡單的應(yīng)用就是用來做后臺計算砸逊,而這種計算并不會中斷前臺用戶的操作
  2. 圖像處理
    通過使用從<canvas>或者<video>元素中獲取的數(shù)據(jù),可以把圖像分割成幾個不同的區(qū)域并且把它們推送給并行的不同Workers來做計算
  3. 大量數(shù)據(jù)的檢索
    當需要在調(diào)用 ajax后處理大量的數(shù)據(jù)掌逛,如果處理這些數(shù)據(jù)所需的時間長短非常重要师逸,可以在Web Worker中來做這些,避免凍結(jié)UI線程豆混。
  4. 背景數(shù)據(jù)分析
    由于在使用Web Worker的時候篓像,我們有更多潛在的CPU可用時間动知,我們現(xiàn)在可以考慮一下JavaScript中的新應(yīng)用場景。
限制
  1. 不能訪問DOMBOM對象(alert不支持遗淳,console.log部分瀏覽器支持拍柒,在safari中不能使用console,否則會報錯)
  2. Locationnavigator的只讀訪問,并且navigator封裝成WorkerNavigator對象屈暗,有部分屬性被更改。
  3. 無法讀取本地文件
  4. 全局變量中不存在this脂男,this并不指向window养叛。有self,指向worker本身
  5. 子線程和父級線程的通訊是通過值拷貝宰翅,子線程對通信內(nèi)容的修改弃甥,不會影響到主線程。在通訊過程中值過大也會影響到性能(解決這個問題可以用transferable objects
  6. 條數(shù)限制汁讼,大多瀏覽器能創(chuàng)建web worker線程的條數(shù)是有限制的淆攻,可以手動去拓展,但是如果不設(shè)置的話嘿架,基本上都在20條以內(nèi)瓶珊,每條線程大概5M左右,需要手動關(guān)掉一些不用的線程才能夠創(chuàng)建新的線程(相關(guān)解決方案

通信方法:

  • 發(fā)送消息

主線程 :worker.postMessage();
worker線程 :self.postMessage();

  • 接收消息

主線程:worker.message();
worker線程:self.message();

  • 監(jiān)聽異常

主線程:worker.error();
worker線程:self.error();

  • 銷毀方法

主線程:worker.terminate();
worker線程:self.close();


二 需求分析:

因為這次需求是做多文件并行上傳耸彪,參考了競品(騰訊視頻伞芹,西瓜視頻,優(yōu)酷視頻蝉娜,youtube 等)唱较,功能也是集各大網(wǎng)站的上傳功能于一身,也是好樣的召川。

需求清單:
  1. 要求并行上傳(但考慮網(wǎng)速南缓,cpu等因素,我們規(guī)定并行上傳的數(shù)量為2)荧呐;
  2. 檢測網(wǎng)絡(luò)狀況(根據(jù)用戶的網(wǎng)速汉形,標示網(wǎng)絡(luò)狀況差/一般/良好)
  3. 單文件上傳進度的百分比
  4. 所有文件上傳總進度的百分比
  5. 視頻文件的MD5計算
  6. 先計算完MD5的先上傳
  7. 分片上傳
  8. 各種狀態(tài)的日志記錄(如MD5轉(zhuǎn)換時間,用戶關(guān)閉操作等)
  9. 后續(xù)或擴展斷點續(xù)傳
  10. 后續(xù)擴展亂序上傳

以上只是上傳部分的功能坛增,對于我這種第一次做上傳的人來說获雕,看了真是一頭霧水。我們不僅要解決上述的需求收捣,還要考慮其他的設(shè)計和性能問題届案,比如:

  1. js是單線程:當上傳一個5G+的大文件,計算MD5的時間約幾分鐘罢艾,此時后添加的文件都在排隊楣颠,需要一個一個計算MD5尽纽。
  2. 并行上傳:不同的瀏覽器,在同一域名下的最大請求數(shù)是不同的童漩,例如chrome是6個弄贿。
  3. 上傳計算:分片上傳,計算當前上傳進度的百分比矫膨,計算網(wǎng)速等一些計算和讀寫操作
  4. 維護上傳隊列:當文件上傳完成或者取消時差凹,自動添加上傳文件。

好在之前偶然間了解了web worker侧馅,在對接需求的時候危尿,感覺用web worker去做再合適不過了,于是就開始構(gòu)思整個結(jié)構(gòu)該怎么寫馁痴。
主要思路:

  • js的主線程負責創(chuàng)建web worker谊娇,相關(guān)UI視圖,更新UI罗晕。
  • worker 負責 文件計算MD5济欢,切片,上傳小渊,計算相關(guān)數(shù)據(jù)法褥。
  • 處理文件,上傳時 如需更新UI粤铭,worker將相關(guān)數(shù)據(jù)傳遞給主線程挖胃,主線程更新相關(guān)UI視圖。
  • 主線程需要對文件 梆惯,上傳 進行計算 和 處理時酱鸭,通知worker,worker完成相關(guān)操作垛吗。

Main<->worker(通信的的主要流程)

視頻文件初始化(切片計算MD5->discovery->init->upload ......)

主要上傳流程.png

為了區(qū)分不同的操作凹髓,和數(shù)據(jù)。規(guī)定了通信的數(shù)據(jù)格式
eventType :'string' //接收方將通過不同的eventType執(zhí)行不同的回調(diào)函數(shù)
data:{} //將需要通信的數(shù)據(jù)放在data中

例如:

eventType:'fileInit',//文件初始化+計算MD5
data:{ 
       file:file,//文件
}
eventType:'discovery',//開始上傳
data:{}
eventType:'reUpload',//重試(上傳失敗怯屉,重試/重新上傳)
data:{}
eventType:'updateLog'//更新日志
data:{ xxx:xxx, xxx:xxx, //日志字段 }
eventType:'postLog',//發(fā)送日志
data:{ extra:'reupload'//上傳失敗蔚舀,點擊重試(重新上傳)時,觸發(fā)發(fā)送日志 }
eventType:'updateUploadStatus',//更新上傳狀態(tài)
data:{ status:'init/uploading/success/fail', }
    <!-- status時uploading還會傳其他參數(shù)锨络,用于更新上傳進度-->
    uploading -> data':{
        'status':'uploading',
        'process_value':當前進度百分比 (0%~100%)
        'currentSize':已上傳大小
        'fileSize':文件總大小
        'detailVal':已上傳大小/文件總大卸奶伞(2MB/30.6MB)
    }
    <!-- uplpading -->
eventType:'updateUploadRate',//更新上傳速度
data:{ uploadRate:number,//number類型,表示每秒的速度 }

核心代碼:

Main:

創(chuàng)建worker
init (){                    //創(chuàng)建web worker
    const xhr = new XMLHttpRequest,
                startTime = (new Date).getTime(),
                workerPath = '';
    xhr.onload = function(){
              const workerUrl = window.URL.createObjectURL(new Blob([xhr.responseText], {
                          type: "text/javascript"
                        })),
              worker = new Worker(workerUrl);
              window.URL.revokeObjectURL(workerUrl);//銷毀url釋放內(nèi)存
              this.bindEvents(worker);
     }
     xhr.onerror = function(){};
     xhr.open("get", workerPath, false);
     xhr.send();
}
bindEvents(worker)  {        //注冊事件
      worker.addEventListener('message', this.message);
      worker.addEventListener('error', this.error);
}
message(e) {                //回調(diào)
      let eventType = e.data.eventType,
          data      = e.data.data;
          switch(eventType){
              case 'updateLog':
                    log.updateLog(data);
                    break;
              case 'postLog':
                    log.postLog(extra);
                    break;
              case 'updateUploadStatus':
                    if(data.status == 'init'){
  
                           upload.init();

                    }else if(data.status == 'initFail'){

                          upload.initFail();

                    }else if(data.status == 'uploading'){
        
                           upload.updateProgress(data);

                     }else if(data.status == 'success'){

                            upload.uploadSuccess(data);

                     }else if(data.status == 'uploadFail'){

                            upload.uploadFail();

                     }else if(data.status == 'checkFail'){

                            upload.checkFail();

                    }
                    break;
                case 'warning':
                    console.log('warn',data);
                    break;
                case 'updateUploadRate':
                    upload.updateUploadRate(data)
                    break;
                case 'discovery':
                    upload.discovery();
                    break;
                }
    }

這部分功能是 1.創(chuàng)建webworker
2.worker注冊事件
3.為worker重的自定義事件綁定不同的回掉函數(shù)

Worker:

相關(guān)參數(shù):
const currentUpload = {
    file: null,
    fileName:'',
    fileCheck: '',
    shardCount: 0,
    shardSize: 0,
    stage: 'upload_init',
    initFailedTimes: 0, //init失敗重試2次
    failedTimes: 3, //失敗重試2次
    serverFaultTimes: 0, //服務(wù)器失敗重試3次
    timeoutTimes: 0, //超時重試3次
    initTimes: 1, //同一次上傳init請求次數(shù)羡儿,用于動態(tài)調(diào)整分片大小
    isFromCheck: false, //標識是否check過
    chunkSize: 2 * 1024 * 1024,
    fileToken: '',
    urlTag: '',
    usid: '',
    objectId: '',
    finished: 0, // 完成第幾片
    loadedSize: 0, // 重新上傳前已上傳的大小
    currentSize: 0, // 當次上傳的大小
};
計算MD5:
getFileMD5(file, initCB) {
        let that = this;
        let fileMD5,
            currentChunk = 0,
            fileReader = new FileReader(),
            spark = new MD5.ArrayBuffer();
        const loadNext = function() {
            let start = currentChunk * currentUpload.chunkSize,
                end = Math.min(start + currentUpload.chunkSize, file.size);
            fileReader.readAsArrayBuffer(that.sliceFile(file, start, end));
        };
        fileReader.onload = function(e) {
            spark.append(e.target.result);
            currentChunk++;
            if (currentChunk < currentUpload.shardCount) {
                loadNext();
            } else {
                fileMD5 = spark.end();
                currentUpload.fileCheck = fileMD5;
                initCB(fileMD5);
            }
        };
        currentUpload.file = file;
        currentUpload.shardCount = Math.ceil(file.size / currentUpload.chunkSize);
        loadNext();
    }
切片方法
sliceFile(file, start, end) {
        let sliceMethod = Blob.prototype.slice || Blob.prototype.webkitSlice || Blob.prototype.mozSlice;
        return sliceMethod.call(file, start, end);
    },
切片上傳
    upload:(sliceNum) {
        let _this = this;
        sliceNum = sliceNum == undefined ? 0 : sliceNum;
        let start = sliceNum * currentUpload.shardSize;
        let end = Math.min(currentUpload.file.size, start + currentUpload.shardSize);
        let blob =this.sliceFile(currentUpload.file, start, end);

        let fileReader = new FileReader();
        let fileArrayBuffer = fileReader.readAsArrayBuffer(blob);
        fileReader.onload = function(e) {
            let sectionCheck = MD5.ArrayBuffer.hash(e.target.result);
            const xhr = new XMLHttpRequest();
            xhr.open('POST', url , true);
            xhr.onload = function() {
                // 記錄上傳結(jié)束時間
                if (xhr.status == 200) {
                    var res = JSON.parse(xhr.response);
                    res.error  ? _this.uploadFailed(sliceNum,args,res.error,xhr.status,res) : _this.uploadCompleted(sliceNum,args,res)
                } else {
                    _this.uploadFailed(sliceNum, args, 'SERVERFAULT',xhr.status);
                }
            };
            xhr.onerror = function() {
                _this.uploadFailed(sliceNum, args, 'NETWORKFAILURE');
            };
            xhr.ontimeout = function() {
                _this.uploadFailed(sliceNum, args, 'TIMEOUT');
            };
            xhr.withCredentials = true;
            xhr.timeout = 30 * 1000;
            xhr.setRequestHeader('Content-Type', 'multipart/form-data');
            xhr.send(blob);
        };
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末礼患,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缅叠,老刑警劉巖悄泥,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肤粱,居然都是意外死亡弹囚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門领曼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸥鹉,“玉大人,你說我怎么就攤上這事庶骄∷蜗希” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵瓢姻,是天一觀的道長。 經(jīng)常有香客問我音诈,道長幻碱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任细溅,我火速辦了婚禮褥傍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喇聊。我一直安慰自己恍风,他們只是感情好,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布誓篱。 她就那樣靜靜地躺著朋贬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窜骄。 梳的紋絲不亂的頭發(fā)上锦募,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天,我揣著相機與錄音邻遏,去河邊找鬼糠亩。 笑死,一個胖子當著我的面吹牛准验,可吹牛的內(nèi)容都是我干的赎线。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼糊饱,長吁一口氣:“原來是場噩夢啊……” “哼垂寥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤矫废,失蹤者是張志新(化名)和其女友劉穎盏缤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蓖扑,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡唉铜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了律杠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片潭流。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖柜去,靈堂內(nèi)的尸體忽然破棺而出灰嫉,到底是詐尸還是另有隱情,我是刑警寧澤嗓奢,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布讼撒,位于F島的核電站股耽,受9級特大地震影響根盒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜物蝙,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一炎滞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诬乞,春花似錦册赛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至责掏,卻和暖如春柜砾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背换衬。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工痰驱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞳浦。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓担映,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叫潦。 傳聞我的和親對象是個殘疾皇子蝇完,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,103評論 1 32
  • 一、概述 JavaScript 語言采用的是單線程模型,也就是說短蜕,所有任務(wù)只能在一個線程上完成氢架,一次只能做一件事。...
    零星小雨_c84a閱讀 2,467評論 0 2
  • 作者:阮一峰www.ruanyifeng.com/blog/2018/07/web-worker.html 概述 ...
    grain先森閱讀 1,081評論 0 1
  • _________________________________________________________...
    fastwe閱讀 618評論 0 0
  • 我們家在廣州天河住的房子時間久遠了朋魔,想換個地住岖研,房子最好大點,新點警检。于是我上網(wǎng)找資料看了看孙援,真是不看不知道,一看嚇...
    益我水果閱讀 218評論 0 0