React+Koa实现文件上传的示例

背景

最近在写毕设的时候,涉及到了一些文件上传的功能,其中包括了普通文件上传,大文件上传,断点续传等等

服务端依赖

  • koa(node.js框架)
  • koa-router(Koa路由)
  • koa-body(Koa body 解析中间件,可以用于解析post请求内容)
  • koa-static-cache(Koa 静态资源中间件,用于处理静态资源请求)
  • koa-bodyparser(解析 request.body 的内容)

后端配置跨域

app.use(async (ctx, next) => {
 ctx.set('Access-Control-Allow-Origin', '*');
 ctx.set(
  'Access-Control-Allow-Headers',
  'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
 );
 ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
 if (ctx.method == 'OPTIONS') {
  ctx.body = 200;
 } else {
  await next();
 }
});

后端配置静态资源访问 使用 koa-static-cache

// 静态资源处理
app.use(
 KoaStaticCache('./pulbic', {
  prefix: '/public',
  dynamic: true,
  gzip: true,
 }),
);

后端配置requst body parse 使用 koa-bodyparser

const bodyParser = require('koa-bodyparser');
app.use(bodyParser());

前端依赖

  • React
  • Antd
  • axios

正常文件上传

后端

后端只需要使用 koa-body 配置好options,作为中间件,传入router.post('url',middleware,callback)即可

后端代码

 // 上传配置
const uploadOptions = {
// 支持文件格式
 multipart: true,
 formidable: {
  // 上传目录 这边直接上传到public文件夹,方便访问 文件夹后面要记得加/
  uploadDir: path.join(__dirname, '../../pulbic/'),
  // 保留文件扩展名
  keepExtensions: true,
 },
};
router.post('/upload', new KoaBody(uploadOptions), (ctx, next) => {
 // 获取上传的文件
 const file = ctx.request.files.file;
 const fileName = file.path.split('/')[file.path.split('/').length-1];
 ctx.body = {
   code:0,
   data:{
    url:`public/${fileName}`
   },
   message:'success'

 }
});

前端

我这里使用的是formData传递的方式,前端通过<input type='file'/> 来访问文件选择器,通过onChange事件 e.target.files[0] 即可获取选择的文件,而后创建FormData 对象将获取的文件formData.append('file',targetFile)即可

前端代码

   const Upload = () => {
   const [url, setUrl] = useState<string>('')
   const handleClickUpload = () => {
     const fileLoader = document.querySelector('#btnFile') as HTMLInputElement;
     if (isNil(fileLoader)) {
       return;
     }
     fileLoader.click();
   }
   const handleUpload = async (e: any) => {
     //获取上传文件
     const file = e.target.files[0];
     const formData = new FormData()
     formData.append('file', file);
     // 上传文件
     const { data } = await uploadSmallFile(formData);
     console.log(data.url);
     setUrl(`${baseURL}${data.url}`);
   }
   return (
     <div>
       <input type="file" id="btnFile" onChange={handleUpload} style={{ display: 'none' }} />
       <Button onClick={handleClickUpload}>上传小文件</Button>
       <img src={url} />
     </div>
   )
 }

其他可选方法

  • input+form 设置form的aciton为后端页面,enctype="multipart/form-data",type=‘post'
  • 使用fileReader读取文件数据进行上传 兼容性不是特别好

大文件上传

文件上传的时候,可能会因为文件过大,导致请求超时,这时候就可以采取分片的方式,简单来说就是将文件拆分为一个个小块,传给服务器,这些小块标识了自己属于哪一个文件的哪一个位置,在所有小块传递完毕后,后端执行merge 将这些文件合并了完整文件,完成整个传输过程

