好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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实现文件上传的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/6947613143141089287

查看更多关于React+Koa实现文件上传的示例的详细内容...

  阅读:40次