Web Worker是Web浏览器提供的一种在后台线程中运行JavaScript的功能。它独立于主线程运行,可以执行计算密集型或长时间运行的任务,而不会阻塞页面的渲染和交互。通过将大文件切片上传的逻辑放在Web Worker中执行,我们可以充分利用浏览器的多线程能力,提高上传速度,并保持页面的流畅运行。
基于Vue的基础用法 在Vue项目中配置webpack来使用web-worker涉及几个关键步骤。这主要涉及到处理worker文件的加载,确保它们被正确地打包和引用。以下是一个基本的配置过程:
1.安装worker-loader npm install --save-dev worker-loader
2.配置webpack 1 2 3 4 5 6 7 8 9 10 11 12 13 module .exports = { publicPath : './' , chainWebpack : config => { config.module .rule('worker' ) .test(/\.worker\.js$/ ) .use('worker-loader' ) .loader('worker-loader' ) .options({ }) } }
3.创建和使用worker 创建一个worker文件,并给它一个.worker.js的扩展名。例如,你可以创建一个my-worker.worker.js文件。
1 2 3 4 5 6 7 8 9 10 11 self.onmessage = function (e ) { console .log('Worker: Hello World' ); const result = doSomeWork(e.data); self.postMessage(result); }; function doSomeWork (data ) { return data * 2 ; }
在你的Vue组件或其他JavaScript文件中,你可以像下面这样创建一个worker实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import MyWorker from './my-worker.worker.js' ; export default { methods : { startWorker ( ) { const myWorker = new MyWorker(); myWorker.onmessage = (e ) => { console .log('Main script: Received result' , e.data); }; myWorker.postMessage(100 ); } }, mounted ( ) { this .startWorker(); } };
实战:实现大文件切片上传 1.逻辑梳理 文件切片 :使用 JavaScript 的 Blob.prototype.slice() 方法将大文件切分成多个切片。
上传切片 :使用 axios 或其他 HTTP 客户端库逐个上传切片。可以为每个切片生成一个唯一的标识符(例如,使用文件的哈希值和切片索引),以便后端能够正确地将它们合并。
客户端线程数 :获取用户CPU线程数量,以便最大优化上传文件速度。
控制上传接口的并发数量 :防止大量的请求并发导致页面卡死,设计一个线程队列,控制请求数量一直保持在6。
2.实现 1.获取客户端线程数量 navigator.hardwareConcurrency 是一个只读属性,它返回用户设备的逻辑处理器内核数。
1 export const getConcurrency = () => navigator.hardwareConcurrency || 4
2.主线程 定义和处理一些必要的常量,并且根据用户的线程数进行开启多线程Web-worker任务处理文件切片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import { defer, createEventHandler } from 'js-hodgepodge' import FileWorker from './files.worker' export const getConcurrency = () => navigator.hardwareConcurrency || 4 export const handleEvent = () => createEventHandler('handleSchedule' )export const sliceFile = file => { const dfd = defer() const chunkSize = 1024 const thread = getConcurrency() const chunks = [] const chunkNum = Math .ceil(file.size / chunkSize) const workerChunkCount = Math .ceil(chunkNum / thread) let finishCount = 0 ; for (let i = 0 ; i < thread; i++) { const worker = new FileWorker() const startIndex = i * workerChunkCount; let endIndex = startIndex + workerChunkCount; if (endIndex > chunkNum) { endIndex = chunkNum; } worker.postMessage({ file, chunkSize, startIndex, endIndex, }); worker.onmessage = (e ) => { for (let i = startIndex; i < endIndex; i++) { chunks[i] = { ...e.data[i - startIndex], chunkNum, filename : file.name }; } worker.terminate(); finishCount++; if (finishCount === thread) { dfd.resolve({ chunks, chunkNum }); } }; } return dfd }
3.实现文件切片 首先,我们需要创建一个 Web Worker 脚本,用于处理文件切片和切片hash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import md5 from 'js-md5' self.onmessage = async function ({ data: { file, chunkSize, startIndex, endIndex, } } ) { const arr = []; for (let i = startIndex; i < endIndex; i++) { arr.push( createChunks(file, i, chunkSize) ); } const chunks = await Promise .all(arr) postMessage(chunks); } const createChunks = ( file, index, chunkSize ) => { return new Promise ((resolve ) => { const start = index * chunkSize; const end = start + chunkSize; const fileReader = new FileReader(); fileReader.onload = (e ) => { const content = new Uint8Array (e.target.result); const files = file.slice(start, end); const md5s = md5.arrayBuffer(content) function arrayBufferToHex (buffer ) { let bytes = new Uint8Array (buffer); let hexString = '' ; for (let i = 0 ; i < bytes.byteLength; i++) { let hex = bytes[i].toString(16 ); hexString += hex.length === 1 ? '0' + hex : hex; } return hexString; } resolve({ start, end, index, hash : arrayBufferToHex(md5s), files, }); }; fileReader.readAsArrayBuffer(file.slice(start, end)); }); }
Web Worker通过onmessage事件接收消息。当主线程发送消息时,这个消息会作为参数传递给onmessage函数。
切片hash处理流程:使用FileReader来读取文件内容。当文件分片读取完毕后,会触发onload这个事件,使用new Uint8Array(e.target.result)将读取的ArrayBuffer转换为Uint8Array,再利用js-md5的使用md5.arrayBuffer(content)计算分片的MD5哈希值,使用arrayBufferToHex函数将切片buffer转换为十六进制String,当所有分片处理完毕后,将结果(即分片及其相关信息)发送postMessage回主线程。
4.请求池的设计与处理 我这里创建一个请求队列,并使用 Promise 来控制并发请求的数量。创建一个数组来存储待处理的请求,并使用 Promise 来控制每次只有一定数量的请求被发送。当某个请求完成时,再从队列中取出下一个请求来发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 export const uploadFile = ( chunks ) => { chunks = chunks || [] let schedule = 0 const { dispatch } = handleEvent() const requestQueue = (concurrency ) => { concurrency = concurrency || 6 const queue = [] let current = 0 const dequeue = () => { while (current < concurrency && queue.length) { current++; const requestPromiseFactory = queue.shift(); requestPromiseFactory() .then(result => { console .log(result) schedule++; dispatch(window , schedule); }) .catch(error => { console .log(error) }) .finally(() => { current--; dequeue(); }); } } return (requestPromiseFactory ) => { queue.push(requestPromiseFactory) dequeue() } } const handleFormData = obj => { const formData = new FormData() Object .entries(obj) .forEach(([key, val] ) => { formData.append(key, val) }) return formData } const enqueue = requestQueue(6 ) for (let i = 0 ; i < chunks.length; i++) { enqueue(() => axios.post( '/api/upload' , handleFormData(chunks[i]), { headers : { 'Content-Type' : 'multipart/form-data' } } )) } return schedule }
利用了第三方库js-hodgepodge的发布订阅,将上传切片成功的数量发布给主界面,得到相应的上传进度。
5.主界面代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <template> <div > <input type ="file" ref ="file" > <button @click ="handleUpload" > 提交</button > <p > 进度:{{ progress * 100 }}%</p > </div > </template > <script > import { sliceFile, uploadFile, handleEvent } from './file.utils' export default { data ( ) { return { progress : 0 } }, methods : { async handleUpload ( ) { const file = this .$refs.file.files[0 ] if (!file) { return } console .time() const dfd = sliceFile(file) dfd .promise .then(({ chunks, chunkNum } ) => { uploadFile(chunks) const { addEventListener } = handleEvent() const eject = addEventListener(window , ({ detail: schedule } ) => { this .progress = schedule / chunkNum if (schedule === chunkNum) { eject() } }) }) console .timeEnd() } } } </script > <style > </style >
6.执行响应结果打印 当执行一个大文件上传时,时间可被大大的压缩了。
node后端切片与组合结果
其实整个流程比较重要的就是文件切片,和请求池的设计
新一代并发方案 SharedArrayBuffer的威力 1 2 3 4 5 6 7 8 const sharedBuffer =new SharedArrayBuffer(1024 )const arr =new Int32Array (sharedBuffer);self.onmessage =function (e ) { const sharedBuffer = e.data; const arr =new Int32Array (sharedBuffer); }
WebAssembly + Worker 1 2 3 4 5 6 const worker = new Worker('wasm-worker.js' );worker.postMessage({wasmFile :'compute-heavy.wasm' }); worker.onmessage=(e )=> { console .l0g('WASM计算结果:' ,e.data); }