前端

  • 获取文件和前面一样,不再赘述
  • 设置默认分片大小,文件切片,每一片名字为 filename.index.ext,递归请求直到整个文件发送完请求合并
  const handleUploadLarge = async (e: any) => {
     //获取上传文件
     const file = e.target.files[0];
     // 对于文件分片
     await uploadEveryChunk(file, 0);
   }
   const uploadEveryChunk = (
     file: File,
     index: number,
   ) => {
     console.log(index);
     const chunkSize = 512; // 分片宽度
     // [ 文件名, 文件后缀 ]
     const [fname, fext] = file.name.split('.');
     // 获取当前片的起始字节
     const start = index * chunkSize;
     if (start > file.size) {
       // 当超出文件大小,停止递归上传
       return mergeLargeFile(file.name);
     }
     const blob = file.slice(start, start + chunkSize);
     // 为每片进行命名
     const blobName = `${fname}.${index}.${fext}`;
     const blobFile = new File([blob], blobName);
     const formData = new FormData();
     formData.append('file', blobFile);
     uploadLargeFile(formData).then((res) => {
       // 递归分片上传
       uploadEveryChunk(file, ++index);
     });
   };

后端

后端需要提供两个接口

上传

将上传的每一个分块存储到对应name 的文件夹,便于之后合并

const uploadStencilPreviewOptions = {
multipart: true,
formidable: {
 uploadDir: path.resolve(__dirname, '../../temp/'), // 文件存放地址
 keepExtensions: true,
 maxFieldsSize: 2 * 1024 * 1024,
},
};

router.post('/upload_chunk', new KoaBody(uploadStencilPreviewOptions), async (ctx) => {
try {
 const file = ctx.request.files.file;
 // [ name, index, ext ] - 分割文件名
 const fileNameArr = file.name.split('.');

 const UPLOAD_DIR = path.resolve(__dirname, '../../temp');
 // 存放切片的目录
 const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`;
 if (!fse.existsSync(chunkDir)) {
  // 没有目录就创建目录
  // 创建大文件的临时目录
  await fse.mkdirs(chunkDir);
 }
 // 原文件名.index - 每个分片的具体地址和名字
 const dPath = path.join(chunkDir, fileNameArr[1]);

 // 将分片文件从 temp 中移动到本次上传大文件的临时目录
 await fse.move(file.path, dPath, { overwrite: true });
 ctx.body = {
  code: 0,
  message: '文件上传成功',
 };
} catch (e) {
 ctx.body = {
  code: -1,
  message: `文件上传失败:${e.toString()}`,
 };
}
});

合并

根据前端传来合并请求,携带的name去临时缓存大文件分块的文件夹找到属于该name的文件夹,根据index顺序读取chunks后,合并文件fse.appendFileSync(path,data) (按顺序追加写即合并),然后删除临时存储的文件夹释放内存空间

router.post('/merge_chunk', async (ctx) => {
 try {
  const { fileName } = ctx.request.body;
  const fname = fileName.split('.')[0];
  const TEMP_DIR = path.resolve(__dirname, '../../temp');
  const static_preview_url = '/public/previews';
  const STORAGE_DIR = path.resolve(__dirname, `../..${static_preview_url}`);
  const chunkDir = path.join(TEMP_DIR, fname);
  const chunks = await fse.readdir(chunkDir);
  chunks
   .sort((a, b) => a - b)
   .map((chunkPath) => {
    // 合并文件
    fse.appendFileSync(
     path.join(STORAGE_DIR, fileName),
     fse.readFileSync(`${chunkDir}/${chunkPath}`),
    );
   });
  // 删除临时文件夹
  fse.removeSync(chunkDir);
  // 图片访问的url
  const url = `http://${ctx.request.header.host}${static_preview_url}/${fileName}`;
  ctx.body = {
   code: 0,
   data: { url },
   message: 'success',
  };
 } catch (e) {
  ctx.body = { code: -1, message: `合并失败:${e.toString()}` };
 }
});

断点续传

