首页前端开发JavaScriptReact+Koa实现文件上传的示例

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

时间2024-02-01 06:49:03发布访客分类JavaScript浏览472
导读:收集整理的这篇文章主要介绍了React+Koa实现文件上传的示例,觉得挺不错的,现在分享给大家,也给大家做个参考。 目录背景服务端依赖后端配置跨域后端配置静态资源访问 使用 koa-st...
收集整理的这篇文章主要介绍了React+Koa实现文件上传的示例,觉得挺不错的,现在分享给大家,也给大家做个参考。
目录
  • 背景
  • 服务端依赖
    • 后端配置跨域
    • 后端配置静态资源访问 使用 koa-static-cache
    • 后端配置requst body parse 使用 koa-bodyparser
  • 前端依赖
    • 正常文件上传
      • 后端
      • 前端
    • 大文件上传
      • 前端
      • 后端
        • 上传
        • 合并
    • 断点续传
      • 文件相同判断
        • 总结

          背景

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

          服务端依赖

          • 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] = useStatestring>
              ('')   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实现文件上传的资料请关注其它相关文章!

          您可能感兴趣的文章:
          • React实现阿里云OSS上传文件的示例
          • react quill中图片上传由默认转成base64改成上传到服务器的方法
          • React Native使用fetch实现图片上传的示例代码
          • React中上传图片到七牛的示例代码
          • React+react-dropzone+node.js实现图片上传的示例代码
          • react native实现往服务器上传网络图片的实例
          • ReactNative实现图片上传功能的示例代码
          • React+ajax+java实现上传图片并预览功能
          • 基于Node的React图片上传组件实现实例代码
          • react显示文件上传进度的示例

          声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!

          KoaReact文件上传文件上传"

          若转载请注明出处: React+Koa实现文件上传的示例
          本文地址: https://pptw.com/jishu/595061.html
          在不同函数中可以使用相同名字的变量吗 如何安装visual studio

          游客 回复需填写必要信息