<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          實(shí)現(xiàn)大文件上傳和斷點(diǎn)續(xù)傳實(shí)踐經(jīng)驗(yàn)總結(jié)

          共 33818字,需瀏覽 68分鐘

           ·

          2022-07-13 07:58

          原文:https://juejin.cn/post/7118671489615790094


          實(shí)現(xiàn)文件上傳,大文件,以及如何斷點(diǎn)續(xù)傳等技術(shù)實(shí)現(xiàn)細(xì)節(jié),我會每個(gè)細(xì)節(jié),每個(gè)代碼都寫出來,一起調(diào)試,一起跟著步驟一一實(shí)現(xiàn)。

          大文件上傳技術(shù)要點(diǎn)分析

          技術(shù)要點(diǎn)分析:

          • e6文件對象,ajax上傳,async await promise,后臺文件存儲,流操作(寫入到服務(wù)器里面去)。

          • 一個(gè)文件傳統(tǒng)上傳 8M,現(xiàn)在文件上傳一般很大的文件,就要考慮切片問題,實(shí)現(xiàn)大文件上傳。

          • jses6 文件對象 file node stream 有所增強(qiáng)。任何文件都是二進(jìn)制,分隔blob(文件的一種類型)。

          • 一個(gè)大的文件可以分解為從哪個(gè)位置開始 start,每一塊多小size,offset。

          • http請求,n個(gè)切片可以并發(fā)上傳。核心利用 Blob.prototype.slice 方法,調(diào)用的slice方法可以返回 原文件的某個(gè)切片。(速度更快,改善了體驗(yàn))

          • 預(yù)先設(shè)置好的切片最大數(shù)量將文件切分為一個(gè)個(gè)切片,然后借助http的可并發(fā)性,同時(shí)上傳多個(gè)切片,這樣從原本傳一個(gè)大文件,變成了同時(shí)傳多個(gè)小的文件切片,可以大大減少上傳時(shí)間。

          • 由于是并發(fā),傳輸?shù)椒?wù)器的順序可能會發(fā)生變化,所以我們還需要給每個(gè)切片記錄順序。(前端的切片上傳,讓http并發(fā)帶來上傳大文件的快感。

          大文件上傳前端

          創(chuàng)建big_file_upload目錄文件,初始化node的項(xiàng)目:npm init -y,生成package.json文件。創(chuàng)建file_slice.html文件,模擬文件上傳,切片的過程,以及說明代碼的意義。

          live-server啟動(dòng)一下我們本地的服務(wù)器,它是npm的一個(gè)包,可以下載npm i -g live-server。也可以下載vs codelive server插件。啟動(dòng).html文件。

          file_slice文件

          file_slice.html代碼:

          <!DOCTYPE html><html>
          <head>
          <meta charset="utf-8">
          <title></title>
          </head>
          <body>
          <input type="file" id="file">
          <script>
          document.getElementById('file')
          .addEventListener('change', (event) => { const file = event.target.files[0]; // es6 文件對象
          // console.log(file);
          // console.log(Object.prototype.toString.call(file)); // [object File]
          // console.log(Object.prototype.toString.call(file.slice(0, 102400))); // [object Blob]
          let cur = 0, size = 1024*1024; // 1M
          // blob等待上傳的對象,所有的切片上傳完
          const fileChunkList = []; // blob數(shù)組
          while(cur < file.size) {
          fileChunkList.push({ // cur start offset end
          file: file.slice(cur, cur + size)
          });
          cur += size;
          } console.log(fileChunkList)
          })
          </script>
          </body></html>
          • file.slice完成切片,blob類型文件切片,js二進(jìn)制文件類型的blob協(xié)議。在文件上傳到服務(wù)器之前就可以提前預(yù)覽。

          返回文檔最后修改的日期和時(shí)間  lastModified: xxxx891269598
          返回文檔最后修改的日期和時(shí)間 lastModifiedDate: Tue Feb 15 xxxx 10:14:29 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間) {}
          名字 name: "JavaScript高級程序設(shè)計(jì)(第4版).pdf"大小 size: 14355650類型 type: "application/pdf"網(wǎng)絡(luò)工具包相對路徑 webkitRelativePath: ""
          size: 102400type""[[Prototype]]: Blob
          (14) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]0: {file: Blob}1: {file: Blob}2: {file: Blob}3: {file: Blob}4: {file: Blob}5: {file: Blob}6: {file: Blob}7: {file: Blob}8: {file: Blob}9: {file: Blob}10: {file: Blob}11: {file: Blob}12: {file: Blob}13: {file: Blob}length14

          Blob.slice

          Blob.slice() 方法用于創(chuàng)建一個(gè)包含源 Blob 的指定字節(jié)范圍內(nèi)的數(shù)據(jù)的新 Blob 對象。

          返回值

          一個(gè)新的 Blob 對象,它包含了原始 Blob 對象的某一個(gè)段的數(shù)據(jù)。

          blob文件

          同目錄下創(chuàng)建 blob.html文件,代碼:

          <!DOCTYPE html><html>
          <head>
          <meta charset="utf-8">
          <title></title>
          </head>
          <body>
          <img src="" alt="" id="pic" width="350px">
          <input type="file" id="file" />
          <script>
          // es6 file對象 blob blob:// 在文件上傳解決的問題。
          // 傳統(tǒng)es5時(shí)代文件只有上傳到服務(wù)器后,靜態(tài)服務(wù)提供一個(gè)遠(yuǎn)程地址給我們,才能夠看到我們上傳的這張圖片。
          // es6在本地客戶端操作文件的能力 file對象。
          // blob 協(xié)議在本地就把它立馬顯示出來,配上上傳進(jìn)度,更好的用戶體驗(yàn)。
          document.getElementById('file').addEventListener('change', (e) => { const file = e.target.files[0]; const URL = window.URL; const objectUrl = URL.createObjectURL(file); console.log(objectUrl); const pic = document.getElementById('pic');
          pic.src = objectUrl;
          pic.onload = function() {
          URL.revokeObjectURL(objectUrl); // 協(xié)議地址 釋放
          }
          })
          </script>
          </body></html>

          預(yù)覽效果:

          思路步驟

          切片,target目標(biāo)后端文件下以名字為目錄的文件;服務(wù)器端,如惡化將這些切片,合并成一個(gè),并且顯示原來的圖片,對于服務(wù)器端nodestream 的概念。

          開始在big_file_upload文件下創(chuàng)建server目錄,初始化一下npm init -y,生成package.json文件,添加一下我們的入口文件,index.js文件。

          創(chuàng)建文件目錄:

          說明:server后端服務(wù),target存儲文件,某文件下等

          server目錄下的index.js文件代碼:

          const path = require('path'); // 路徑const fse = require('fs-extra'); // fs擴(kuò)展包// 上傳目錄const UPLOAD_DIR = path.resolve(__dirname, ".", "target"); // server/target// console.log(UPLOAD_DIR);const filename = 'da';const filePath = path.resolve(UPLOAD_DIR, '..', `${filename}.mp3`); // 路徑console.log(filePath); // 根目錄下const pipeStream = (path, writeStream) =>
          new Promise(resolve => { const readStream = fse.createReadStream(path);
          readStream.on('end',() => {
          fse.unlinkSync(path); // 移除
          resolve();
          })
          readStream.pipe(writeStream);
          })const mergeFileChunk = async (filePath, filename, size) => { // console.log(filePath, filename, size)
          // 大文件上傳時(shí),設(shè)計(jì)后端思想時(shí)每個(gè)要上傳的文件,先以文件名,
          // 為target目錄名,把分文件blob,放入這個(gè)目錄
          // 文件blob上傳前要加上index
          // node 文件合并肯定可以的,stream
          const chunkDir = path.resolve(UPLOAD_DIR, filename); // console.log(chunkDir);
          const chunkPaths = await fse.readdir(chunkDir); // console.log(chunkPaths); // 路徑下的數(shù)組文件名
          chunkPaths.sort((a, b) => a.split('-')[1] - b.split('-')[1]); // console.log(chunkPaths, '++');
          // 每塊內(nèi)容寫入最后的文件,promise
          await Promise.all(
          chunkPaths.map((chunkPath, index) =>
          pipeStream( // 回流的方法
          path.resolve(chunkDir, chunkPath),
          fse.createWriteStream(filePath, { start: index * size, end: (index + 1) * size
          })
          )
          )
          ) // console.log('文件合并成功');
          fse.rmdirSync(chunkDir); // 刪除}

          mergeFileChunk(filePath, filename, 0.5*1024*1024);
          • fs提供文件的讀寫,刪除,文件的移動(dòng),文件的目錄,文件的目錄查看等等

          • yarn add fs-extra

          • yarn global add nodemon

          • stream

          • 可讀流,可寫流

          • chunk都是一個(gè)二進(jìn)制流文件

          • Promise.all 來包裝每個(gè)chunk的寫入

          • start end fse createWriteStream

          • 每個(gè)chunk寫入 先創(chuàng)建可讀流,再pipe給可寫流的過程。

          思路:以原文件做為文件夾的名字,在上傳blobs到這個(gè)文件夾,前且每個(gè)blob 都以文件-index的命名方式來存儲。

          http并發(fā)上傳大文件切片

          修改file_slice.html文件:

          <!DOCTYPE html><html>
          <head>
          <meta charset="utf-8">
          <title></title>
          </head>
          <body>
          <input type="file" id="file">
          <script>
          // 請求封裝
          // http并發(fā)文件上傳 blob上傳 chunk POST
          // 當(dāng)blob Promise.All 再發(fā)送一個(gè)merge的請求 /merge
          function request({
          url,
          method = 'POST',
          data,
          headers = {},
          requestList // 上傳的文件列表
          }
          )
          { return new Promise(resolve => { const xhr = new XMLHttpRequest(); // js ajax 對象
          xhr.open(method, url); // 請求
          Object.keys(headers).forEach(key => {
          xhr.setRequestHeader(key, headers[key]) // 請求加頭信息
          })
          xhr.send(data);
          xhr.onload = e => { // 事件監(jiān)聽
          resolve({ data: e.target.response
          })
          }
          })
          } document.getElementById('file')
          .addEventListener('change', async (event) => { const file = event.target.files[0]; // es6 文件對象
          // console.log(file);
          const file_name = file.name.split('.')[0]; // console.log(Object.prototype.toString.call(file)); // [object File]
          // console.log(Object.prototype.toString.call(file.slice(0, 102400))); // [object Blob]
          let cur = 0, size = 1024*1024; // 1M
          // blob等待上傳的對象,所有的切片上傳完
          const fileChunkList = []; // blob數(shù)組
          while(cur < file.size) {
          fileChunkList.push({ // cur start offset end
          file: file.slice(cur, cur + size)
          });
          cur += size;
          } console.log(fileChunkList) const requestList = fileChunkList.map(({file}, index) => { // 請求的數(shù)組
          const formData = new FormData(); // js post form
          formData.append('chunk', file);
          formData.append('filename', `${file_name}-${index}`); return {
          formData
          };
          })
          .map(async ({ formData }) => request({ url: 'http://localhost:3000', // 前后端的api
          data: formData
          })) await Promise.all(requestList); // 并發(fā)吧
          // console.log(requestList);
          })
          </script>
          </body></html>

          server目錄下,創(chuàng)建main.js文件,處理提交:

          • 下載yarn add multiparty

          const http = require('http');const path = require('path');const multiparty = require('multiparty');const fse = require('fs-extra');const server = http.createServer();const UPLOAD_DIR = path.resolve(__dirname, '.', 'target');

          server.on('request', async (req, res) => {
          res.setHeader("Access-Control-Allow-Origin", "*");
          res.setHeader("Access-Control-Allow-Headers", "*"); // res.end("hello");

          if (req.url == '/') { // chunk, name
          const multipart = new multiparty.Form(); // console.log(multipart)
          multipart.parse(req, async (err, fields, files) => { if (err) { return;
          } // console.log(files);

          const [chunk] = files.chunk; // 拿到了文件塊
          const [filename] = fields.filename; // 文件名
          // 塊名
          // console.log(filename);

          const dir_name = filename.split('-')[0]; const chunkDir = path.resolve(UPLOAD_DIR, dir_name); if (!fse.existsSync(chunkDir)) { await fse.mkdirs(chunkDir)
          } // chunk.path
          // 把chunk放入目錄
          await fse.move(chunk.path, `${chunkDir}/${filename}`);
          })
          } else if (req.url == '/merge/') { // 合并
          res.end('OK');
          }
          })

          server.listen(3000, () => console.log('正在監(jiān)聽3000端口'))
          Form {  _writableState: WritableState {    objectMode: false,    highWaterMark: 16384,    finalCalled: false,    needDrain: false,    ending: false,    ended: false,    finished: false,    destroyed: false,    decodeStrings: true,    defaultEncoding: 'utf8',    length: 0,    writing: false,    corked: 0,    sync: true,    bufferProcessing: false,    onwrite: [Function: bound onwrite],    writecb: null,    writelen: 0,    afterWriteTickInfo: null,    buffered: [],    bufferedIndex: 0,    allBuffers: true,    allNoop: true,    pendingcb: 0,    prefinished: false,    errorEmitted: false,    emitClose: false,    autoDestroy: true,    errored: null,    closed: false
          }, _events: [Object: null prototype] { newListener: [Function (anonymous)] }, _eventsCount: 1, _maxListeners: undefined, error: null, autoFields: false, autoFiles: false, maxFields: 1000, maxFieldsSize: 2097152, maxFilesSize: Infinity, uploadDir: 'C:\\Users\\xxx\\xxx\\Local\\xxx', encoding: 'utf8', bytesReceived: 0, bytesExpected: null, openedFiles: [], totalFieldSize: 0, totalFieldCount: 0, totalFileSize: 0, flushing: 0, backpressure: false, writeCbs: [], emitQueue: [],
          [Symbol(kCapture)]: false}

          斷點(diǎn)續(xù)傳

          1. 服務(wù)器端返回,告知我從那開始

          2. 瀏覽器端自行處理

          緩存處理

          1. 在切片上傳的axios成功回調(diào)中,存儲已上傳成功的切片

          2. 在切片上傳前,先看下localstorage中是否存在已上傳的切片,并修改uploaded

          3. 構(gòu)造切片數(shù)據(jù)時(shí),過濾掉uploadedtrue

          垃圾文件清理

          1. 前端在localstorage設(shè)置緩存時(shí)間,超過時(shí)間就發(fā)送請求通知后端清理碎片文件,同時(shí)前端也要清理緩存。

          2. 前后端都約定好,每個(gè)緩存從生成開始,只能存儲12小時(shí),12小時(shí)后自動(dòng)清理

          3. 為每個(gè)文件切割塊添加不同的標(biāo)識, hash

          4. 當(dāng)上傳成功后,記錄上傳成功的標(biāo)識

          5. 當(dāng)我們暫?;蛘甙l(fā)送失敗后,可以重新發(fā)送沒有上傳成功的切割文件

          創(chuàng)建vue項(xiàng)目:vue create vue-upload-big-file.

          $ vue --version
          @vue/cli 4.5.13vue create vue-upload-big-file

          $ vue create vue-upload-big-file
          ? Please pick a preset: (Use arrow keys)
          ? Please pick a preset: Manually select features
          ? Check the features needed for your project: (Press <space> to select, <a> to t
          ? Check the features needed for your project: Choose Vue version, Babel
          ? Choose a version of Vue.js that you want to start the project with (Use arrow
          ? Choose a version of Vue.js that you want to start the project with 2.x
          ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
          > In dedicated config files
          ? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
          ? Save this as a preset for future projects? (y/N) n

          yarn add element-ui

          main.js中引入element-ui,代碼如下:

          import Vue from 'vue
          import App from '
          ./App.vue'
          import ElementUI from '
          element-uiimport 'element-ui/lib/theme-chalk/index.css'Vue.use(ElementUI);

          Vue.config.productionTip = falsenew Vue({ render: h => h(App),
          }).$mount('#app')

          App.vue代碼清理如下:

          <template> <div id="app">
          </div>
          </template><script>export default { name: 'app', components: {
          }
          }
          </script>

          App.vue代碼實(shí)現(xiàn):

          async calculateHash (fileChunkList) { return new Promise(resolve => {  // 需要花時(shí)間的任務(wù)
          // web workers
          // js 單線程的 UI 線程
          // html5 web workers 單獨(dú)開一個(gè)線程 獨(dú)立于 worker
          // 回調(diào)
          this.container.worker = new Worker('/hash.js'); this.container.worker.postMessage({ fileChunkList }); this.container.worker.onmessage = e => { console.log(e.data);
          }
          })
          }async handleUpload (e) { // 大量的任務(wù)
          if (!this.container.file) return; this.status = Status.uploading; const fileChunkList = this.createFileCHunk(this.container,file); this.container.hash = await this.calculateHash(fileChunkList);
          }

          createFileCHunk (file, size = SIZE) { const fileChunkList = []; let cur = 0; while (cur < file.size) {
          fileChunkList.push({ file: file.slice(cur, cur + size)
          });
          cur += size;
          } return fileChunkList;
          }handleFileChange(e) { // 分隔文件
          const [ file ] = e.target.files; // 拿到第一個(gè)文件
          // console.log(e.target.files);
          this.container.file = file;
          }

          無論時(shí)前端還是后端,要考慮傳輸文件,特別是大文件,有可能發(fā)生丟失文件的情況,網(wǎng)速卡頓,服務(wù)器超時(shí),如何避免丟失的情況。hash

          當(dāng)點(diǎn)擊上傳按鈕時(shí)候,調(diào)用createFileChunk將文件進(jìn)行切片,切片數(shù)量通過文件大小控制,這里設(shè)置默認(rèn)值大小,進(jìn)行默認(rèn)值大小的進(jìn)行切片

          createFileChunk內(nèi)使用while循環(huán)和slice方法將切片放入fileChunkList數(shù)組中返回

          在生成文件切片時(shí),需要給每個(gè)切片一個(gè)標(biāo)識作為hash,這里使用文件名+下標(biāo),這樣后端可以知道切片是第幾個(gè)切片,用于之后的合并切片

          FormData.append()

          發(fā)送數(shù)據(jù)用到了 FormData

          formData.append(name, value, filename),其中 filename 為可選參數(shù),是傳給服務(wù)器的文件名稱, 當(dāng)一個(gè) Blob 或 File 被作為第二個(gè)參數(shù)的時(shí)候, Blob 對象的默認(rèn)文件名是 "blob"。

          什么叫hash呢

          什么叫hash呢?文件名,并不是唯一的,1.jpg圖片,1.jpg圖片,或 2.jpg圖片 一樣的內(nèi)容。- 不同名的圖片,內(nèi)容是一樣的。針對文件內(nèi)容進(jìn)行 hash計(jì)算。丟失重傳。

          隨后調(diào)用uploadChunks上傳所有的文件切片,將文件切片,切片hash,以及文件名放入FormData中,再調(diào)用上一步的 request 函數(shù)返回一個(gè) promise,最后調(diào)用 Promise.all并發(fā)上傳所有的切片

          spark-md5.min.js:

          (function(factory){if(typeof exports==="object"){module.exports=factory()}else if(typeof define==="function"&&define.amd){define(factory)}else{var glob;try{glob=window}catch(e){glob=self}glob.SparkMD5=factory()}})(function(undefined){"use strict";var add32=function(a,b){return a+b&4294967295},hex_chr=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];function cmn(q,a,b,x,s,t){a=add32(add32(a,q),add32(x,t));return add32(a<<s|a>>>32-s,b)}function md5cycle(x,k){var a=x[0],b=x[1],c=x[2],d=x[3];a+=(b&c|~b&d)+k[0]-680876936|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[1]-389564586|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[2]+606105819|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[3]-1044525330|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[4]-176418897|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[5]+1200080426|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[6]-1473231341|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[7]-45705983|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[8]+1770035416|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[9]-1958414417|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[10]-42063|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[11]-1990404162|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[12]+1804603682|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[13]-40341101|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[14]-1502002290|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[15]+1236535329|0;b=(b<<22|b>>>10)+c|0;a+=(b&d|c&~d)+k[1]-165796510|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[6]-1069501632|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[11]+643717713|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[0]-373897302|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[5]-701558691|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[10]+38016083|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[15]-660478335|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[4]-405537848|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[9]+568446438|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[14]-1019803690|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[3]-187363961|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[8]+1163531501|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[13]-1444681467|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[2]-51403784|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[7]+1735328473|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[12]-1926607734|0;b=(b<<20|b>>>12)+c|0;a+=(b^c^d)+k[5]-378558|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[8]-2022574463|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[11]+1839030562|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[14]-35309556|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[1]-1530992060|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[4]+1272893353|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[7]-155497632|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[10]-1094730640|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[13]+681279174|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[0]-358537222|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[3]-722521979|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[6]+76029189|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[9]-640364487|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[12]-421815835|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[15]+530742520|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[2]-995338651|0;b=(b<<23|b>>>9)+c|0;a+=(c^(b|~d))+k[0]-198630844|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[7]+1126891415|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[14]-1416354905|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[5]-57434055|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[12]+1700485571|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[3]-1894986606|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[10]-1051523|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[1]-2054922799|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[8]+1873313359|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[15]-30611744|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[6]-1560198380|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[13]+1309151649|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[4]-145523070|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[11]-1120210379|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[2]+718787259|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[9]-343485551|0;b=(b<<21|b>>>11)+c|0;x[0]=a+x[0]|0;x[1]=b+x[1]|0;x[2]=c+x[2]|0;x[3]=d+x[3]|0}function md5blk(s){var md5blks=[],i;for(i=0;i<64;i+=4){md5blks[i>>2]=s.charCodeAt(i)+(s.charCodeAt(i+1)<<8)+(s.charCodeAt(i+2)<<16)+(s.charCodeAt(i+3)<<24)}return md5blks}function md5blk_array(a){var md5blks=[],i;for(i=0;i<64;i+=4){md5blks[i>>2]=a[i]+(a[i+1]<<8)+(a[i+2]<<16)+(a[i+3]<<24)}return md5blks}function md51(s){var n=s.length,state=[1732584193,-271733879,-1732584194,271733878],i,length,tail,tmp,lo,hi;for(i=64;i<=n;i+=64){md5cycle(state,md5blk(s.substring(i-64,i)))}s=s.substring(i-64);length=s.length;tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(i=0;i<length;i+=1){tail[i>>2]|=s.charCodeAt(i)<<(i%4<<3)}tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(state,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=n*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(state,tail);return state}function md51_array(a){var n=a.length,state=[1732584193,-271733879,-1732584194,271733878],i,length,tail,tmp,lo,hi;for(i=64;i<=n;i+=64){md5cycle(state,md5blk_array(a.subarray(i-64,i)))}a=i-64<n?a.subarray(i-64):new Uint8Array(0);length=a.length;tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(i=0;i<length;i+=1){tail[i>>2]|=a[i]<<(i%4<<3)}tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(state,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=n*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(state,tail);return state}function rhex(n){var s="",j;for(j=0;j<4;j+=1){s+=hex_chr[n>>j*8+4&15]+hex_chr[n>>j*8&15]}return s}function hex(x){var i;for(i=0;i<x.length;i+=1){x[i]=rhex(x[i])}return x.join("")}if(hex(md51("hello"))!=="5d41402abc4b2a76b9719d911017c592"){add32=function(x,y){var lsw=(x&65535)+(y&65535),msw=(x>>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}}if(typeof ArrayBuffer!=="undefined"&&!ArrayBuffer.prototype.slice){(function(){function clamp(val,length){val=val|0||0;if(val<0){return Math.max(val+length,0)}return Math.min(val,length)}ArrayBuffer.prototype.slice=function(from,to){var length=this.byteLength,begin=clamp(from,length),end=length,num,target,targetArray,sourceArray;if(to!==undefined){end=clamp(to,length)}if(begin>end){return new ArrayBuffer(0)}num=end-begin;target=new ArrayBuffer(num);targetArray=new Uint8Array(target);sourceArray=new Uint8Array(this,begin,num);targetArray.set(sourceArray);return target}})()}function toUtf8(str){if(/[\u0080-\uFFFF]/.test(str)){str=unescape(encodeURIComponent(str))}return str}function utf8Str2ArrayBuffer(str,returnUInt8Array){var length=str.length,buff=new ArrayBuffer(length),arr=new Uint8Array(buff),i;for(i=0;i<length;i+=1){arr[i]=str.charCodeAt(i)}return returnUInt8Array?arr:buff}function arrayBuffer2Utf8Str(buff){return String.fromCharCode.apply(null,new Uint8Array(buff))}function concatenateArrayBuffers(first,second,returnUInt8Array){var result=new Uint8Array(first.byteLength+second.byteLength);result.set(new Uint8Array(first));result.set(new Uint8Array(second),first.byteLength);return returnUInt8Array?result:result.buffer}function hexToBinaryString(hex){var bytes=[],length=hex.length,x;for(x=0;x<length-1;x+=2){bytes.push(parseInt(hex.substr(x,2),16))}return String.fromCharCode.apply(String,bytes)}function SparkMD5(){this.reset()}SparkMD5.prototype.append=function(str){this.appendBinary(toUtf8(str));return this};SparkMD5.prototype.appendBinary=function(contents){this._buff+=contents;this._length+=contents.length;var length=this._buff.length,i;for(i=64;i<=length;i+=64){md5cycle(this._hash,md5blk(this._buff.substring(i-64,i)))}this._buff=this._buff.substring(i-64);return this};SparkMD5.prototype.end=function(raw){var buff=this._buff,length=buff.length,i,tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],ret;for(i=0;i<length;i+=1){tail[i>>2]|=buff.charCodeAt(i)<<(i%4<<3)}this._finish(tail,length);ret=hex(this._hash);if(raw){ret=hexToBinaryString(ret)}this.reset();return ret};SparkMD5.prototype.reset=function(){this._buff="";this._length=0;this._hash=[1732584193,-271733879,-1732584194,271733878];return this};SparkMD5.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}};SparkMD5.prototype.setState=function(state){this._buff=state.buff;this._length=state.length;this._hash=state.hash;return this};SparkMD5.prototype.destroy=function(){delete this._hash;delete this._buff;delete this._length};SparkMD5.prototype._finish=function(tail,length){var i=length,tmp,lo,hi;tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(this._hash,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=this._length*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(this._hash,tail)};SparkMD5.hash=function(str,raw){return SparkMD5.hashBinary(toUtf8(str),raw)};SparkMD5.hashBinary=function(content,raw){var hash=md51(content),ret=hex(hash);return raw?hexToBinaryString(ret):ret};SparkMD5.ArrayBuffer=function(){this.reset()};SparkMD5.ArrayBuffer.prototype.append=function(arr){var buff=concatenateArrayBuffers(this._buff.buffer,arr,true),length=buff.length,i;this._length+=arr.byteLength;for(i=64;i<=length;i+=64){md5cycle(this._hash,md5blk_array(buff.subarray(i-64,i)))}this._buff=i-64<length?new Uint8Array(buff.buffer.slice(i-64)):new Uint8Array(0);return this};SparkMD5.ArrayBuffer.prototype.end=function(raw){var buff=this._buff,length=buff.length,tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],i,ret;for(i=0;i<length;i+=1){tail[i>>2]|=buff[i]<<(i%4<<3)}this._finish(tail,length);ret=hex(this._hash);if(raw){ret=hexToBinaryString(ret)}this.reset();return ret};SparkMD5.ArrayBuffer.prototype.reset=function(){this._buff=new Uint8Array(0);this._length=0;this._hash=[1732584193,-271733879,-1732584194,271733878];return this};SparkMD5.ArrayBuffer.prototype.getState=function(){var state=SparkMD5.prototype.getState.call(this);state.buff=arrayBuffer2Utf8Str(state.buff);return state};SparkMD5.ArrayBuffer.prototype.setState=function(state){state.buff=utf8Str2ArrayBuffer(state.buff,true);return SparkMD5.prototype.setState.call(this,state)};SparkMD5.ArrayBuffer.prototype.destroy=SparkMD5.prototype.destroy;SparkMD5.ArrayBuffer.prototype._finish=SparkMD5.prototype._finish;SparkMD5.ArrayBuffer.hash=function(arr,raw){var hash=md51_array(new Uint8Array(arr)),ret=hex(hash);return raw?hexToBinaryString(ret):ret};return SparkMD5});
          ('/hash.js') // 放在根目錄 public

          web workers 優(yōu)化我們的前端性能,將要花大量時(shí)間的,復(fù)雜的,放到一個(gè)新的線程中去計(jì)算,文件上傳通過hash計(jì)算。

          hash.js代碼:

          // 通過內(nèi)容計(jì)算md5值self.importScripts('/spark-md5.min.js')

          self.onmessage = e => { // self.postMessage({
          // "msg": "您好"
          // })
          const { fileChunkList } = e.data; const spark = new self.SparkMD5.ArrayBuffer(); let percentage = 0; let count = 0; // console.log(fileChunkList, 'worker fileChunkList');
          // 計(jì)算出hash
          const loadNext = index => { const reader = new FileReader(); // 文件閱讀對象
          reader.readAsArrayBuffer(fileChunkList[index].file);
          reader.onload = e => { // 事件
          count++;
          spark.append(e.target.result); if (count === fileChunkList.length)
          {
          self.postMessage({ percentage: 100, hash: spark.end()
          });
          self.close(); // 關(guān)閉當(dāng)前線程
          } else { // 還沒讀完
          percentage += 100/fileChunkList.length;
          self.postMessage({
          percentage
          });
          loadNext(count);
          }
          }
          }
          loadNext(0)
          } // this 當(dāng)前的線程

          大文件上傳

          1. 將大文件轉(zhuǎn)換為二進(jìn)制流的格式

          2. 利用流可以切割的屬性,將二進(jìn)制流切割成多份

          3. 組裝和分割塊同等數(shù)量的請求塊,并行或串行的形式發(fā)出請求

          4. 再給服務(wù)器端發(fā)出一個(gè)合并的信息

          App.vue

          <template>	<div id="app">
          <div>
          <input type="file" :disabled="status !== Status.wait" @change="handleFileChange" />
          <el-button @click="handleUpload" :disabled="uploadDisabled">上傳</el-button>
          <el-button @click="handleResume" v-if="status === Status.pause">恢復(fù)</el-button>
          <el-button v-else :disabled="status !== Status.uploading || !container.hash" @click="handlePause">暫停 </el-button>
          </div>
          <div>
          <div>計(jì)算文件hash</div>
          <el-progress :percentage="hashPercentage"></el-progress>
          <div>總進(jìn)度</div>
          <!-- 每個(gè)blob 進(jìn)度 計(jì)算出來?
          1. 每塊blob 上傳 值percentage 變的, watch
          2. 計(jì)算屬性 computed -->

          <el-progress :percentage="fakeUploadPercentage"></el-progress>
          </div>
          <!-- 多個(gè)切片 -->
          <!-- [{a:1}] -->
          <el-table :data="data">
          <el-table-column prop="hash" label="切片hash" align="center">
          </el-table-column>
          <el-table-column label="大小(kb)" align="center" width="120">
          <template v-slot="{row}">
          {{row.size | transformByte}} </template>
          </el-table-column>
          <el-table-column label="進(jìn)度" align="center">
          <template v-slot="{row}">
          <el-progress :percentage="row.percentage" color="#909399">
          </el-progress>
          </template>
          </el-table-column>
          </el-table>
          </div>
          </template><script>
          const SIZE = 10 * 1024 * 1024; // 切片大小
          const Status = { wait: "wait", pause: "pause", uploading: "uploading"
          }; export default { name: 'app', filters: { transformByte(val) { return Number((val / 1024).toFixed(0))
          }
          }, computed: { uploadDisabled() { return (
          !this.container.file || [Status.pause, Status.uploading].includes(this.status)
          );
          }, uploadPercentage() { if (!this.container.file || !this.data.length) return 0; const loaded = this.data
          .map(item => item.size * item.percentage)
          .reduce((acc, cur) => acc + cur); return parseInt((loaded / this.container.file.size).toFixed(2));
          }
          }, watch: { uploadPercentage(now) { if (now > this.fakeUploadPercentage) { this.fakeUploadPercentage = now;
          }
          }
          }, data: () => ({
          Status, container: { file: null, hash: "", worker: null
          }, hashPercentage: 0, data: [], requestList: [], status: Status.wait, // 當(dāng)暫停時(shí)會取消 xhr 導(dǎo)致進(jìn)度條后退
          // 為了避免這種情況,需要定義一個(gè)假的進(jìn)度條
          fakeUploadPercentage: 0
          }), methods: { async handleResume() { this.status = Status.uploading; const {
          uploadedList
          } = await this.verifyUpload( this.container.file.name, this.container.hash
          ) await this.uploadChunks(uploadedList);
          }, handlePause() { this.status = Status.pause; // 狀態(tài)停
          this.resetData();
          }, resetData() { this.requestList.forEach(xhr => xhr.abort()) this.requestList = []; if (this.container.worker) { //hash 計(jì)算過程中
          this.container.worker.onmessage = null;
          }
          }, // xhr
          request({
          url,
          method = "post",
          data,
          headers = {},
          onProgress = e => e,
          requestList
          }
          )
          { return new Promise(resolve => { const xhr = new XMLHttpRequest();
          xhr.upload.onprogress = onProgress;
          xhr.open(method, url); Object.keys(headers).forEach(key =>
          xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => { // 將請求成功的 xhr 從列表中刪除
          if (requestList) { const xhrIndex = requestList.findIndex(item => item === xhr);
          requestList.splice(xhrIndex, 1);
          }
          resolve({ data: e.target.response
          });
          }; // 暴露當(dāng)前 xhr 給外部
          requestList?.push(xhr);
          });
          }, async calculateHash(fileChunkList) { return new Promise(resolve => { // 封裝花時(shí)間的任務(wù)
          // web workers
          // js 單線程的 UI 主線程
          // html5 web workers 單獨(dú)開一個(gè)線程 獨(dú)立于 worker
          // 回調(diào) 不會影響原來的UI
          // html5 帶來的優(yōu)化,
          this.container.worker = new Worker("/hash.js"); this.container.worker.postMessage({
          fileChunkList
          }); this.container.worker.onmessage = e => { // console.log(e.data);
          const {
          percentage,
          hash
          } = e.data; console.log(percentage, '----'); this.hashPercentage = percentage; if (hash) {
          resolve(hash);
          }

          }
          })
          }, async handleUpload(e) { // 大量的任務(wù)
          if (!this.container.file) return; this.status = Status.uploading; const fileChunkList = this.createFileChunk(this.container.file); console.log(fileChunkList); this.container.hash = await this.calculateHash(fileChunkList); // 文件 hash 沒必要上傳同一個(gè)文件多次
          const {
          shouldUpload,
          uploadedList
          } = await this.verifyUpload( //上傳, 驗(yàn)證
          this.container.file.name, this.container.hash
          ); console.log(shouldUpload, uploadedList); if (!shouldUpload) { this.$message.success("秒傳:上傳成功"); this.status = Status.wait; return;
          } this.data = fileChunkList.map(({
          file
          }, index
          ) =>
          ({ fileHash: this.container.hash, //文件的hash
          index, hash: this.container.hash + "-" + index, //每個(gè)塊都有自己的index 在內(nèi)的hash, 可排序, 可追蹤
          chunk: file, size: file.size, percentage: uploadedList.includes(index) ? 100 : 0 //當(dāng)前切片是否已上傳過
          })); await this.uploadChunks(uploadedList); //上傳切片
          }, // 上傳切片,同時(shí)過濾已上傳的切片
          async uploadChunks(uploadedList = []) { // console.log(this.data);
          // 數(shù)據(jù)數(shù)組this.data => 請求數(shù)組 =》 并發(fā)
          const requestList = this.data
          .filter(({
          hash
          }
          ) =>
          !uploadedList.includes(hash))
          .map(({
          chunk,
          hash,
          index
          }
          ) =>
          { const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          formData.append("fileHash", this.container.hash); return {
          formData,
          index
          };
          })
          .map(async ({
          formData,
          index
          }) => this.request({ url: "http://localhost:3000", data: formData, onProgress: this.createProgressHandler(this.data[index]), requestList: this.requestList
          })); await Promise.all(requestList); // 之前上傳的切片數(shù)量+本次上傳的切片數(shù)量=所有切片數(shù)量
          if (uploadedList.length + requestList.length == this.data.length) { await this.mergeRequest();
          } console.log('可以發(fā)送合并請求了');
          }, async mergeRequest() { await this.request({ url: 'http://localhost:3000/merge', headers: { "content-type": "application/json"
          }, data: JSON.stringify({ size: SIZE, fileHash: this.container.hash, filename: this.container.file.name
          })
          }) this.$message.success('上傳成功'); this.status = Status.wait;
          }, // 用閉包保存每個(gè) chunk 的進(jìn)度數(shù)據(jù)
          createProgressHandler(item) { return e => {
          item.percentage = parseInt(String((e.loaded / e.total) * 100)); console.log(e.loaded, e.total, '----------');
          }
          }, // 根據(jù) hash 驗(yàn)證文件是否曾經(jīng)已經(jīng)被上傳過
          // 沒有才進(jìn)行上傳
          async verifyUpload(filename, fileHash) { const {
          data
          } = await this.request({ url: 'http://localhost:3000/verify', headers: { "content-type": "application/json"
          }, data: JSON.stringify({ // 字符串化
          filename,
          fileHash
          })
          }) return JSON.parse(data);
          }, // es6的特性你和代碼是如何結(jié)合的?少傳這個(gè)參數(shù)
          createFileChunk(file, size = SIZE) { const fileChunkList = []; let cur = 0; while (cur < file.size) {
          fileChunkList.push({ file: file.slice(cur, cur + size)
          })
          cur += size;
          } return fileChunkList;
          }, handleFileChange(e) { const [file] = e.target.files; if (!file) return; this.resetData(); Object.assign(this.$data, this.$options.data()); this.container.file = file;
          },
          }, components: {

          }
          }
          </script>
          <style>
          #app { font-family: 'Avenir', Helvetica, Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px;
          }
          </style>

          秒傳

          原理:計(jì)算整個(gè)文件的hash,在執(zhí)行上傳操作前,向服務(wù)端發(fā)送請求,傳遞MD5值,后端進(jìn)行文件檢索。若服務(wù)器中已存在該文件,便不進(jìn)行后續(xù)的任何操作,上傳也便直接結(jié)束。

          大文件上傳 + 斷點(diǎn)續(xù)傳的解決方案就完成了

          總結(jié)

          • 前端上傳大文件時(shí)使用 Blob.prototype.slice 將文件切片,并發(fā)上傳多個(gè)切片,最后發(fā)送一個(gè)合并的請求通知服務(wù)端合并切片

          • 后端進(jìn)行合并到最終文件,  原生 XMLHttpRequestupload.onprogress 對切片上傳進(jìn)度的監(jiān)聽

          • 使用 spark-md5 根據(jù)文件內(nèi)容算出文件 hash, 通過 hash 可以判斷服務(wù)端是否已經(jīng)上傳該文件,從而直接提示用戶上傳成功(秒傳)

          • 前端在計(jì)算文件hash時(shí),能否異步并實(shí)現(xiàn)進(jìn)度響應(yīng)

          • 文件切片使用持久化或者內(nèi)存存儲導(dǎo)致溢出怎么辦?

          • “繼續(xù)下載”方案是否還有優(yōu)化空間?

          • 分片上傳、接收、存儲、合并,這些步驟抽象成一個(gè)文件上傳協(xié)議是否更理想

          • 上傳狀態(tài)由服務(wù)端動(dòng)態(tài)獲取,前端只做兩個(gè)事:hash和切片。這個(gè)前提下,多切片并發(fā)上傳、多文件并發(fā)上傳,復(fù)雜度會提高很多,當(dāng)然主要是后端復(fù)雜度。

          源代碼

          • file-breakpoint-continue


          瀏覽 90
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中国黄色学生妹一级片 | 人妻斩蜜桃视频网站 | 俺去也俺来也 | 亚洲殴洲国产黄片 | 加勒比国产在线 |