React实现文件上传和断点续传功能的示例代码
作者:一咻
这篇文章主要为大家详细介绍了React实现文件上传和断点续传功能的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
实现思路
分片上传的思路:
- 我们先拿到文件,在前端进行分片,将分片之后的小的文件传递给服务端。
- 当在客户端传送完成的时候,发送最后一个请求告诉服务端,文件已经传送完成了,然后服务端再将之前接收到文件进行合并成一个大的文件。最终再告诉客户端合并好的这个大文件。
断点续传,两种方案:
- 在上传之前先拉一下已经上传了那些切片在服务端了,然后客户端就可以跳过已经上传的切片了。
- 客户端不处理,在服务端进行处理,客户端上传所有的切片,然后服务端发现如果已经上传过了,则迅速返回成功,告诉客户端再继续传送下一个切片。
前端实现
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(); }; // 根据文件生成 一个hash 值 function changeBuffer(file) { return new Promise((resolve) => { let fileReader = new FileReader(); // 调用读取 file 内容的函数,当读取完成的时候, readyState 变成 DONE (已完成), 并触发 loadend 时间,同时 result 属性中包含一个 // arrayBuffer 对象表示所读取文件的数据。 fileReader.readAsArrayBuffer(file); // 当读取操作完成的时候,触发 loaded 事件 fileReader.onload = (ev) => { // result 表示所读取的文件数据 let buffer = ev.target.result; let spark = new SparkMD5.ArrayBuffer(); let HASH, suffix; spark.append(buffer); // 根据文件内容生成一个 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; // 根据文件内容生成 hash 值的时候,也是必要消耗时间的 setLoading(true); // 根据文件内容生成 hash 值,和获取文件后缀 let { HASH, suffix } = await changeBuffer(file); // 获取已经上传的切片信息 try { data = await instance.get("/upload_already", { params: { HASH, }, }); // 拿到已经上传好的 切片列表 if (+data.code === 0) { already = data.fileList; } } catch (err) { console.log(err); } // 实现文件的切片处理 // 有两种策略 【固定大小 或者 固定数量】 let max = 1024 * 100; // 每次传输的最大字节数 let count = Math.ceil(file.size / max); // 计算一共要分多少个切片 let index = 0; let chunks = []; // 如果计算的 切片个数大于 100 个则,就固定切片个数 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++; // 当如果没有达到最大的个数 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) => { // 看看是否有已经上传的切片信息 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;
实现文件上传切片处理的方式
- 固定数量。
- 固定大小。(设置一个每次传输的最大字节数,如果根据最大字节数计算出来的切片数量超过最大切片数量的话,则按照最大的切片数量重新计算每次传输的最大字节数)
后端实现(nodejs)
1.引用包
body-parser
:bodyParser用于解析客户端请求的body中的内容,内部使用JSON编码处理,url编码,处理以及对于文件的上传处理。
express
: 创建 api
服务
multiparty
: 解析Content-Type multipart/form-data的HTTP请求,也被称为文件上传。
spark-md5
:
- MD5计算将整个文件或者字符串,通过其不可逆的字符串变换计算,产生文件或字符串的MD5散列值。任意两个文件、字符串不会有相同的散列值(即“很大可能”是不一样的,理论上要创造出两个散列值相同的字符串是很困难的)。
- 因此MD5常用于校验字符串或者文件,以防止文件、字符串被“篡改”。因为如果文件、字符串的MD5散列值不一样,说明文件内容也是不一样的,即经过修改的,如果发现下载的文件和给的MD5值不一样,需要慎重使用。
2. 代码实现
// 使用 express 编写 api 程序 const express = require("express"); const fs = require("fs"); const bodyParser = require("body-parser"); const multipart = require("multiparty"); const sparkMd5 = require("spark-md5"); // 创建服务 const app = express(); const PORT = 8888; const HOST = "http://127.0.0.1"; const HOSTNAME = `${HOST}:${PORT}`; app.listen(PORT, () => { console.log(`上传服务启动,请访问${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 // 延时函数 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); // 根据 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}`, }); }); }; // 创建文件 并写入到指定的目录 并且返回给客户端结果 const writeFile = (res, path, file, filename, stream) => { return new Promise((resolve, reject) => { if (stream) { try { // 创建可读,可写 流 let readStream = fs.createReadStream(file.path); let writeStream = fs.createWriteStream(path); // 将可写流交给可读流管道 // 上面三行代码的作用是,将文件从 file.path 复制到 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 插件实现文件上传处理 & 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, }); } }); // 请求已经上传好的分片 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) => { // 匹配数字 let reg = /_(\d)+/; return reg.exec(a)[1] - reg.exec(b)[1]; }); // 发送给前端 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; // 创建存放 切片的临时目录 let [, HASH] = /^([^_]+)_(\d+)/.exec(filename); path = `${uploadDir}/${HASH}`; !fs.existsSync(path) ? fs.mkdirSync(path) : null; // 把切片存储发哦临时目录中 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实现文件上传和断点续传功能的示例代码的详细内容,更多关于React文件上传和断点续传的资料请关注脚本之家其它相关文章!