最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

React+Koa實(shí)現(xiàn)文件上傳的示例

 更新時(shí)間:2021年04月06日 09:58:43   作者:孤雨隨風(fēng)zz  
這篇文章主要介紹了React+Koa實(shí)現(xiàn)文件上傳的示例,幫助大家更好的理解和學(xué)習(xí)使用React,感興趣的朋友可以了解下

背景

最近在寫畢設(shè)的時(shí)候,涉及到了一些文件上傳的功能,其中包括了普通文件上傳,大文件上傳,斷點(diǎn)續(xù)傳等等

服務(wù)端依賴

  • koa(node.js框架)
  • koa-router(Koa路由)
  • koa-body(Koa body 解析中間件,可以用于解析post請(qǐng)求內(nèi)容)
  • koa-static-cache(Koa 靜態(tài)資源中間件,用于處理靜態(tài)資源請(qǐng)求)
  • koa-bodyparser(解析 request.body 的內(nèi)容)

后端配置跨域

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();
 }
});

后端配置靜態(tài)資源訪問 使用 koa-static-cache

// 靜態(tài)資源處理
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/'),
  // 保留文件擴(kuò)展名
  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] 即可獲取選擇的文件,而后創(chuàng)建FormData 對(duì)象將獲取的文件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 設(shè)置form的aciton為后端頁面,enctype="multipart/form-data",type=‘post'
  • 使用fileReader讀取文件數(shù)據(jù)進(jìn)行上傳 兼容性不是特別好

大文件上傳

  文件上傳的時(shí)候,可能會(huì)因?yàn)槲募^大,導(dǎo)致請(qǐng)求超時(shí),這時(shí)候就可以采取分片的方式,簡單來說就是將文件拆分為一個(gè)個(gè)小塊,傳給服務(wù)器,這些小塊標(biāo)識(shí)了自己屬于哪一個(gè)文件的哪一個(gè)位置,在所有小塊傳遞完畢后,后端執(zhí)行merge 將這些文件合并了完整文件,完成整個(gè)傳輸過程

前端

  • 獲取文件和前面一樣,不再贅述
  • 設(shè)置默認(rèn)分片大小,文件切片,每一片名字為 filename.index.ext,遞歸請(qǐng)求直到整個(gè)文件發(fā)送完請(qǐng)求合并
  const handleUploadLarge = async (e: any) => {
     //獲取上傳文件
     const file = e.target.files[0];
     // 對(duì)于文件分片
     await uploadEveryChunk(file, 0);
   }
   const uploadEveryChunk = (
     file: File,
     index: number,
   ) => {
     console.log(index);
     const chunkSize = 512; // 分片寬度
     // [ 文件名, 文件后綴 ]
     const [fname, fext] = file.name.split('.');
     // 獲取當(dāng)前片的起始字節(jié)
     const start = index * chunkSize;
     if (start > file.size) {
       // 當(dāng)超出文件大小,停止遞歸上傳
       return mergeLargeFile(file.name);
     }
     const blob = file.slice(start, start + chunkSize);
     // 為每片進(jìn)行命名
     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);
     });
   };

后端

后端需要提供兩個(gè)接口

上傳

將上傳的每一個(gè)分塊存儲(chǔ)到對(duì)應(yīng)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)) {
  // 沒有目錄就創(chuàng)建目錄
  // 創(chuàng)建大文件的臨時(shí)目錄
  await fse.mkdirs(chunkDir);
 }
 // 原文件名.index - 每個(gè)分片的具體地址和名字
 const dPath = path.join(chunkDir, fileNameArr[1]);

 // 將分片文件從 temp 中移動(dòng)到本次上傳大文件的臨時(shí)目錄
 await fse.move(file.path, dPath, { overwrite: true });
 ctx.body = {
  code: 0,
  message: '文件上傳成功',
 };
} catch (e) {
 ctx.body = {
  code: -1,
  message: `文件上傳失敗:${e.toString()}`,
 };
}
});

