React實現(xiàn)文件上傳和斷點續(xù)傳功能的示例代碼
實現(xiàn)思路
分片上傳的思路:
- 我們先拿到文件,在前端進行分片,將分片之后的小的文件傳遞給服務(wù)端。
- 當在客戶端傳送完成的時候,發(fā)送最后一個請求告訴服務(wù)端,文件已經(jīng)傳送完成了,然后服務(wù)端再將之前接收到文件進行合并成一個大的文件。最終再告訴客戶端合并好的這個大文件。
斷點續(xù)傳,兩種方案:
- 在上傳之前先拉一下已經(jīng)上傳了那些切片在服務(wù)端了,然后客戶端就可以跳過已經(jīng)上傳的切片了。
- 客戶端不處理,在服務(wù)端進行處理,客戶端上傳所有的切片,然后服務(wù)端發(fā)現(xiàn)如果已經(jīng)上傳過了,則迅速返回成功,告訴客戶端再繼續(xù)傳送下一個切片。
前端實現(xiàn)
1. axios 封裝
import axios from "axios";
import Qs from "qs";
let instance = axios.create();
instance.defaults.baseURL = "http://127.0.0.1:8888";
instance.defaults.headers["Content-Type"] = "multipart/form-data";
instance.defaults.transformRequest = (data, headers) => {
const contentType = headers["Content-Type"];
if (contentType === "application/x-www-form-urlencoded")
return Qs.stringify(data);
return data;
};
instance.interceptors.response.use((response) => {
return response.data;
});
export default instance;
2. 分片上傳邏輯
import { useRef, useState } from "react";
import SparkMD5 from "spark-md5";
import "./large-file-upload.less";
import instance from "../utils/instance";
function LargeFileUpload() {
const [loading, setLoading] = useState(false);
const inputFileRef = useRef();
const handleUploadClick = () => {
inputFileRef.current.click();
};
// 根據(jù)文件生成 一個hash 值
function changeBuffer(file) {
return new Promise((resolve) => {
let fileReader = new FileReader();
// 調(diào)用讀取 file 內(nèi)容的函數(shù),當讀取完成的時候, readyState 變成 DONE (已完成), 并觸發(fā) loadend 時間,同時 result 屬性中包含一個
// arrayBuffer 對象表示所讀取文件的數(shù)據(jù)。
fileReader.readAsArrayBuffer(file);
// 當讀取操作完成的時候,觸發(fā) loaded 事件
fileReader.onload = (ev) => {
// result 表示所讀取的文件數(shù)據(jù)
let buffer = ev.target.result;
let spark = new SparkMD5.ArrayBuffer();
let HASH, suffix;
spark.append(buffer);
// 根據(jù)文件內(nèi)容生成一個 hash 值
HASH = spark.end();
// 在文件名里面匹配 . 后面的字母
// 第一個是匹配的 所有字符, 第二個是 匹配的第一個組
suffix = /\.([a-zA-z0-9]+)$/.exec(file.name)[1];
resolve({
buffer,
HASH,
suffix,
filename: `${HASH}.${suffix}`,
});
};
});
}
const onInputFileChange = async () => {
let file = inputFileRef.current.files[0];
// 獲取文件的 hash 值
let already = [],
data = null;
// 根據(jù)文件內(nèi)容生成 hash 值的時候,也是必要消耗時間的
setLoading(true);
// 根據(jù)文件內(nèi)容生成 hash 值,和獲取文件后綴
let { HASH, suffix } = await changeBuffer(file);
// 獲取已經(jīng)上傳的切片信息
try {
data = await instance.get("/upload_already", {
params: {
HASH,
},
});
// 拿到已經(jīng)上傳好的 切片列表
if (+data.code === 0) {
already = data.fileList;
}
} catch (err) {
console.log(err);
}
// 實現(xiàn)文件的切片處理
// 有兩種策略 【固定大小 或者 固定數(shù)量】
let max = 1024 * 100; // 每次傳輸?shù)淖畲笞止?jié)數(shù)
let count = Math.ceil(file.size / max); // 計算一共要分多少個切片
let index = 0;
let chunks = [];
// 如果計算的 切片個數(shù)大于 100 個則,就固定切片個數(shù)
if (count > 100) {
count = 100;
// 重新計算切片的大小
max = file.size / 100;
}
// 生成切片信息
while (index < count) {
chunks.push({
file: file.slice(index * max, (index + 1) * max),
filename: `${HASH}_${index + 1}.${suffix}`,
});
index++;
}
index = 0;
const complate = async () => {
index++;
// 當如果沒有達到最大的個數(shù)
if (index < count) return;
try {
data = await instance.post(
"/upload_merge",
{
HASH,
count,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (+data.code === 0) {
setLoading(false);
// 上傳完成之后清除 form 的值
inputFileRef.current.value = "";
alert(
`恭喜你,文件上傳成功,你可以訪問 ${data.servicePath} 訪問該文件~~`
);
}
} catch (err) {
alert("切片合并失敗,請稍后再試");
}
};
// 遍歷收集好的 切片信息
chunks.forEach((chunk) => {
// 看看是否有已經(jīng)上傳的切片信息
if (already.length > 0 && already.includes(chunk.filename)) {
// 這里的 return 表示跳過的意思
complate();
return;
}
let fm = new FormData();
fm.append("file", chunk.file);
fm.append("filename", chunk.filename);
instance.post("/upload_chunk", fm).then((data) => {
if (+data.code === 0) {
complate();
}
// 如果 code 不是 0
return Promise.reject(data.codeText);
});
});
};
return (
<div className="large-file-upload" onClick={handleUploadClick}>
{loading ? (
<div className="loading">loading...</div>
) : (
<span className="add">+</span>
)}
<input type="file" ref={inputFileRef} onChange={onInputFileChange} />
</div>
);
}
export default LargeFileUpload;


實現(xiàn)文件上傳切片處理的方式
- 固定數(shù)量。
- 固定大小。(設(shè)置一個每次傳輸?shù)淖畲笞止?jié)數(shù),如果根據(jù)最大字節(jié)數(shù)計算出來的切片數(shù)量超過最大切片數(shù)量的話,則按照最大的切片數(shù)量重新計算每次傳輸?shù)淖畲笞止?jié)數(shù))
后端實現(xiàn)(nodejs)

