web worker 处理多文件并行上传

一 web worker:

什么是web worker

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和通道属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序 (反之亦然);

兼容性:

webworker 兼容性.png

worker中可用的函数和接口

你可以在web worker中使用大多数的标准javascript特性,包括

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

在一个worker中最主要的你不能做的事情就是直接影响父页面。包括操作父页面的节点以及使用页面中的对象。你只能间接地实现,通过self.postMessage回传消息给主脚本,然后从主脚本那里执行操作或变化。

特性:
  1. 为 JavaScript引入真正的线程,不必再使用 setTimeout()、setInterval()、XMLHttpRequest 来模拟并行
  2. Worker 利用类似线程的消息传递实现并行。这非常适合确保对 UI 的刷新、性能以及对用户的响应。
  3. Web Worker 的三大主要特征:能够长时间运行(响应),理想的启动性能以及理想的内存消耗。
适用场景
  1. 使用专用线程进行数学运算
    Web Worker最简单的应用就是用来做后台计算,而这种计算并不会中断前台用户的操作
  2. 图像处理
    通过使用从<canvas>或者<video>元素中获取的数据,可以把图像分割成几个不同的区域并且把它们推送给并行的不同Workers来做计算
  3. 大量数据的检索
    当需要在调用 ajax后处理大量的数据,如果处理这些数据所需的时间长短非常重要,可以在Web Worker中来做这些,避免冻结UI线程。
  4. 背景数据分析
    由于在使用Web Worker的时候,我们有更多潜在的CPU可用时间,我们现在可以考虑一下JavaScript中的新应用场景。
限制
  1. 不能访问DOMBOM对象(alert不支持,console.log部分浏览器支持,在safari中不能使用console,否则会报错)
  2. Locationnavigator的只读访问,并且navigator封装成WorkerNavigator对象,有部分属性被更改。
  3. 无法读取本地文件
  4. 全局变量中不存在thisthis并不指向window。有self,指向worker本身
  5. 子线程和父级线程的通讯是通过值拷贝,子线程对通信内容的修改,不会影响到主线程。在通讯过程中值过大也会影响到性能(解决这个问题可以用transferable objects
  6. 条数限制,大多浏览器能创建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 等),功能也是集各大网站的上传功能于一身,也是好样的。

需求清单:
  1. 要求并行上传(但考虑网速,cpu等因素,我们规定并行上传的数量为2);
  2. 检测网络状况(根据用户的网速,标示网络状况差/一般/良好)
  3. 单文件上传进度的百分比
  4. 所有文件上传总进度的百分比
  5. 视频文件的MD5计算
  6. 先计算完MD5的先上传
  7. 分片上传
  8. 各种状态的日志记录(如MD5转换时间,用户关闭操作等)
  9. 后续或扩展断点续传
  10. 后续扩展乱序上传

以上只是上传部分的功能,对于我这种第一次做上传的人来说,看了真是一头雾水。我们不仅要解决上述的需求,还要考虑其他的设计和性能问题,比如:

  1. js是单线程:当上传一个5G+的大文件,计算MD5的时间约几分钟,此时后添加的文件都在排队,需要一个一个计算MD5。
  2. 并行上传:不同的浏览器,在同一域名下的最大请求数是不同的,例如chrome是6个。
  3. 上传计算:分片上传,计算当前上传进度的百分比,计算网速等一些计算和读写操作
  4. 维护上传队列:当文件上传完成或者取消时,自动添加上传文件。

好在之前偶然间了解了web worker,在对接需求的时候,感觉用web worker去做再合适不过了,于是就开始构思整个结构该怎么写。
主要思路:

  • js的主线程负责创建web worker,相关UI视图,更新UI。
  • worker 负责 文件计算MD5,切片,上传,计算相关数据。
  • 处理文件,上传时 如需更新UI,worker将相关数据传递给主线程,主线程更新相关UI视图。
  • 主线程需要对文件 ,上传 进行计算 和 处理时,通知worker,worker完成相关操作。

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

视频文件初始化(切片计算MD5->discovery->init->upload ......)

主要上传流程.png

为了区分不同的操作,和数据。规定了通信的数据格式
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);
        };
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 一、概述 JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。...
    零星小雨_c84a阅读 2,435评论 0 2
  • 作者:阮一峰www.ruanyifeng.com/blog/2018/07/web-worker.html 概述 ...
    grain先森阅读 1,072评论 0 1
  • _________________________________________________________...
    fastwe阅读 609评论 0 0
  • 我们家在广州天河住的房子时间久远了,想换个地住,房子最好大点,新点。于是我上网找资料看了看,真是不看不知道,一看吓...
    益我水果阅读 214评论 0 0