合并

  根據(jù)前端傳來合并請(qǐng)求,攜帶的name去臨時(shí)緩存大文件分塊的文件夾找到屬于該name的文件夾,根據(jù)index順序讀取chunks后,合并文件fse.appendFileSync(path,data) (按順序追加寫即合并),然后刪除臨時(shí)存儲(chǔ)的文件夾釋放內(nèi)存空間

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}`),
    );
   });
  // 刪除臨時(shí)文件夾
  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()}` };
 }
});

斷點(diǎn)續(xù)傳

  大文件在傳輸過程中,如果刷新頁面或者臨時(shí)的失敗導(dǎo)致傳輸失敗,又需要從頭傳輸對(duì)于用戶的體驗(yàn)是很不好的。因此就需要在傳輸失敗的位置,做好標(biāo)記,下一次直接在這里進(jìn)行傳輸即可,我采取的是在localStorage讀寫的方式

  const handleUploadLarge = async (e: any) => {
    //獲取上傳文件
    const file = e.target.files[0];
    const record = JSON.parse(localStorage.getItem('uploadRecord') as any);
    if (!isNil(record)) {
      // 這里為了便于展示,先不考慮碰撞問題, 判斷文件是否是同一個(gè)可以使用hash文件的方式
      // 對(duì)于大文件可以采用hash(一塊文件+文件size)的方式來判斷兩文件是否相同
      if(record.name === file.name){
        return await uploadEveryChunk(file, record.index);
      }
    }
    // 對(duì)于文件分片
    await uploadEveryChunk(file, 0);
  }
  const uploadEveryChunk = (
    file: File,
    index: number,
  ) => {
    const chunkSize = 512; // 分片寬度
    // [ 文件名, 文件后綴 ]
    const [fname, fext] = file.name.split('.');
    // 獲取當(dāng)前片的起始字節(jié)
    const start = index * chunkSize;
    if (start > file.size) {
      // 當(dāng)超出文件大小,停止遞歸上傳
      return mergeLargeFile(file.name).then(()=>{
        // 合并成功以后刪除記錄
        localStorage.removeItem('uploadRecord')
      });
    }
    const blob = file.slice(start, start + chunkSize);
    // 為每片進(jìn)行命名
    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);
    });
  };

文件相同判斷

  通過計(jì)算文件MD5,hash等方式均可,當(dāng)文件過大時(shí),進(jìn)行hash可能會(huì)花費(fèi)較大的時(shí)間。 可取文件的一塊chunk與文件的大小進(jìn)行hash,進(jìn)行局部的采樣比對(duì), 這里展示 通過 crypto-js庫進(jìn)行計(jì)算md5,F(xiàn)ileReader讀取文件的代碼

// 計(jì)算md5 看是否已經(jīng)存在
   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)) {
     // 直接從頭傳 記錄這個(gè)md5
     return uploadPreview(file, 0, md5);
    }
    const recordObj = JSON.parse(record);
    if (recordObj.md5 == md5) {
     // 從記錄位置開始傳
     //斷點(diǎn)續(xù)傳
     return uploadPreview(file, recordObj.index, md5);
    }
    return uploadPreview(file, 0, md5);
   };
   reader.readAsBinaryString(signFile);

總結(jié)

  之前一直對(duì)于上傳文件沒有過太多的了解,通過畢設(shè)的這個(gè)功能,對(duì)于上傳文件的前后端代碼有了初步的認(rèn)識(shí),可能這些方法也只是其中的選項(xiàng)并不包括所有,希望未來的學(xué)習(xí)中能夠不斷的完善。
  第一次在掘金寫博客,在參加實(shí)習(xí)以后,發(fā)現(xiàn)自己的知識(shí)體量的不足,希望能夠通過堅(jiān)持寫博客的方式,來梳理自己的知識(shí)體系,記錄自己的學(xué)習(xí)歷程,也希望各位大神在發(fā)現(xiàn)問題時(shí)不吝賜教,thx

