github倉庫
要了解文件上傳粉臊,首先需要了解blob對象。
Blob
全稱是 binary large object,二進(jìn)制大文件對象驶兜,非 Javascript 特有扼仲,計算機(jī)通用對象,不可修改抄淑。
構(gòu)造函數(shù):Blob
new Blob(blobBits[, options])
blobBits:一個數(shù)組屠凶,成員可以是二進(jìn)制對象或字符串,表示 Blob 的內(nèi)容肆资。
options:可選參數(shù)矗愧,可以設(shè)置以下兩個屬性:
- type:默認(rèn)值為 "",它代表了將會被放入到 blob 中的數(shù)組內(nèi)容的 MIME 類型郑原。
- endings:表示換行符的類型唉韭,可以是"transparent"、"native"或"lf"犯犁、"cr"属愤、"crlf"。默認(rèn)為"transparent"酸役。
Blob的核心方法:
slice([start[, end[, contentType]]])
返回一個新的 Blob 對象住诸,包含原 Blob 對象的一部分?jǐn)?shù)據(jù)驾胆。
- start:可選,類型為 Number贱呐,表示開始位置的索引丧诺,默認(rèn)值為 0。
- end:可選奄薇,類型為 Number锅必,表示結(jié)束位置的索引,默認(rèn)值為 blob 的大小惕艳。
- contentType:可選搞隐,類型為 String,表示新 Blob 對象的 MIME 類型远搪,默認(rèn)值為原 Blob 對象的 MIME 類型劣纲。
例子:
const myBlob = new Blob(["了解Blob"], { type: "text/plain" });
使用場景:
1、分片上傳
File 對象是特殊類型的 Blob谁鳍,且可以用在任意的 Blob 類型的上下文中癞季。所以針對大文件傳輸?shù)膱鼍埃覀兛梢允褂?slice 方法對大文件進(jìn)行切割倘潜,然后分片進(jìn)行上傳绷柒,具體示例如下:
const file = new File(["a".repeat(1000000)], "test.txt");
const chunkSize = 40000;
const url = "https://httpbin.org/post";
async function chunkedUpload() {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append("data", chunk);
await fetch(url, { method: "post", body: fd }).then((res) =>
res.text()
);
}
}
2、預(yù)覽圖片
/* 使用blob預(yù)覽 */
const fileDom = document.getElementById("file");
const file = e.target.files[0];
const img = document.getElementById("img");
// File是Blob的子類涮因,所以可以直接使用Blob的方法
// const blob = new Blob([file], { type: "image/png" });
img.src = URL.createObjectURL(blob);
/* 使用base64預(yù)覽 */
const fileDom = document.getElementById("file");
const file = e.target.files[0];
const img = document.getElementById("img");
// FileReader將file轉(zhuǎn)換為base64
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = (e) => {
img.src = e.target.result
};
3废睦、下載文件
fetch('xxxx')
.then(function (response) {
// 使用fetch獲取到的response對象的blob方法,將文件轉(zhuǎn)換為blob對象
return response.blob();
})
.then(function (myBlob) {
/*
URL.createObjectURL(blob)生成的 URL 存儲了一個 URL → Blob 映射养泡。
但實際上它也有副作用嗜湃。雖然存儲了 URL → Blob 的映射,但 Blob 本身仍駐留在內(nèi)存中澜掩,瀏覽器無法釋放它购披。映射在文檔卸載時自動清除,因此 Blob 對象隨后被釋放肩榕。
但是刚陡,如果應(yīng)用程序壽命很長,那不會很快發(fā)生株汉。因此筐乳,如果我們創(chuàng)建一個 Blob URL,即使不再需要該 Blob郎逃,它也會存在內(nèi)存中哥童。
對這個問題,我們可以調(diào)用 URL.revokeObjectURL(url) 方法褒翰,從內(nèi)部映射中刪除引用,從而允許刪除 Blob
*/
let objectURL = URL.createObjectURL(myBlob);
const link = document.createElement("a");
// 設(shè)置下載鏈接
link.href = objectURL;
// 文件名稱(類型)
ink.download = 'test.png';
ink.click();
ink.remove();
URL.revokeObjectURL(link.href);
});
File
const file = document.getElementById("file");
// 獲取文件
const file = input.current.files[0];
// file文件是File類型
console.log(file instanceof File); // true
File 對象代表一個文件,用來讀寫文件信息优训。它繼承了 Blob 對象朵你,或者說是一種特殊的 Blob 對象,所有可以使用 Blob 對象的場合都可以使用它揣非。
構(gòu)造函數(shù):File
new File(fileBits, fileName[, options])
fileBits:一個數(shù)組抡医,成員可以是二進(jìn)制對象或字符串,表示文件的內(nèi)容早敬。
fileName:文件名忌傻。
options:可選參數(shù),可以設(shè)置以下兩個屬性:
- lastModified:文件最后修改時間搞监,單位為毫秒水孩,如果不傳入該參數(shù),會使用當(dāng)前時間琐驴。
- type:文件類型俘种,如果不傳入該參數(shù),會根據(jù)文件名自動判斷绝淡。(MIME)
例子:
var file = new File(["hello world"], "hello.txt", {
type: "text/plain",
});
適用于Blob的所有方法宙刘,F(xiàn)ile也可以使用
- 切片上傳
- 預(yù)覽圖片
- 下載文件
FileReader
FileReader是一種異步讀取文件機(jī)制。
FileReader提供了如下方法:
- readAsDataURL(file):讀取文件內(nèi)容牢酵,結(jié)果用data:url的字符串形式表示(base64)
- readAsText(file, '編碼方式'):按字符讀取文件內(nèi)容悬包,結(jié)果用字符串形式表示
- abort():終止文件讀取操作
例子:
var reader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
var txt = reader.result;
}
FormData
FormData提供了一種表示表單數(shù)據(jù)的鍵值對 key/value 的構(gòu)造方式,我們可以異步上傳一個二進(jìn)制文件(Blob)馍乙。
const formData = new FormData();
formData.append("file", file);
...
- formData.append():添向 FormData 中添加新的屬性值玉罐,F(xiàn)ormData 對應(yīng)的屬性值存在也不會覆蓋原值,而是新增一個值潘拨,如果屬性不存在則新增一項屬性值吊输。
- formData.get():獲取一個鍵值對
- formData.getAll():獲取所有鍵值對
- formData.has():判斷是否存在某個鍵值對
- formData.delete():刪除某個鍵值對
- formData.set():設(shè)置某個鍵值對
上傳文件主要分為兩種方式:點擊上傳以及拖拽上傳。
點擊上傳
// 單文件上傳
const App = () => {
const change = e => {
const file = e.target.files[0];
// 我們可以通過file.type來獲取文件的類型铁追,從而判斷是否是圖片或者限制上傳的文件類型
// const type = file.type;
const formData = new FormData();
formData.append('file', file);
axios.post('http://localhost:1111/upload', formData).then(res => {
console.log(res);
}
);
};
// accept屬性用來限制上傳的文件類型季蚂,可以是圖片、視頻琅束、音頻等
return (
<div className={styles.root}>
<input type="file" onChange={change} /* accept=".jpeg,.png" */ />
</div>
);
};
// 多文件上傳
/*
遍歷上傳
我們可以給input標(biāo)簽添加multiple屬性扭屁,這樣就可以上傳多個文件了。
然后再通過e.target.files獲取到所有的文件涩禀,然后遍歷上傳即可料滥。
*/
/*
合并上傳
我們可以將所有的文件合并成一個FormData對象,然后一次性上傳艾船。
但是這樣會有一個問題葵腹,就是后端無法區(qū)分每個文件高每,所以我們需要在FormData對象中添加一個字段,用來區(qū)分每個文件践宴。
例如:formData.append('file', file, file.name);
這樣后端就可以通過file.name來區(qū)分每個文件了鲸匿。
*/
/*
我們也可以不設(shè)置multiple屬性,達(dá)到多文件上傳的目的(一個一個的添加阻肩,緩存進(jìn)數(shù)組里面再一次性上傳)
*/
圖片預(yù)覽
// 圖片預(yù)覽
const App = () => {
const [base64, setBase64] = useState('');
const change = e => {
// file轉(zhuǎn)base64預(yù)覽
// const file = e.target.files[0];
// const fileReader = new FileReader();
// fileReader.readAsDataURL(file);
// fileReader.onload = e => {
// setBase64(e.target.result);
// };
// url -> blob映射預(yù)覽
const file = e.target.files[0];
const url = URL.createObjectURL(file);
setBase64(url);
};
return (
<div className={styles.root}>
<input type="file" onChange={change} />
<img src={base64} />
</div>
);
};
服務(wù)器文件下載
// 設(shè)置responseType為blob
// 接口數(shù)據(jù)返回的是一個blob類型的數(shù)據(jù)带欢,我們可以通過URL.createObjectURL()方法將其轉(zhuǎn)換成一個url,然后通過a標(biāo)簽的download屬性來下載文件烤惊。
const App = () => {
useEffect(() => {
axios.get('http://localhost:1111/download',{
responseType: 'blob'
}).then(res => {
const url = URL.createObjectURL(res.data);
const a = document.createElement('a');
a.href = url;
a.download = 'test.png';
a.click();
URL.revokeObjectURL(url);
});
}, []);
return (
<div className={styles.root}></div>
);
};
切片上傳
// 設(shè)置每個片段的大小chunkSize
// 使用blob的slice分片乔煞,大小根據(jù)chunkSize以及上傳到的start決定。直到start > file.size
const App = () => {
const change = e => {
const file = e.target.files[0];
const chunkSize = 4000;
const url = "http://localhost:1111/upload";
async function chunkedUpload() {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append("file", chunk);
await axios.post(url, fd);
}
}
chunkedUpload();
};
return (
<div className={styles.root}>
<input type="file" onChange={change} />
</div>
);
};
斷點續(xù)傳
// 通過current記錄當(dāng)前上傳到的位置柒室,通過pauseRef.current來判斷是否暫停
// chunkCount = Math.ceil(fileSize / chunkSize) 計算出總共需要上傳的片段數(shù)
const App = () => {
const [file, setFile] = useState({});
const [current, setCurrent] = useState(0);
const pauseRef = useRef(false);
const [pause, setPause] = useState(false);
const change = e => {
const _file = e.target.files[0];
setFile(_file);
};
const upload = async file => {
const chunkSize = 4000;
const url = "http://localhost:1111/upload";
const fileSize = file.size;
const chunkCount = Math.ceil(fileSize / chunkSize);
for (let i = current; i < chunkCount; i+=1) {
if (pauseRef.current) return;
const start = i * chunkSize;
const end = (i + 1) * chunkSize;
const chunk = file.slice(start, end);
const fd = new FormData();
fd.append("file", chunk);
await axios.post(url, fd);
setCurrent(i + 1);
}
};
const clickPause = () => {
pauseRef.current = !pauseRef.current;
setPause(pauseRef.current);
if (!pauseRef.current) upload(file);
};
return (
<div className={styles.root}>
<input type="file" onChange={change} />
<button onClick={() => upload(file)}>上傳</button>
<button onClick={clickPause}>
{pause ? '繼續(xù)' : '暫停'}
</button>
</div>
);
};
上傳進(jìn)度
- 大文件切片上傳渡贾,可以通過計算每個片段的大小,然后計算出總共需要上傳的片段數(shù)伦泥,然后通過current / chunkCount來計算出進(jìn)度剥啤。
const App = () => {
const [file, setFile] = useState({});
const [current, setCurrent] = useState(0);
const [progress, setProgress] = useState(0);
const pauseRef = useRef(false);
const [pause, setPause] = useState(false);
const change = e => {
const _file = e.target.files[0];
setFile(_file);
};
const upload = async file => {
const chunkSize = 4000;
const url = "http://localhost:1111/upload";
const fileSize = file.size;
const chunkCount = Math.ceil(fileSize / chunkSize);
for (let i = current; i < chunkCount; i+=1) {
if (pauseRef.current) return;
const start = i * chunkSize;
const end = (i + 1) * chunkSize;
const chunk = file.slice(start, end);
const fd = new FormData();
fd.append("file", chunk);
await axios.post(url, fd);
setCurrent(i + 1);
setProgress(Math.floor((i + 1) / chunkCount * 100));
}
};
const clickPause = () => {
pauseRef.current = !pauseRef.current;
setPause(pauseRef.current);
if (!pauseRef.current) upload(file);
};
return (
<div className={styles.root}>
<input type="file" onChange={change} />
<button onClick={() => upload(file)}>上傳</button>
<button onClick={clickPause}>
{pause ? '繼續(xù)' : '暫停'}
</button>
<div className={styles.progress}>
{progress}%
</div>
</div>
);
};
- 通過xhr的onprogress來計算進(jìn)度
const App = () => {
const [file, setFile] = useState({});
const [progress, setProgress] = useState(0);
const change = e => {
const _file = e.target.files[0];
setFile(_file);
};
const upload = async file => {
const url = "http://localhost:1111/upload";
const fd = new FormData();
fd.append("file", file);
await axios.post(url, fd, {
onUploadProgress: e => {
const percent = Math.floor((e.loaded / e.total) * 100);
setProgress(percent);
}
});
};
return (
<div className={styles.root}>
<input type="file" onChange={change} />
<button onClick={() => upload(file)}>上傳</button>
<div className={styles.progress}>
{progress}%
</div>
</div>
);
};
拖拽上傳
const App = () => {
const dragover = e => {
// 阻止默認(rèn)事件
e.preventDefault();
// 阻止冒泡
e.stopPropagation();
};
const drop = e => {
// 阻止默認(rèn)事件
e.preventDefault();
// 阻止冒泡
e.stopPropagation();
// DataTransfer表示拖放操作中的數(shù)據(jù)
const file = e.dataTransfer.files[0];
const formData = new FormData();
formData.append('file', file);
axios.post('http://localhost:1111/upload', formData).then(res => {
console.log(res);
}
);
};
return (
<div className={styles.root} onDragOver={dragover} onDrop={drop} >
請拖到此處上傳
<input type="file" />
</div>
);
};