1.引用包
body-parser:bodyParser用于解析客戶端請求的body中的內(nèi)容,內(nèi)部使用JSON編碼處理,url編碼,處理以及對于文件的上傳處理。
express: 創(chuàng)建 api服務(wù)
multiparty: 解析Content-Type multipart/form-data的HTTP請求,也被稱為文件上傳。
spark-md5:
- MD5計算將整個文件或者字符串,通過其不可逆的字符串變換計算,產(chǎn)生文件或字符串的MD5散列值。任意兩個文件、字符串不會有相同的散列值(即“很大可能”是不一樣的,理論上要創(chuàng)造出兩個散列值相同的字符串是很困難的)。
- 因此MD5常用于校驗字符串或者文件,以防止文件、字符串被“篡改”。因為如果文件、字符串的MD5散列值不一樣,說明文件內(nèi)容也是不一樣的,即經(jīng)過修改的,如果發(fā)現(xiàn)下載的文件和給的MD5值不一樣,需要慎重使用。
2. 代碼實現(xiàn)
// 使用 express 編寫 api 程序
const express = require("express");
const fs = require("fs");
const bodyParser = require("body-parser");
const multipart = require("multiparty");
const sparkMd5 = require("spark-md5");
// 創(chuàng)建服務(wù)
const app = express();
const PORT = 8888;
const HOST = "http://127.0.0.1";
const HOSTNAME = `${HOST}:${PORT}`;
app.listen(PORT, () => {
console.log(`上傳服務(wù)啟動,請訪問${HOSTNAME}`);
});
// 中間件
app.use((req, res, next) => {
res.header("Access-Control-allow-origin", "*");
// 如果是 options 請求則放行
req.method === "OPTIONS"
? res.send("current services support cross domain requests!")
: next();
});
app.use(
bodyParser.urlencoded({
extended: false,
limit: "1024mb",
})
);
// API
// 延時函數(shù)
function delay(interval) {
typeof interval !== "number" ? (interval = 1000) : null;
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, interval);
});
}
// 檢測文件是否存在
const exists = function exists(path) {
return new Promise((resolve) => {
fs.access(path, fs.constants.F_OK, (err) => {
if (err) {
resolve(false);
return;
}
resolve(true);
});
});
};
// 大文件上傳 & 合并切片
const merge = (HASH, count) => {
return new Promise(async (resolve, reject) => {
let path = `${uploadDir}/${HASH}`;
let fileList = [];
let suffix = "";
let isExists;
// 看當前路徑是否存在
isExists = await exists(path);
// 根據(jù) hash 值沒有找到
if (!isExists) {
reject("HASH path is not found!");
return;
}
fileList = fs.readdirSync(path);
if (fileList.length < count) {
reject("ths slice has not been uploaded!");
return;
}
fileList
.sort((a, b) => {
let reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
})
.forEach((item) => {
!suffix ? (suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1]) : null;
// 把切片合并成一個文件
fs.appendFileSync(
`${uploadDir}/${HASH}.${suffix}`,
fs.readFileSync(`${path}/${item}`)
);
// 刪除切片
fs.unlinkSync(`${path}/${item}`);
});
// 移除臨時空的文件夾
fs.rmdirSync(path);
resolve({
path: `${uploadDir}/${HASH}.${suffix}`,
filename: `${HASH}.${suffix}`,
});
});
};
// 創(chuàng)建文件 并寫入到指定的目錄 并且返回給客戶端結(jié)果
const writeFile = (res, path, file, filename, stream) => {
return new Promise((resolve, reject) => {
if (stream) {
try {
// 創(chuàng)建可讀,可寫 流
let readStream = fs.createReadStream(file.path);
let writeStream = fs.createWriteStream(path);
// 將可寫流交給可讀流管道
// 上面三行代碼的作用是,將文件從 file.path 復(fù)制到 path
readStream.pipe(writeStream);
readStream.on("end", () => {
resolve();
// 然后刪除掉 file.path 下面的文件
fs.unlinkSync(file.path);
res.send({
code: 0,
codeText: "upload success",
originalFilename: filename,
servicePath: path.replace(__dirname, HOSTNAME),
});
});
} catch (err) {
reject(err);
res.send({
code: 1,
codeText: err,
});
}
return;
}
fs.writeFile(path, file, (err) => {
if (err) {
reject(err);
res.send({
code: 1,
codeText: err,
});
return;
}
resolve();
res.send({
code: 0,
codeText: "upload success",
originalFilename: filename,
servicePath: path.replace(__dirname, HOSTNAME),
});
});
});
};
// 基于 multiparty 插件實現(xiàn)文件上傳處理 & form-data 解析
const uploadDir = `${__dirname}/upload`;
const multiparty_upload = (req, auto) => {
typeof auto !== "boolean" ? (auto = false) : null;
let config = {
maxFieldsSize: 200 * 1024 * 1024,
};
if (auto) config.uploadDir = uploadDir;
return new Promise(async (resolve, reject) => {
await delay();
new multipart.Form(config).parse(req, (err, fields, files) => {
if (err) {
reject(err);
return;
}
// 解析出文件,和文件名
resolve({
fields,
files,
});
});
});
};
// 合并文件
app.post("/upload_merge", async (req, res) => {
let { HASH, count } = req.body;
// 嘗試合并文件
try {
let { filename, path } = await merge(HASH, count);
res.send({
code: 0,
codeText: "merge success",
originalFilename: filename,
servicePath: path.replace(__dirname, HOSTNAME),
});
} catch (err) {
res.send({
code: 1,
codeText: err,
});
}
});
// 請求已經(jīng)上傳好的分片
app.get("/upload_already", async (req, res) => {
let { HASH } = req.query;
let path = `${uploadDir}/${HASH}`;
let fileList = [];
try {
// 讀取文件目錄
fileList = fs.readdirSync(path);
// 對文件 進行一個排序
fileList = fileList.sort((a, b) => {
// 匹配數(shù)字
let reg = /_(\d)+/;
return reg.exec(a)[1] - reg.exec(b)[1];
});
// 發(fā)送給前端
res.send({
code: 0,
codeText: "",
fileList,
});
} catch (err) {
res.send({
code: 0,
codeText: "",
fileList: fileList,
});
}
});
// 上傳分片的接口
app.post("/upload_chunk", async (req, res) => {
try {
let { fields, files } = await multiparty_upload(req);
let file = (files.file && files.file[0]) || {};
let filename = (fields.filename && fields.filename[0]) || "";
let path = "";
let isExists = false;
// 創(chuàng)建存放 切片的臨時目錄
let [, HASH] = /^([^_]+)_(\d+)/.exec(filename);
path = `${uploadDir}/${HASH}`;
!fs.existsSync(path) ? fs.mkdirSync(path) : null;
// 把切片存儲發(fā)哦臨時目錄中
path = `${uploadDir}/${HASH}/${filename}`;
isExists = await exists(path);
if (isExists) {
res.send({
// 0 表示成功
code: 0,
codeText: "file is exists",
originalFilename: filename,
servicePath: path.replace(__dirname, HOSTNAME),
});
return;
}
writeFile(res, path, file, filename, true);
} catch (err) {
res.send({
code: 1,
codeText: err,
});
}
});
app.use(express.static("./"));
app.use((req, res) => {
res.status(404);
res.send("not found!");
});
以上就是React實現(xiàn)文件上傳和斷點續(xù)傳功能的示例代碼的詳細內(nèi)容,更多關(guān)于React文件上傳和斷點續(xù)傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React-View-UI組件庫封裝Loading加載中源碼
這篇文章主要介紹了React-View-UI組件庫封裝Loading加載樣式,主要包括組件介紹,組件源碼及組件測試源碼,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
jenkins分環(huán)境部署vue/react項目的方法步驟
這篇文章主要介紹了jenkins分環(huán)境部署vue/react項目的方法,本文分步驟給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02
React封裝高階組件實現(xiàn)路由權(quán)限的控制詳解
這篇文章主要介紹了React封裝高階組件實現(xiàn)路由權(quán)限的控制,在React中,為了實現(xiàn)安全可靠的路由權(quán)限控制,可以通過多種方式來確保只有經(jīng)過授權(quán)的用戶才能訪問特定路徑下的資源,下面來介紹封裝高階組件控制的方法,需要的朋友可以參考下2025-02-02

