一 web worker:
什么是web worker
Web Worker為Web內(nèi)容在后臺線程中運行腳本提供了一種簡單的方法。線程可以執(zhí)行任務(wù)而不干擾用戶界面约谈。此外,他們可以使用XMLHttpRequest執(zhí)行 I/O (盡管responseXML和通道屬性總是為空)秕脓。一旦創(chuàng)建雕旨, 一個worker 可以將消息發(fā)送到創(chuàng)建它的JavaScript代碼, 通過將消息發(fā)布到該代碼指定的事件處理程序 (反之亦然);
兼容性:
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í)行操作或變化。
特性:
- 為 JavaScript引入真正的線程,不必再使用 setTimeout()涯冠、setInterval()炉奴、XMLHttpRequest 來模擬并行
- Worker 利用類似線程的消息傳遞實現(xiàn)并行。這非常適合確保對 UI 的刷新蛇更、性能以及對用戶的響應(yīng)瞻赶。
- Web Worker 的三大主要特征:能夠長時間運行(響應(yīng)),理想的啟動性能以及理想的內(nèi)存消耗派任。
適用場景
- 使用專用線程進行數(shù)學(xué)運算
Web Worker最簡單的應(yīng)用就是用來做后臺計算砸逊,而這種計算并不會中斷前臺用戶的操作- 圖像處理
通過使用從<canvas>或者<video>元素中獲取的數(shù)據(jù),可以把圖像分割成幾個不同的區(qū)域并且把它們推送給并行的不同Workers來做計算- 大量數(shù)據(jù)的檢索
當需要在調(diào)用 ajax后處理大量的數(shù)據(jù)掌逛,如果處理這些數(shù)據(jù)所需的時間長短非常重要师逸,可以在Web Worker中來做這些,避免凍結(jié)UI線程豆混。- 背景數(shù)據(jù)分析
由于在使用Web Worker的時候篓像,我們有更多潛在的CPU可用時間动知,我們現(xiàn)在可以考慮一下JavaScript中的新應(yīng)用場景。
限制
- 不能訪問
DOM
和BOM
對象(alert不支持遗淳,console.log部分瀏覽器支持拍柒,在safari中不能使用console,否則會報錯)Location
和navigator
的只讀訪問,并且navigator
封裝成WorkerNavigator
對象屈暗,有部分屬性被更改。- 無法讀取本地文件
- 全局變量中不存在
this
脂男,this
并不指向window
养叛。有self
,指向worker
本身- 子線程和父級線程的通訊是通過值拷貝宰翅,子線程對通信內(nèi)容的修改弃甥,不會影響到主線程。在通訊過程中值過大也會影響到性能(解決這個問題可以用
transferable objects
)- 條數(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)站的上傳功能于一身,也是好樣的召川。
需求清單:
- 要求并行上傳(但考慮網(wǎng)速南缓,cpu等因素,我們規(guī)定并行上傳的數(shù)量為2)荧呐;
- 檢測網(wǎng)絡(luò)狀況(根據(jù)用戶的網(wǎng)速汉形,標示網(wǎng)絡(luò)狀況差/一般/良好)
- 單文件上傳進度的百分比
- 所有文件上傳總進度的百分比
- 視頻文件的MD5計算
- 先計算完MD5的先上傳
- 分片上傳
- 各種狀態(tài)的日志記錄(如MD5轉(zhuǎn)換時間,用戶關(guān)閉操作等)
- 后續(xù)或擴展斷點續(xù)傳
- 后續(xù)擴展亂序上傳
以上只是上傳部分的功能坛增,對于我這種第一次做上傳的人來說获雕,看了真是一頭霧水。我們不僅要解決上述的需求收捣,還要考慮其他的設(shè)計和性能問題届案,比如:
- js是單線程:當上傳一個5G+的大文件,計算MD5的時間約幾分鐘罢艾,此時后添加的文件都在排隊楣颠,需要一個一個計算MD5尽纽。
- 并行上傳:不同的瀏覽器,在同一域名下的最大請求數(shù)是不同的童漩,例如chrome是6個弄贿。
- 上傳計算:分片上傳,計算當前上傳進度的百分比矫膨,計算網(wǎng)速等一些計算和讀寫操作
- 維護上傳隊列:當文件上傳完成或者取消時差凹,自動添加上傳文件。
好在之前偶然間了解了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 ......)
為了區(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);
};
}