文章目录
目的
学习文件上传,自己封装一个简单的文件上传组件,熟悉文件上传的前后端流程
一、设计思路
思路:触发文件上传的事件 - -> 获取需要上传的文件 - - > 处理文件并通过接口将文件上传到服务器 - -> 服务端判断上传文件是否重复 - - > 返回文件上传结果。
前端技术栈: 所有使用的第三方库需要npm install 安装
- react
- axios
- sparkMD5 这个第三方库能够根据文件的内容生成hash值,通过生成的hash值我们可以给每个文件构建一个具有唯一性的文件名,用于文件是否重复选择和上传的判断。
解释:在这篇文章中<input type="file">的点击事件可以触发选择上传文件的行为,如下图,所以在这篇文章中,我将input type=“file”的click事件缩略为:文件选择事件
1.1 如何获取上传的文件
通过给input绑定click事件并触发,我们可以选择要上传的文件,但是通常我们不直接给input绑定click事件,而是通过其他方式触发input的click,为什么?因为input的默认样式丑且比较难调。
// 类型为file的input标签,当该标签触发click事件,就会触发选择文件的行为
<input
type="file"
className="uploade_inp"
onChange={changeFile}
ref={singleFileRef}
style
/>
1.点击上传:在按钮的点击事件的事件处理函数中触发input的点击事件,对于用户来说,button的点击触发的行为相当于input的点击
触发的行为,两者效果一样。
<button onClick={selectFile} className="select-file">选择文件</button>
const selectFile = () => {
// 触发input的点击事件,即开启文件选择
singleFileRef.current.click();
};
// 监听input的onchange事件,这该事件中可以获取最新选择的文件
const changeFile = async () => {
// 获取选中的文件(是一个对象),singleFileRef.current就是dom元素,
let file = singleFileRef.current.files[0];
// 限制文件大小
if (file.size > maxSize * 1024 * 1024) {
message.error(`上传文件大小不能超过${maxSize}MB`);
return;
}
// 文件符要求则存入列表中
setFileList([
...fileList,
{
file: file,
fileName: file.name,
size: file.size,
// 图片是否已经上传到服务器,默认没有(应用场景:已上传了部分图片并要继续上传图片)
done: false,
},
]);
};
2.拖拽上传:在onDrop事件的处理函数中获取文件
<div
className={`dragBox ${active ? "active" : ""}`}
onDrop={dropHandle} // 当文件拖拽到div这个容器(区域)并放入则会触发onDrop事件
></div>
const dropHandle = async (ev) => {
ev.preventDefault();
// 获取拖拽上传的文件
let file = ev.dataTransfer.files[0];
// 将拖拽的文件存入列表中
setFileList([
...fileList,
{
file: file,
fileName: file.name,
size: file.size,
},
]);
};
1.2 文件如何上传?如何处理?
文件不经过处理无法直接上传到服务器
文件上传的三种方式:
- form表单 =====> 会导致页面刷新,一般不用
- 接口 + FormData =====> 最常用(本篇文章选用的方案)
- 接口 + 文件码(将文件转为二进制或者base64格式传递给服务器) =====> 比较常用,但是大文件编码浪费时间,且服务端需要解码。
// 处理文件对象
const upLoadOneFile = async (file) => {
let fileObj = new FormData();
// 将文件添加到FormData实例对象中
fileObj.append("file", file);
fileObj.append("fileName", fileName);
// 通过axios发送文件到服务端
return request("/api/file/single/hash", "POST", fileObj, {
// 配置,否则服务端无法识别FormData格式的请求头数据。
headers: { "Content-Type": "multipart/form-data" }
});
};
1.3 如何实现图片预览(图片缩略图)
将文件转为base64格式,浏览器可以直接识别base64格式图片并展示
const transformToBase = (file) => {
return new Promise((resolve) => {
let fileReader = new FileReader();
// 将文件转为base64格式 - 异步
fileReader.readAsDataURL(file);
// 监听图片是否完全转base64格式
fileReader.onload = (ev) => {
// base64格式的图片
resolve(ev.target.result);
};
});
};
let base64File = await transformToBase(获取的文件对象)
// 展示文件
<img className="item" src={base64File} alt="上传图片" />
1.4 判断文件是重复
每个文件生成一个具有唯一性的文件名,通过文件名判断是否重复上传,该操作既可以在服务端处理也可以在前端处理,这里我在前端处理了,代码如下:
import SparkMD5 from "spark-md5";
// 生成文件名
const getHashName = (file) => {
return new Promise((resolve) => {
let fileReader1 = new FileReader();
let fileBuffer;
// 将文件对象转为buffer
fileReader1.readAsArrayBuffer(file);
// 监听onload事件
fileReader1.onload = (ev) => {
fileBuffer = ev.target.result;
// 根据文件内容生成hash值
let spark = new SparkMD5.ArrayBuffer();
spark.append(fileBuffer);
let hashValue = spark.end();
// 匹配文件后缀名
let suffix = /\.([0-9a-zA-Z]+)$/.exec(file.name)[1];
resolve({
buffer: fileBuffer,
hash: hashValue,
suffix,
// 通过hash值 + 后缀生成唯一性的文件名
fileName: `${hashValue}.${suffix}`,
});
};
});
};
二、文件上传组件封装
技术栈:react、sparkMD5、axios
2.1 效果
2.2使用
import UploadFile from "@comp/UploadFile/UploadFile";
<UploadFile maxSize={1000} drag={true}></UploadFile>
// maxSize 上传文件最大多少,单位MB
// drag 是否开启拖拽上传,默认不开启
2.3具体实现
三、后端设计
技术栈:node、express, multiparty (解析formate格式数据)
配置接口:
const express = require("express");
const multiparty = require("multiparty");
const file = express.Router();
const { uploadFile, uploadFileByBase, uploadFileByHash } = require("../controllers/file");
file.post("/single/hash", uploadFileByHash); //文件上传接口
module.exports = file;
在controllers/file.js编写接口触发的处理函数:
// 客户端传递的文件的存储路径
const basePath = path.resolve(__dirname, "../upload");
// 解析formate格式的请求体
const parseFile = (req, auto = true) => {
let config = {
// 接收文件不能超过100mb
maxFieldsSize: 100 * 1024 * 1024,
};
// 如果自动上传,则将图片上传到src/upload目录下
if (auto) config.uploadDir = basePath;
return new Promise((resolve, reject) => {
new multiparty.Form(config).parse(req, (err, fields, files) => {
if (err) return reject(err);
resolve({
fields,
files,
});
});
});
};
// 检测文件是否已存在
exports.exists = function exists(path) {
return new Promise(resolve => {
fs.access(path, fs.constants.F_OK, err => {
if (err) {
resolve(false);
return;
}
resolve(true);
});
});
};
// 创建文件并写入到指定的目录 & 返回客户端结果
exports.writeFile = function writeFile(res, path, file, filename, stream) {
return new Promise((resolve, reject) => {
if (stream) {
try {
let readStream = fs.createReadStream(file.path),
writeStream = fs.createWriteStream(path);
readStream.pipe(writeStream);
readStream.on('end', () => {
resolve();
fs.unlinkSync(file.path);
res.formatSend({
code: 0,
codeText: 'upload success',
originalFilename: filename,
servicePath: path.replace(__dirname, `../${path}`)
}, 200);
});
} catch (err) {
reject(err);
res.formatSend({
code: 1,
codeText: err
}, 200);
}
return;
}
fs.writeFile(path, file, err => {
if (err) {
reject(err);
res.formatSend({
code: 1,
codeText: err
}, 200);
return;
}
resolve();
res.formatSend({
code: 0,
codeText: 'upload success',
originalFilename: filename,
servicePath: path.replace(__dirname, `../${path}`)
}, 200);
});
});
};
// 文件上传
exports.uploadFileByHash = async (req, res) => {
try {
const { files, fields } = await parseFile(req, false);
let file = (files.file && files.file[0]) || {};
let filename = (fields.fileName && fields.fileName[0]) || "";
let isExist = false,
path = `${basePath}/${filename}`;
// 检查文件是否存在
isExist = await exists(path);
if (isExist) {
res.formatSend(
{
code: 1,
CodeText: "file is exists",
originalFilename: filename,
servicePath: path.replace(__dirname, `../${path}`),
},
200
);
return;
}
writeFile(res, path, file, filename, true);
} catch (err) {
res.formatSend(
{
code: 1,
CodeText: err,
},
200
);
}
};