以上就是React+Koa實(shí)現(xiàn)文件上傳的示例的詳細(xì)內(nèi)容,更多關(guān)于React+Koa實(shí)現(xiàn)文件上傳的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • React useState超詳細(xì)講解用法

    React useState超詳細(xì)講解用法

    我正在處理的組件是表單的時(shí)間輸入。表單相對(duì)復(fù)雜,并且是動(dòng)態(tài)生成的,根據(jù)嵌套在其他數(shù)據(jù)中的數(shù)據(jù)顯示不同的字段。我正在用useReducer管理表單的狀態(tài),到目前為止效果很好
    2022-11-11
  • react axios配置代理(proxy),如何解決本地開發(fā)時(shí)的跨域問題

    react axios配置代理(proxy),如何解決本地開發(fā)時(shí)的跨域問題

    這篇文章主要介紹了react axios配置代理(proxy),如何解決本地開發(fā)時(shí)的跨域問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • React中useEffect 與 useLayoutEffect的區(qū)別

    React中useEffect 與 useLayoutEffect的區(qū)別

    本文主要介紹了React中useEffect與useLayoutEffect的區(qū)別,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-07-07
  • React中父組件如何獲取子組件的值或方法

    React中父組件如何獲取子組件的值或方法

    這篇文章主要介紹了React中父組件如何獲取子組件的值或方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • 深入理解React?State?原理

    深入理解React?State?原理

    本文主要介紹了React?State?原理,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • React項(xiàng)目打包發(fā)布到Tomcat頁面空白問題及解決

    React項(xiàng)目打包發(fā)布到Tomcat頁面空白問題及解決

    這篇文章主要介紹了React項(xiàng)目打包發(fā)布到Tomcat頁面空白問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • React Native第三方平臺(tái)分享的實(shí)例(Android,IOS雙平臺(tái))

    React Native第三方平臺(tái)分享的實(shí)例(Android,IOS雙平臺(tái))

    本篇文章主要介紹了React Native第三方平臺(tái)分享的實(shí)例(Android,IOS雙平臺(tái)),具有一定的參考價(jià)值,有興趣的可以了解一下
    2017-08-08
  • React中的頁面跳轉(zhuǎn)方式示例詳解

    React中的頁面跳轉(zhuǎn)方式示例詳解

    React Router提供了幾種不同的跳轉(zhuǎn)方式,包括使用組件進(jìn)行頁面跳轉(zhuǎn)、使用組件進(jìn)行重定向,以及使用編程式導(dǎo)航進(jìn)行跳轉(zhuǎn),這篇文章主要介紹了React中的頁面跳轉(zhuǎn)方式詳解,需要的朋友可以參考下
    2023-09-09
  • React SSR樣式及SEO的實(shí)踐

    React SSR樣式及SEO的實(shí)踐

    這篇文章主要介紹了React SSR樣式及SEO的實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-10-10
  • React?split實(shí)現(xiàn)分割字符串的使用示例

    React?split實(shí)現(xiàn)分割字符串的使用示例

    當(dāng)我們需要將一個(gè)字符串按照指定的分隔符進(jìn)行分割成數(shù)組時(shí),我們可以在組件的生命周期方法中使用split方法來實(shí)現(xiàn)這個(gè)功能,本文就來介紹一下,感興趣的可以了解下
    2023-10-10

最新評(píng)論

隆化县| 元氏县| 石台县| 特克斯县| 鹤庆县| 金门县| 陇川县| 普定县| 益阳市| 富民县| 阿拉善右旗| 靖边县| 邢台县| 阿克| 通州区| 庆安县| 常山县| 菏泽市| 微山县| 开远市| 泸定县| 唐山市| 中山市| 固安县| 大邑县| 玉田县| 烟台市| 宜宾县| 都江堰市| 新巴尔虎左旗| 禹城市| 和林格尔县| 山东省| 翼城县| 抚州市| 司法| 仁布县| 焦作市| 应用必备| 江西省| 台北市|