大文件在传输过程中,如果刷新页面或者临时的失败导致传输失败,又需要从头传输对于用户的体验是很不好的。因此就需要在传输失败的位置,做好标记,下一次直接在这里进行传输即可,我采取的是在localStorage读写的方式

  const handleUploadLarge = async (e: any) => {
    //获取上传文件
    const file = e.target.files[0];
    const record = JSON.parse(localStorage.getItem('uploadRecord') as any);
    if (!isNil(record)) {
      // 这里为了便于展示,先不考虑碰撞问题, 判断文件是否是同一个可以使用hash文件的方式
      // 对于大文件可以采用hash(一块文件+文件size)的方式来判断两文件是否相同
      if(record.name === file.name){
        return await uploadEveryChunk(file, record.index);
      }
    }
    // 对于文件分片
    await uploadEveryChunk(file, 0);
  }
  const uploadEveryChunk = (
    file: File,
    index: number,
  ) => {
    const chunkSize = 512; // 分片宽度
    // [ 文件名, 文件后缀 ]
    const [fname, fext] = file.name.split('.');
    // 获取当前片的起始字节
    const start = index * chunkSize;
    if (start > file.size) {
      // 当超出文件大小,停止递归上传
      return mergeLargeFile(file.name).then(()=>{
        // 合并成功以后删除记录
        localStorage.removeItem('uploadRecord')
      });
    }
    const blob = file.slice(start, start + chunkSize);
    // 为每片进行命名
    const blobName = `${fname}.${index}.${fext}`;
    const blobFile = new File([blob], blobName);
    const formData = new FormData();
    formData.append('file', blobFile);
    uploadLargeFile(formData).then((res) => {
      // 传输成功每一块的返回后记录位置
      localStorage.setItem('uploadRecord',JSON.stringify({
        name:file.name,
        index:index+1
      }))
      // 递归分片上传
      uploadEveryChunk(file, ++index);
    });
  };

文件相同判断

通过计算文件MD5,hash等方式均可,当文件过大时,进行hash可能会花费较大的时间。 可取文件的一块chunk与文件的大小进行hash,进行局部的采样比对, 这里展示 通过 crypto-js库进行计算md5,FileReader读取文件的代码

// 计算md5 看是否已经存在
   const sign = tempFile.slice(0, 512);
   const signFile = new File(
    [sign, (tempFile.size as unknown) as BlobPart],
    '',
   );
   const reader = new FileReader();
   reader.onload = function (event) {
    const binary = event?.target?.result;
    const md5 = binary && CryptoJs.MD5(binary as string).toString();
    const record = localStorage.getItem('upLoadMD5');
    if (isNil(md5)) {
     const file = blobToFile(blob, `${getRandomFileName()}.png`);
     return uploadPreview(file, 0, md5);
    }
    const file = blobToFile(blob, `${md5}.png`);
    if (isNil(record)) {
     // 直接从头传 记录这个md5
     return uploadPreview(file, 0, md5);
    }
    const recordObj = JSON.parse(record);
    if (recordObj.md5 == md5) {
     // 从记录位置开始传
     //断点续传
     return uploadPreview(file, recordObj.index, md5);
    }
    return uploadPreview(file, 0, md5);
   };
   reader.readAsBinaryString(signFile);

总结

之前一直对于上传文件没有过太多的了解,通过毕设的这个功能,对于上传文件的前后端代码有了初步的认识,可能这些方法也只是其中的选项并不包括所有,希望未来的学习中能够不断的完善。
  第一次在掘金写博客,在参加实习以后,发现自己的知识体量的不足,希望能够通过坚持写博客的方式,来梳理自己的知识体系,记录自己的学习历程,也希望各位大神在发现问题时不吝赐教,thx

以上就是React+Koa实现文件上传的示例的详细内容,更多关于React+Koa实现文件上传的资料请关注我们其它相关文章!

(0)

