React實(shí)現(xiàn)文件上傳和斷點(diǎn)續(xù)傳功能的示例代碼
實(shí)現(xiàn)思路
分片上傳的思路:
- 我們先拿到文件,在前端進(jìn)行分片,將分片之后的小的文件傳遞給服務(wù)端。
- 當(dāng)在客戶端傳送完成的時(shí)候,發(fā)送最后一個(gè)請求告訴服務(wù)端,文件已經(jīng)傳送完成了,然后服務(wù)端再將之前接收到文件進(jìn)行合并成一個(gè)大的文件。最終再告訴客戶端合并好的這個(gè)大文件。
斷點(diǎn)續(xù)傳,兩種方案:
- 在上傳之前先拉一下已經(jīng)上傳了那些切片在服務(wù)端了,然后客戶端就可以跳過已經(jīng)上傳的切片了。
- 客戶端不處理,在服務(wù)端進(jìn)行處理,客戶端上傳所有的切片,然后服務(wù)端發(fā)現(xiàn)如果已經(jīng)上傳過了,則迅速返回成功,告訴客戶端再繼續(xù)傳送下一個(gè)切片。
前端實(shí)現(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ù)文件生成 一個(gè)hash 值 function changeBuffer(file) { return new Promise((resolve) => { let fileReader = new FileReader(); // 調(diào)用讀取 file 內(nèi)容的函數(shù),當(dāng)讀取完成的時(shí)候, readyState 變成 DONE (已完成), 并觸發(fā) loadend 時(shí)間,同時(shí) result 屬性中包含一個(gè) // arrayBuffer 對(duì)象表示所讀取文件的數(shù)據(jù)。 fileReader.readAsArrayBuffer(file); // 當(dāng)讀取操作完成的時(shí)候,觸發(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)容生成一個(gè) hash 值 HASH = spark.end(); // 在文件名里面匹配 . 后面的字母 // 第一個(gè)是匹配的 所有字符, 第二個(gè)是 匹配的第一個(gè)組 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 值的時(shí)候,也是必要消耗時(shí)間的 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); } // 實(shí)現(xiàn)文件的切片處理 // 有兩種策略 【固定大小 或者 固定數(shù)量】 let max = 1024 * 100; // 每次傳輸?shù)淖畲笞止?jié)數(shù) let count = Math.ceil(file.size / max); // 計(jì)算一共要分多少個(gè)切片 let index = 0; let chunks = []; // 如果計(jì)算的 切片個(gè)數(shù)大于 100 個(gè)則,就固定切片個(gè)數(shù) if (count > 100) { count = 100; // 重新計(jì)算切片的大小 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++; // 當(dāng)如果沒有達(dá)到最大的個(gè)數(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;
實(shí)現(xiàn)文件上傳切片處理的方式
- 固定數(shù)量。
- 固定大小。(設(shè)置一個(gè)每次傳輸?shù)淖畲笞止?jié)數(shù),如果根據(jù)最大字節(jié)數(shù)計(jì)算出來的切片數(shù)量超過最大切片數(shù)量的話,則按照最大的切片數(shù)量重新計(jì)算每次傳輸?shù)淖畲笞止?jié)數(shù))
后端實(shí)現(xiàn)(nodejs)
1.引用包
body-parser
:bodyParser用于解析客戶端請求的body中的內(nèi)容,內(nèi)部使用JSON編碼處理,url編碼,處理以及對(duì)于文件的上傳處理。
express
: 創(chuàng)建 api
服務(wù)
multiparty
: 解析Content-Type multipart/form-data的HTTP請求,也被稱為文件上傳。
spark-md5
:
- MD5計(jì)算將整個(gè)文件或者字符串,通過其不可逆的字符串變換計(jì)算,產(chǎn)生文件或字符串的MD5散列值。任意兩個(gè)文件、字符串不會(huì)有相同的散列值(即“很大可能”是不一樣的,理論上要?jiǎng)?chuàng)造出兩個(gè)散列值相同的字符串是很困難的)。
- 因此MD5常用于校驗(yàn)字符串或者文件,以防止文件、字符串被“篡改”。因?yàn)槿绻募⒆址腗D5散列值不一樣,說明文件內(nèi)容也是不一樣的,即經(jīng)過修改的,如果發(fā)現(xiàn)下載的文件和給的MD5值不一樣,需要慎重使用。
2. 代碼實(shí)現(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ù)啟動(dòng),請?jiān)L問${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í)函數(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; // 看當(dāng)前路徑是否存在 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; // 把切片合并成一個(gè)文件 fs.appendFileSync( `${uploadDir}/${HASH}.${suffix}`, fs.readFileSync(`${path}/${item}`) ); // 刪除切片 fs.unlinkSync(`${path}/${item}`); }); // 移除臨時(shí)空的文件夾 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 插件實(shí)現(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); // 對(duì)文件 進(jìn)行一個(gè)排序 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)建存放 切片的臨時(shí)目錄 let [, HASH] = /^([^_]+)_(\d+)/.exec(filename); path = `${uploadDir}/${HASH}`; !fs.existsSync(path) ? fs.mkdirSync(path) : null; // 把切片存儲(chǔ)發(fā)哦臨時(shí)目錄中 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實(shí)現(xiàn)文件上傳和斷點(diǎn)續(xù)傳功能的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于React文件上傳和斷點(diǎn)續(xù)傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React-View-UI組件庫封裝Loading加載中源碼
這篇文章主要介紹了React-View-UI組件庫封裝Loading加載樣式,主要包括組件介紹,組件源碼及組件測試源碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06React修改數(shù)組對(duì)象的注意事項(xiàng)及說明
這篇文章主要介紹了React修改數(shù)組對(duì)象的注意事項(xiàng)及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12jenkins分環(huán)境部署vue/react項(xiàng)目的方法步驟
這篇文章主要介紹了jenkins分環(huán)境部署vue/react項(xiàng)目的方法,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02React封裝高階組件實(shí)現(xiàn)路由權(quán)限的控制詳解
這篇文章主要介紹了React封裝高階組件實(shí)現(xiàn)路由權(quán)限的控制,在React中,為了實(shí)現(xiàn)安全可靠的路由權(quán)限控制,可以通過多種方式來確保只有經(jīng)過授權(quán)的用戶才能訪問特定路徑下的資源,下面來介紹封裝高階組件控制的方法,需要的朋友可以參考下2025-02-02