文件上传具体实现


目的

学习文件上传,自己封装一个简单的文件上传组件,熟悉文件上传的前后端流程

一、设计思路

思路:触发文件上传的事件 - -> 获取需要上传的文件 - - > 处理文件并通过接口将文件上传到服务器 - -> 服务端判断上传文件是否重复 - - > 返回文件上传结果。

前端技术栈: 所有使用的第三方库需要npm install 安装

  1. react
  2. axios
  3. 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 文件如何上传?如何处理?

文件不经过处理无法直接上传到服务器
文件上传的三种方式:

  1. form表单 =====> 会导致页面刷新,一般不用
  2. 接口 + FormData =====> 最常用(本篇文章选用的方案)
  3. 接口 + 文件码(将文件转为二进制或者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
    );
  }
};

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值