一 web worker:
什么是web worker
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和通道属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序 (反之亦然);
兼容性:
worker中可用的函数和接口
你可以在web worker中使用大多数的标准javascript特性,包括
- Navigator
- XMLHttpRequest
- Array,Date,Math, and String
- WindowTimers.setTimeout`and WindowTimers.setInterval
在一个worker中最主要的你不能做的事情就是直接影响父页面。包括操作父页面的节点以及使用页面中的对象。你只能间接地实现,通过self.postMessage回传消息给主脚本,然后从主脚本那里执行操作或变化。
特性:
- 为 JavaScript引入真正的线程,不必再使用 setTimeout()、setInterval()、XMLHttpRequest 来模拟并行
- Worker 利用类似线程的消息传递实现并行。这非常适合确保对 UI 的刷新、性能以及对用户的响应。
- Web Worker 的三大主要特征:能够长时间运行(响应),理想的启动性能以及理想的内存消耗。
适用场景
- 使用专用线程进行数学运算
Web Worker最简单的应用就是用来做后台计算,而这种计算并不会中断前台用户的操作- 图像处理
通过使用从<canvas>或者<video>元素中获取的数据,可以把图像分割成几个不同的区域并且把它们推送给并行的不同Workers来做计算- 大量数据的检索
当需要在调用 ajax后处理大量的数据,如果处理这些数据所需的时间长短非常重要,可以在Web Worker中来做这些,避免冻结UI线程。- 背景数据分析
由于在使用Web Worker的时候,我们有更多潜在的CPU可用时间,我们现在可以考虑一下JavaScript中的新应用场景。
限制
- 不能访问
DOM
和BOM
对象(alert不支持,console.log部分浏览器支持,在safari中不能使用console,否则会报错)Location
和navigator
的只读访问,并且navigator
封装成WorkerNavigator
对象,有部分属性被更改。- 无法读取本地文件
- 全局变量中不存在
this
,this
并不指向window
。有self
,指向worker
本身- 子线程和父级线程的通讯是通过值拷贝,子线程对通信内容的修改,不会影响到主线程。在通讯过程中值过大也会影响到性能(解决这个问题可以用
transferable objects
)- 条数限制,大多浏览器能创建
web worker
线程的条数是有限制的,可以手动去拓展,但是如果不设置的话,基本上都在20条以内,每条线程大概5M左右,需要手动关掉一些不用的线程才能够创建新的线程(相关解决方案)
通信方法:
- 发送消息
主线程 :worker.postMessage();
worker线程 :self.postMessage();
- 接收消息
主线程:worker.message();
worker线程:self.message();
- 监听异常
主线程:worker.error();
worker线程:self.error();
- 销毁方法
主线程:worker.terminate();
worker线程:self.close();
二 需求分析:
因为这次需求是做多文件并行上传,参考了竞品(腾讯视频,西瓜视频,优酷视频,youtube 等),功能也是集各大网站的上传功能于一身,也是好样的。
需求清单:
- 要求并行上传(但考虑网速,cpu等因素,我们规定并行上传的数量为2);
- 检测网络状况(根据用户的网速,标示网络状况差/一般/良好)
- 单文件上传进度的百分比
- 所有文件上传总进度的百分比
- 视频文件的MD5计算
- 先计算完MD5的先上传
- 分片上传
- 各种状态的日志记录(如MD5转换时间,用户关闭操作等)
- 后续或扩展断点续传
- 后续扩展乱序上传
以上只是上传部分的功能,对于我这种第一次做上传的人来说,看了真是一头雾水。我们不仅要解决上述的需求,还要考虑其他的设计和性能问题,比如:
- js是单线程:当上传一个5G+的大文件,计算MD5的时间约几分钟,此时后添加的文件都在排队,需要一个一个计算MD5。
- 并行上传:不同的浏览器,在同一域名下的最大请求数是不同的,例如chrome是6个。
- 上传计算:分片上传,计算当前上传进度的百分比,计算网速等一些计算和读写操作
- 维护上传队列:当文件上传完成或者取消时,自动添加上传文件。
好在之前偶然间了解了web worker,在对接需求的时候,感觉用web worker去做再合适不过了,于是就开始构思整个结构该怎么写。
主要思路:
- js的主线程负责创建web worker,相关UI视图,更新UI。
- worker 负责 文件计算MD5,切片,上传,计算相关数据。
- 处理文件,上传时 如需更新UI,worker将相关数据传递给主线程,主线程更新相关UI视图。
- 主线程需要对文件 ,上传 进行计算 和 处理时,通知worker,worker完成相关操作。
Main<->worker(通信的的主要流程)
视频文件初始化(切片计算MD5->discovery->init->upload ......)
为了区分不同的操作,和数据。规定了通信的数据格式
eventType :'string' //接收方将通过不同的eventType执行不同的回调函数
data:{} //将需要通信的数据放在data中
例如:
eventType:'fileInit',//文件初始化+计算MD5
data:{
file:file,//文件
}
eventType:'discovery',//开始上传
data:{}
eventType:'reUpload',//重试(上传失败,重试/重新上传)
data:{}
eventType:'updateLog'//更新日志
data:{ xxx:xxx, xxx:xxx, //日志字段 }
eventType:'postLog',//发送日志
data:{ extra:'reupload'//上传失败,点击重试(重新上传)时,触发发送日志 }
eventType:'updateUploadStatus',//更新上传状态
data:{ status:'init/uploading/success/fail', }
<!-- status时uploading还会传其他参数,用于更新上传进度-->
uploading -> data':{
'status':'uploading',
'process_value':当前进度百分比 (0%~100%)
'currentSize':已上传大小
'fileSize':文件总大小
'detailVal':已上传大小/文件总大小(2MB/30.6MB)
}
<!-- uplpading -->
eventType:'updateUploadRate',//更新上传速度
data:{ uploadRate:number,//number类型,表示每秒的速度 }
核心代码:
Main:
创建worker
init (){ //创建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释放内存
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) { //回调
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.创建webworker
2.worker注册事件
3.为worker重的自定义事件绑定不同的回掉函数
Worker:
相关参数:
const currentUpload = {
file: null,
fileName:'',
fileCheck: '',
shardCount: 0,
shardSize: 0,
stage: 'upload_init',
initFailedTimes: 0, //init失败重试2次
failedTimes: 3, //失败重试2次
serverFaultTimes: 0, //服务器失败重试3次
timeoutTimes: 0, //超时重试3次
initTimes: 1, //同一次上传init请求次数,用于动态调整分片大小
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() {
// 记录上传结束时间
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);
};
}