- 上傳文件視頻
-
File
是Blob(Binry Large Object)
的子類(lèi)誊涯,size
的單位是Byte
new File([u8arr], filename, { type: 'image/jpeg' });
// Blob對(duì)象通常用于存儲(chǔ)大量二進(jìn)制數(shù)據(jù)邮破,可以包含任意類(lèi)型的數(shù)據(jù)
new Blob([data], { type: 'application/octet-stream; charset=utf-8' })
// Uint8Array是ArrayBuffer的一種視圖類(lèi)型揍堕,是一個(gè)類(lèi)數(shù)組對(duì)象,
// 每項(xiàng)都是一個(gè)8 位無(wú)符號(hào)整數(shù)值蜒滩,可以直接讀取和寫(xiě)入二進(jìn)制數(shù)據(jù)。
u8ay = new Uint8Array(arraybuffer);
u8ay[0] = 10
// u8ay.buffer === arraybuffer
-
Blob
和File
類(lèi)型可以通過(guò)slice
進(jìn)行切片,通過(guò)FormData
的搭載來(lái)上傳到服務(wù)器
let formData = new FormData();
formData.append("Content-Disposition", `attachment; filename="${fileName}"`);
formData.append('key', key);
formData.append('file', file);
- 用
FileReader
讀取一個(gè)txt
格式的File
對(duì)象(編碼為UTF-8
):
readAsText.png
1. 如何將input選擇的圖片展示在頁(yè)面上
需要使用fileReader
來(lái)轉(zhuǎn)換文件:readAsDataURL
const reader = new FileReader();
reader.readAsDataURL(input.files[0]); // input.files 是一個(gè)FileList 玫氢,里面可以包含多個(gè)File文件
reader.onload = function (e) {
console.log(e.target.result); // 拿到base64,再賦值給img節(jié)點(diǎn)的src
}
2. 分片上傳 - 本文最下面有偽代碼
- 用
slice
將文件切分為多個(gè)Blob
類(lèi)型對(duì)象谜诫,存儲(chǔ)在數(shù)組里 - 計(jì)算文件的哈希值漾峡,先詢(xún)問(wèn)服務(wù)端,該文件是否上傳過(guò)喻旷,上傳到第幾片了
- 按分片順序生逸,循環(huán)-依次傳給服務(wù)器(攜帶文件的哈希值,用以標(biāo)識(shí)文件)且预,同時(shí)攜帶當(dāng)前處于第幾分片
- 當(dāng)最后一片上傳成功后槽袄,發(fā)送一個(gè)結(jié)束上傳的請(qǐng)求 - 攜帶文件哈希值- 后端返回文件地址
3. md5計(jì)算文件的哈希值——用以唯一標(biāo)識(shí)文件
- 思路是用
FileReader
將File readAsArrayBuffer,然后再對(duì)結(jié)果進(jìn)行md5運(yùn)算 - 但是大文件不能一次性全部讀到內(nèi)存里來(lái)锋谐,所以要用切片數(shù)組來(lái)計(jì)算整個(gè)文件的哈希值遍尺,需要用到
spark-md5
,以通過(guò)增量的方式計(jì)算整個(gè)文件的md5值涮拗,這通常需要一些時(shí)間 - 所以對(duì)文件哈希值的計(jì)算可能要使用
Web Worker
開(kāi)新線程乾戏,避免頁(yè)面卡住
4. 斷點(diǎn)續(xù)傳的觸發(fā)節(jié)點(diǎn)是什么?
4.1. 選擇文件上傳三热,服務(wù)端檢查發(fā)現(xiàn)此文件以前上傳過(guò)一部分(可能原因:1. 之前上傳過(guò)程中用戶(hù)關(guān)閉頁(yè)面 2. 之前上傳過(guò)程中網(wǎng)絡(luò)故障 3. 之前上傳手動(dòng)暫停)
4.2. 用戶(hù)手動(dòng)點(diǎn)了暫停歧蕉,再點(diǎn)擊繼續(xù),從暫停的部分繼續(xù)上傳
4.3. window
監(jiān)聽(tīng)online
事件康铭,發(fā)現(xiàn)有未完成的上傳任務(wù)惯退,繼續(xù)上傳(例如斷網(wǎng)導(dǎo)致上傳中斷,前端記錄了斷點(diǎn)信息从藤。但現(xiàn)在遇到的用戶(hù)的操作一般不是單純的上傳催跪,上傳完后要拿到url去做后續(xù)的操作锁蠕,例如把文件地址保存到某個(gè)單據(jù)上, 但如果斷網(wǎng)導(dǎo)致上次的用戶(hù)操作已經(jīng)中斷了懊蒸,現(xiàn)在即使續(xù)傳完畢荣倾,也不知道把拿到的url怎么辦了。既然用戶(hù)操作中斷骑丸,下次應(yīng)該會(huì)重新操作舌仍,在重新上傳文件時(shí)觸發(fā)第一種情況下的斷點(diǎn)續(xù)傳,我覺(jué)得這樣更合理一點(diǎn)通危,而不是online時(shí)自動(dòng)繼續(xù)上傳)
5. 上傳進(jìn)度
- axios的
onUploadProgress
選項(xiàng) - web版本監(jiān)聽(tīng)的是
ajax
的progress
事件 - 客戶(hù)端版本是調(diào)客戶(hù)端方法進(jìn)行的分片铸豁,回調(diào)里面通知前端 “文件的總大小、當(dāng)前進(jìn)度菊碟、文件的key”节芥,當(dāng)進(jìn)度為100%的時(shí)候前端通過(guò)key獲取文件在線地址
- 自己做分片上傳的進(jìn)度就是已上傳的size / 總size
6. 粘貼圖片后上傳
6.1 監(jiān)聽(tīng)paste
事件,獲取clipboardData
el.addEventListener('paste', function (e) {
const cbd = e.clipboardData;
if (!(cbd && cbd.items)) {
return;
}
let pasteItems = cbd.items;
}, false);
6.2 如果直接能獲取到文件數(shù)據(jù):getAsFile
可以拿到file
類(lèi)型逆害,此時(shí)就可以上傳了头镊,用 FileReader
讀取base64
后也能展示到頁(yè)面上
for (let i = 0, lth = pasteItems.length; i < lth; i++) {
let item = pasteItems[i];
if (item.kind === 'file') {
let file = item.getAsFile(); // 第一步:getAsFile
if (!file || file.size === 0) {
continue;
}
// 第二步:這里再用 FileReader 來(lái)讀這個(gè) blob: readAsDataURL,結(jié)果就能作為 src 賦值給 img 了
}
}
6.3 否則嘗試以html格式解析文本:getAsString
魄幕、new DOMParser().parseFromString(a, 'text/html')
相艇,拿到內(nèi)部img
標(biāo)簽的src
屬性
for (let i = 0, lth = pasteItems.length; i < lth; i++) {
pasteItems[i].getAsString(function (a) {
if (!a) {
return;
}
let nodes = new DOMParser().parseFromString(a, 'text/html'), // 這步很重要
imgDomList = nodes.getElementsByTagName('img'); // 獲取IMG DOM
// 接下來(lái)就可以取 IMG 節(jié)點(diǎn)的 src 屬性了
});
}
從 img dom 取出來(lái)的 src 可能是:
-
file:///C:/Users/cache/Image/2008208101184.png
—— 【本地文件系統(tǒng)】的地址
(本地路徑我們是無(wú)法展示的,目前借助客戶(hù)端纯陨,先把本地路徑給客戶(hù)端厂捞,客戶(hù)端上傳后返給我們?cè)诰€鏈接) -
https://upload-images.jianshu.io/upload_images/1552a9bb5bd4.png
—— 【在線】地址
7. 在瀏覽器地址欄輸入文件地址時(shí),為什么有的是預(yù)覽队丝,有的直接會(huì)下載
服務(wù)器可以通過(guò)設(shè)置響應(yīng)頭來(lái)指定文件的 MIME 類(lèi)型和處理方式。如果服務(wù)器發(fā)送的響應(yīng)頭中包含了 "Content-Disposition: attachment"
欲鹏,則瀏覽器會(huì)將其視為需要下載的文件机久,而不是直接打開(kāi)預(yù)覽。
8. 為什么有的網(wǎng)站圖片只能在這個(gè)網(wǎng)站內(nèi)部才能訪問(wèn)赔嚎?
有的網(wǎng)站設(shè)置了防盜鏈:
- 例如簡(jiǎn)書(shū)通過(guò)請(qǐng)求頭中的
Referer
來(lái)判斷來(lái)源膘盖,不允許的來(lái)源就會(huì)返回403 Forbidden
(但Referer信息可以偽造,也可以不傳尤误,例如<meta name="referrer" content="no-referrer" />
侠畔,沒(méi)傳的話就能訪問(wèn)) - 例如群暉是通過(guò)一個(gè)
session ID
來(lái)檢查這個(gè)資源請(qǐng)求的合法性(這個(gè)sid
是要用賬號(hào)成功登錄群暉服務(wù)器才能生成并返回的,默認(rèn)放在cookie
里损晤,也存在 有效期)软棺,請(qǐng)求時(shí)拼接在【url地址上】或者【通過(guò)cookie】帶過(guò)去(資源請(qǐng)求默認(rèn)不會(huì)攜帶cookie),服務(wù)端校驗(yàn)通過(guò)才會(huì)返回正確資源尤勋〈洌【如果你是通過(guò)攜帶sid參數(shù)拼接到鏈接上的方式去獲取茵宪,短時(shí)別人拿你的請(qǐng)求地址也能訪問(wèn),不過(guò)sid失效后就無(wú)法訪問(wèn)了】 - 一個(gè)工具:在完整url前添加
https://images.weserv.nl/?url=
可以預(yù)覽這個(gè)圖片
9. 下載碰到過(guò)的一個(gè)問(wèn)題
下載文件時(shí)發(fā)現(xiàn)含有中文逗號(hào)的無(wú)法下載瘦棋。而這個(gè)報(bào)錯(cuò)碼表示服務(wù)器在響應(yīng)頭部中設(shè)置了多個(gè) Content-Disposition 字段稀火。
原因:上傳文件的時(shí)候會(huì)把到時(shí)下載要用的Content-Disposition
頭的值傳給后端,但是由于filename沒(méi)有包雙引號(hào)赌朋,導(dǎo)致設(shè)置請(qǐng)求頭的時(shí)候變成 a=xxxx凰狞,aaaa,文件名中的逗號(hào)當(dāng)作屬性分割了沛慢。
修改:上傳時(shí)用雙引號(hào)包上文件名赡若,前后對(duì)比:
- image.png
|
- image.png
|
---|
10. StreamSaver流式下載文件,并讀取下載進(jìn)度
(參見(jiàn)另一篇 service worker)
const fileStream = streamSaver.createWriteStream(fileName); // WritableStream實(shí)例
const writer = fileStream.getWriter();
fetch(url).then(res => {
const totalSize = parseInt(res.headers.get('content-length'));
let loadedSize = 0;
const readableStream = res.body;
const reader = readableStream.getReader();
reader.read().then(function processResult(result) {
if (result.done) {
writer.close(); // 下載完成
return;
}
loadedSize += result.value.length;
console.log(`下載進(jìn)度:${loadedSize / totalSize * 100}%`);
// 讀過(guò)進(jìn)度之后颠焦,可以繼續(xù)寫(xiě)入可寫(xiě)流當(dāng)中
writer.write(result.value).catch(error => {
writer.close();
});
return reader.read().then(processResult);
});
})
11. 分片上傳偽代碼
const STATE_UPLOADING = 'sending', // 正在上傳
STATE_STOP = 'stop', // 各種原因?qū)е碌闹袛? STATE_SUCCESS = 'success'; // 成功
const CHUNK_SIZE = 10 * 1024 * 1024; // 切成10M
// const CHUNK_SIZE = 10 * 1024; // 切成10K
const uploadFile = Symbol('uploadFile');
const continueUpload = Symbol('continueUpload');
const compPort = Symbol('componentPort');
const changeState = Symbol('changeState');
const handleUploadFail = Symbol('handleUploadFail');
const updatePercent = Symbol('updatePercent');
const tools = {
calcSign(file) { },
getFileUrl(fileSign, fileSize) { },
postToServer(fileSign, chunkIndex, chunkFile, signal) { },
checkFileFromServer(fileSign) { }
};
export default class Upload {
state = STATE_UPLOADING
stopChunkIndex = 0
fileSign = null // 真正用的地方就continueUpload一個(gè)斩熊,調(diào)用位置在:初始化、從暫停處繼續(xù)
file = null
fileUrl = ''
messagePort = null
percent = 0
constructor(f, options = {}) {
this.file = f;
this.fileSign = options.fileSign || tools.calcSign(f);
if (options.startIndex) {
this.stopChunkIndex = options.startIndex;
}
this[uploadFile]();
// 接下來(lái)用于實(shí)例之間通信:去通知實(shí)例上傳狀態(tài)發(fā)生變化伐庭、進(jìn)度發(fā)生變化
// 實(shí)例需要知道的話就用messagePort去監(jiān)聽(tīng)message事件
const { port1, port2 } = new MessageChannel();
this[compPort] = port1;
this.messagePort = port2;
}
// 暫停上傳
suspend() {
this[changeState](STATE_STOP);
this.abortController.abort();
}
// 繼續(xù)上傳
proceed() {
if (this.state === STATE_STOP) {
this[changeState](STATE_UPLOADING);
this[continueUpload]().catch((...args) => {
this[handleUploadFail](...args);
});
}
}
// 上傳文件粉渠,成功則resolve url,失敗會(huì)reject error
async [uploadFile]() {
// 檢查這個(gè)文件是否上傳過(guò)
let res = await tools.checkFileFromServer(this.fileSign);
// 已上傳完畢:秒傳
if (res.url) {
this.fileUrl = res.url;
this[updatePercent](100);
this[changeState](STATE_SUCCESS);
return res.url;
}
// 上傳了一部分:從上次位置續(xù)傳
if (res.uploadedChunkCount > 0) {
this.stopChunkIndex = res.uploadedChunkCount;
}
// 沒(méi)傳過(guò):傳
return this[continueUpload]().catch((...args) => {
this[handleUploadFail](...args);
});
}
async [continueUpload]() {
this.abortController = new AbortController();
const startIndex = this.stopChunkIndex;
const fileSize = this.file.size;
let transferSize = startIndex * CHUNK_SIZE, // 已上傳的部分
chunkIndex = startIndex;
let isErr = false;
while (transferSize < fileSize) {
if (this.state === STATE_STOP) { // 暫停
this.stopChunkIndex = chunkIndex;
return Promise.reject({ code: 300 });
}
let chunkFile = this.file.slice(transferSize, transferSize + CHUNK_SIZE);
try {
let res = await tools.postToServer(this.fileSign, chunkIndex, chunkFile, this.abortController.signal);
if (res.ok) {
transferSize += chunkFile.size;
chunkIndex++;
this[updatePercent](transferSize / fileSize * 100);
} else {
isErr = true;
break;
}
} catch (err) {
console.log('fetch error:', err);
isErr = true;
break;
}
}
if (isErr) { // 分片上傳中斷
this.stopChunkIndex = chunkIndex;
return Promise.reject({ code: 100 });
}
// 上傳完成圾另,獲取下載地址
return tools.getFileUrl(this.fileSign, fileSize).then(url => {
this.fileUrl = url;
this[changeState](STATE_SUCCESS);
return url;
}).catch(() => {
return Promise.reject({ code: 400, });
});
}
[changeState](state) {
if (this.state === state) {
return;
}
// 成功了就不能再變更狀態(tài)了
if (this.state === STATE_SUCCESS) {
return;
}
this.state = state;
this[compPort].postMessage({ action: 'statechange', state });
}
// 該文件的上傳進(jìn)度
[updatePercent](value) {
value = parseInt(value);
value = Math.min(value, 100);
this[compPort].postMessage({ action: 'progress', value, text: value + '%' });
}
// 統(tǒng)一處理上傳失敗
[handleUploadFail](err) {
const errInfo = {
'100': '分片上傳中斷',
'400': '已全部上傳霸株,但獲取文件地址失敗',
'300': '用戶(hù)暫停上傳'
};
this[changeState](STATE_STOP);
if (err.code === 100) {
// 存到localStorage,便于下次續(xù)傳集乔,記錄file去件、fileSign、斷點(diǎn)位置
// 在合適的節(jié)點(diǎn)監(jiān)聽(tīng)online-如果localStorage有未完成的任務(wù)則繼續(xù)上傳 -> new Upload(file, {fileSign, startIndex})
// localStorage.setItem();
return;
}
}
};
12. 遇到的問(wèn)題記錄:
問(wèn)題1:通知實(shí)例狀態(tài)的更新和進(jìn)度的更新
→ 現(xiàn)在用的MessageChannel
扰路,給實(shí)例一個(gè)port
讓它自己去監(jiān)聽(tīng)messgae
問(wèn)題2:點(diǎn)了暫停之后進(jìn)度條還會(huì)再更新一次
→ 因?yàn)槲沂窃谙乱粋€(gè)分片發(fā)送前才校驗(yàn)并停止發(fā)送請(qǐng)求尤溜,所以即使點(diǎn)了暫停,本次分片還是會(huì)上傳完汗唱,并且更新一次進(jìn)度宫莱。所以如果在最后一個(gè)分片正在上傳時(shí)點(diǎn)暫停,實(shí)際上是沒(méi)有用的哩罪,你看著進(jìn)度條沒(méi)滿(mǎn)的時(shí)候點(diǎn)的暫停授霸,但上傳結(jié)束了。
→ 所以點(diǎn)暫停時(shí)應(yīng)該abort
當(dāng)前正在發(fā)生的請(qǐng)求(給請(qǐng)求一個(gè)abortController.signal
际插,在點(diǎn)擊暫停時(shí)調(diào)用abortController.abort()
)