前端大文件上傳解決方案-分片上傳
文件上傳功能
我們先看一個antd的文件上傳的案例
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { UploadOutlined } from '@ant-design/icons';
import { Button, message, Upload } from 'antd';
const props = {
name: 'file',
action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
headers: {
authorization: 'authorization-text',
},
onChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
};
const App = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
);
export default App;
我們可以發(fā)現(xiàn),文件的上傳只進行了一次請求。
前端在處理文件上傳時,通常也是一次性發(fā)送到server端,如果遇到大文件的時候,xhr請求會處理很長時間,這就大大增加了失敗的概率,通常我們會將大文件切片然后發(fā)送,也就是說將一個大文件的上傳問題轉化為多個小文件上傳的問題。
文件類型
先上一段簡單的單文件上傳和兩個類型定義
/** A file-like object of immutable, raw data.Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. */
interface Blob {
readonly size: number;
readonly type: string;
arrayBuffer(): Promise; // 字節(jié)數(shù)組
slice(start?: number, end?: number, contentType?: string): Blob; // 切片的核心
stream(): ReadableStream; // 返回ReadableStream對象(包含getReader())Blob 的內(nèi)容
text(): Promise; // promise中返回 USVString 基本為 UTF-8 的blob字符串數(shù)據(jù)接近FileReader 的 readAsText()
}
/** Provides information about files and allows JavaScript in a web page to access their content. */
interface File extends Blob {
readonly lastModified: number;
readonly name: string;
}
const upload = async (file: File) => {
if (!file) {
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('hash', 'true');
const res = await fileUpload(formData);
}
通過定義我們知道, Blob是一個不可變、存儲文件原數(shù)據(jù)的一個類文件,但其并非是JS的原生數(shù)據(jù),而 File繼承于 Blob,使得 Blob信息擴展為用戶操作系統(tǒng)可支持的文件,并使得頁面里可以使用 Javascript訪問其文件信息。
分片上傳
切片
下面我們來對文件進行切片操作
/**
* 文件切片
* @param {File} file 切片文件
* @param {number} pieceSize 切片大小
* @param {string} fileKey 文件唯一標識
*/
const getSliceFile = async (file: File, pieceSizes = 50, fileKey: string) => {
const piece = 1024 * 1024 * pieceSizes;
// 文件總大小
const totalSize = file.size;
const fileName = file.name;
// 每次上傳的開始字節(jié)
let start = 0;
let index = 1;
// 每次上傳的結尾字節(jié)
let end = start + piece;
const chunks = [];
while (start < totalSize) {
const current = Math.min(end, totalSize);
// 根據(jù)長度截取每次需要上傳的數(shù)據(jù)
// File對象繼承自Blob對象,因此包含slice方法
const blob = file.slice(start, current);
const hash = (await getHash(blob)) as string;
// 特殊處理,對接阿里云大文件上傳
chunks.push({
file: blob,
size: totalSize,
index,
fileSizeInByte: totalSize,
name: fileName,
fileName,
hash,
sliceSizeInByte: blob.size,
fileKey,
});
start = current;
end = start + piece;
index += 1;
}
return chunks;
};
Promise.all
文件被分成若干塊后,需要確保每一塊兒都上傳成功,也就是若干請求都成功,首先想到了Promise.all。
// 獲取promise數(shù)組
const getTasks = (
files: FileInfo[],
uploadId: string,
fileKey: string,
finish: number[],
): Promise<commonresponse_largefileuploadresponse_>[] => {
const tasks: Promise<commonresponse_largefileuploadresponse_>[] = [];
const currentTaskIndex: number[] = [];
files.forEach((chunk: FileInfo) => {
if (finish.includes(chunk.index)) {
return;
}
currentTaskIndex.push(chunk.index);
const formData = new FormData();
formData.append('file', chunk.file);
// @ts-ignore
formData.append('sliceIndex', chunk.index);
formData.append('hash', chunk.hash);
formData.append('uploadId', uploadId);
// @ts-ignore
formData.append('fileSizeInByte', chunk.sliceSizeInByte);
tasks.push(sliceUpload(formData));
});
return tasks;
};
瀏覽器連接數(shù)瓶頸
但是這樣操作我們就會看到如下圖這種結果,先說說下圖的起因一次將所有的分片發(fā)出去,由于瀏覽器對同一個域名連接數(shù)量有限制(如:chrome是6個連接,各個瀏覽器版本和HTTP協(xié)議版本的連接數(shù)有些許差距,但是大部分都在6個左右,原則上是不超過10個連接數(shù)),這導致大量請求處于pending狀態(tài)(也就是排隊,hold在了瀏覽器,沒有發(fā)出去),后面的請求可能因為排隊而超時(超時的請求瀏覽會自動cancel了),只能將請求的超時時間設置的長一些(但是這個時間不好確定);而且還會阻塞了同域下的別的請求,這可能導致頁面不能響應UI交互了。
基于上述問題,不能一次將請求全部發(fā)出去,那么需要確定什么時候發(fā)請求并且需要知道文件什么時候能全部上傳完畢。可以通過執(zhí)行棧或者隊列的方式,一次往?;蛘哧犃屑尤氩怀^3個的執(zhí)行單元,再通過狀態(tài)請求接口輪詢或者Promise.allSettled()獲取對應promise異步請求的結果來更新棧或者隊列,具體操作這里不再贅述。
性能優(yōu)化 Web Workers
大家都知道js是單線程,但是如果有多線程的方案那么對性能的影響將是巨大的,而不阻塞主線程之后體驗上也會有極大改善。一個 workers 是使用一個構造函數(shù)創(chuàng)建的一個對象(e.g. Worker()) 運行一個命名的 JavaScript 文件 - 這個文件包含將在工作線程中運行的代碼; workers 運行在另一個全局上下文中,不同于當前的window. 因此,在 Worker 內(nèi)通過 window 獲取全局作用域 和 DOM 將返回錯誤。所以你可以理解為 worker 就是新建了一個私有的有別于 window 主線程的工作線程。因為 workers 實體是js腳本,想使用的同學可以去了解下worker-plugin,可以防止babel轉換附帶進額外信息導致 workers 腳本失效。
// 創(chuàng)建Workers
const myWorker = new Worker('worker.js');
// 界面發(fā)送消息給myWorker
input1.onchange = function() {
myWorker.postMessage([input1.value,input2.value]);
console.log('Message posted to worker');
}
input2.onchange = function() {
myWorker.postMessage([input1.value,input2.value]);
console.log('Message posted to worker');
}
// 相應workers的返回
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
// worker.js 獲取兩個參數(shù)并且求和返回
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
通過上面例子其實我們可以看出workers的本質(zhì)是運行腳本js,既然是資源那么是在client端的,所以這里就要注意同源跨域的問題,同時意味著web Workers是可以被多個window等共享使用的,也存在 SharedWorker 但是共享workers的端口配置等就會復雜不少。官方現(xiàn)在好像還沒有<script>,腳本的形式引入web workers的js, 但是可以通過 <script type="text/js-worker"> 嵌入而這種方式其實就是比較常見的數(shù)據(jù)塊的形式,而對于數(shù)據(jù)塊可以像下面一樣將函數(shù)數(shù)據(jù)化,而返回的帶hash的url, 前端的不通過后端下載指定數(shù)據(jù)都可以通過下面類似的方式獲取下載的url。
function fn2workerURL(fn) {
var blob = new Blob(['('+fn.toString()+')()'], {type: 'application/javascript'})
return URL.createObjectURL(blob)
}
想嘗試的同學可以嘗試本地谷歌打開這個HTML會收到 Received: Hello World!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>MDN Example - Embedded worker</title>
<script type="text/js-worker">
// 該腳本不會被 JS 引擎解析,因為它的 mime-type 是 text/js-worker。
var myVar = "Hello World!";
// 剩下的 worker 代碼寫到這里。
</script>
<script type="text/javascript">
// 該腳本會被 JS 引擎解析,因為它的 mime-type 是 text/javascript。
function pageLog (sMsg) {
// 使用 fragment:這樣瀏覽器只會進行一次渲染/重排。
var oFragm = document.createDocumentFragment();
oFragm.appendChild(document.createTextNode(sMsg));
oFragm.appendChild(document.createElement("br"));
document.querySelector("#logDisplay").appendChild(oFragm);
}
</script>
<script type="text/js-worker">
// 該腳本不會被 JS 引擎解析,因為它的 mime-type 是 text/js-worker。
onmessage = function (oEvent) {
postMessage(myVar);
};
// 剩下的 worker 代碼寫到這里。
</script>
<script type="text/javascript">
// 該腳本會被 JS 引擎解析,因為它的 mime-type 是 text/javascript。
// 在過去...:
// 我們使用 blob builder
// ...但是現(xiàn)在我們使用 Blob...:
var blob = new Blob(Array.prototype.map.call(document.querySelectorAll("script[type=\"text\/js-worker\"]"), function (oScript) { return oScript.textContent; }),{type: "text/javascript"});
// 創(chuàng)建一個新的 document.worker 屬性,包含所有 "text/js-worker" 腳本。
document.worker = new Worker(window.URL.createObjectURL(blob));
document.worker.onmessage = function (oEvent) {
pageLog("Received: " + oEvent.data);
};
// 啟動 worker.
window.onload = function() { document.worker.postMessage(""); };
</script>
</head>
<body><div id="logDisplay"></div></body>
</html>
小伙伴們你們知道怎么辦了嗎?趕緊Get下來,自己試一試吧!

猜你喜歡LIKE
相關推薦HOT
更多>>
json格式是什么意思
JSON(JavaScript Object Notation)是一種輕量級的數(shù)據(jù)交換格式,常用于前后端數(shù)據(jù)傳輸、配置文件和日志文件等場景中。JSON 采用鍵值對的方式來...詳情>>
2023-04-11 13:51:20
什么是mybatisplus?有什么特點
Mybatis-Plus(簡稱MP)是一個基于Mybatis的持久開源層框架,它在Mybatis的基礎上擴展了一些實用的功能,使開發(fā)更加簡單、快速。以下是Mybatis-Pl...詳情>>
2023-03-06 16:05:42
zookeeper集群配置怎樣操作
ZooKeeper是一個分布式應用程序協(xié)調(diào)服務,它使用一組服務器來提供高可用性和容錯性。要配置ZooKeeper集群,需要完成以下步驟:1.下載和安裝ZooK...詳情>>
2023-03-03 11:23:01
性能測試的指標是什么
性能測試是一種通過測量系統(tǒng)或應用程序的特定方面來評估其性能的測試方法。性能測試可以幫助發(fā)現(xiàn)性能瓶頸,優(yōu)化應用程序或系統(tǒng)的性能,從而提高...詳情>>
2023-03-01 10:11:00熱門推薦
技術干貨







快速通道 更多>>
-
課程介紹
點擊獲取大綱 -
就業(yè)前景
查看就業(yè)薪資 -
學習費用
了解課程價格 -
優(yōu)惠活動
領取優(yōu)惠券 -
學習資源
領3000G教程 -
師資團隊
了解師資團隊 -
實戰(zhàn)項目
獲取項目源碼 -
開班地區(qū)
查看來校路線