相关推荐

  • React+ajax+java实现上传图片并预览功能

    之前有在网上找ajax上传图片的资料,大部分的人写得都是用jQuery,但是在这里用JQuery就大才小用了,所以我就自己写了,先上图. 由上图,首先点击上面的选择文件,在选择图片之后,将会自动上传图片到服务器,并且返回图片名字和图片在服务器的路径,然后在页面显示文件名字和图片. 源码:ajax上传预览 React中: import React from 'react'; import Http from './http' const URL = 'http://localhost:8080/f

  • 基于Node的React图片上传组件实现实例代码

    写在前面 红旗不倒,誓把JavaScript进行到底!今天介绍我的开源项目 Royal 里的图片上传组件的前后端实现原理(React + Node),花了一些时间,希望对你有所帮助. 前端实现 遵循React 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接import即可. import React, { Component } from 'react' import Upload from '../../components/FormControls/Upload/' /

  • React+react-dropzone+node.js实现图片上传的示例代码

    本文将会用typescript+react+react-dropzone+express.js实现前后端上传图片.当然是用typescript需要提前下载相应的模块,在这里就不依依介绍了. 第一步,配置tsconfig.js "compilerOptions": { "outDir": "./public/", "sourceMap": true, "noImplicitAny": true, "

  • React中上传图片到七牛的示例代码

    之前有写过类似的一篇文章,有位同学突然找来解惑,发现自己采用了另外的一个方法,这里也分享下,希望对使用reactjs的同学有帮助. 逻辑思路是这样子的,在componentDidMount中实现更新dom的操作,异步加载需要的资源文件,然后在加载完后实现qiniu的初始化操作.这里就不需要在webpack或者其他打包工具中去引入qiniu的包文件,导致打完包的文件过大了. 我这里使用了nodejs的库scriptjs, const $S = require('scriptjs'); 可以实现异步

  • react quill中图片上传由默认转成base64改成上传到服务器的方法

    使用react-quill富文本编辑器,里面处理图片是默认转成base64,提交到后台的时候文件太大,因此这里改写处理image的逻辑,改成上传到服务器. 具体代码如下: 配置1 import Quill from 'quill' import ReactQuill from 'react-quill' import 'react-quill/dist/quill.core.css' import 'react-quill/dist/quill.snow.css' import QuillEmo

  • react native实现往服务器上传网络图片的实例

    如下所示: let common_url = 'http://192.168.1.1:8080/'; //服务器地址 let token = ''; //用户登陆后返回的token /** * 使用fetch实现图片上传 * @param {string} url 接口地址 * @param {JSON} params body的请求参数 * @return 返回Promise */ function uploadImage(url,params){ return new Promise(fun

  • react显示文件上传进度的示例

    Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中. 在使用react, vue框架的时候, 如果需要监听文件上传可以使用axios里的onUploadProgress. react上传文件显示进度 demo 前端 快速安装react应用 确保有node环境 npx create-react-app my-app //当前文件夹创建my-app文件 cd my-app //进入目录 npm install antd //安装antd UI组件 npm

  • React实现阿里云OSS上传文件的示例

    简介 阿里云 OSS 是 阿里云提供的海量.安全.低成本.高可靠的云存储服务,提供 99.9999999999%的数据可靠性(号称).能够使用 RESTful API 可以在互联网任何位置存储和访问,支持容量和处理能力弹性扩展. 基本术语 1.bucket :类似本地的一个文件夹 2.object : oss 存储数据的基本单元,类似本地的一个文件. 3.region:oss 存储的数据中心所在区域 4.Endpoint:oss 对外服务的访问域名,oss 以 http api 提供服务,不同

  • ReactNative实现图片上传功能的示例代码

    最近在学习ReactNative,ReactNative可以基于目前大热的开源JavaScript库React.js来开发iOS和Android原生App,今天就学习一下ReactNative实现图片上传功能 在查看ReactNative的官方文档的时候,你会发现其实Fackbook是没有提供图片上传功能的. 如果我们的项目里需要使用图片上传(用js 实现图片上传),那我们有没有什么办法呢? 通过搜索React-native的github, 会发现里面有这么一篇文章:https://github

  • React Native使用fetch实现图片上传的示例代码

    本文介绍了React Native使用fetch实现图片上传的示例代码,分享给大家,具体如下: 普通网络请求参数是JSON对象 图片上传的请求参数使用的是formData对象 使用fetch上传图片代码封装如下: let common_url = 'http://192.168.1.1:8080/'; //服务器地址 let token = ''; //用户登陆后返回的token /** * 使用fetch实现图片上传 * @param {string} url 接口地址 * @param {J

随机推荐