React+Koa によるファイルアップロードの実装例

React+Koa によるファイルアップロードの実装例

背景

最近、最終プロジェクトを書いていたときに、通常のファイルのアップロード、大きなファイルのアップロード、ブレークポイントの再開など、いくつかのファイルアップロード機能に携わりました。

サーバーの依存関係

  • koa(node.jsフレームワーク)
  • koa-router (Koa ルーティング)
  • koa-body (Koa ボディ解析ミドルウェア。POST リクエスト コンテンツを解析するために使用できます)
  • koa-static-cache (Koa 静的リソース ミドルウェア。静的リソース要求の処理に使用)
  • koa-bodyparser (request.body の内容を解析する)

バックエンド構成クロスドメイン

app.use(async (ctx, next) => {
 ctx.set('アクセス制御許可オリジン', '*');
 ctx.set(
  「アクセス制御許可ヘッダー」、
  'Content-Type、Content-Length、Authorization、Accept、X-Requested-With、yourHeaderFeild'、
 );
 ctx.set('アクセス制御許可メソッド', 'PUT、POST、GET、DELETE、OPTIONS');
 ctx.method == 'OPTIONS'の場合{
  ctx.body = 200;
 } それ以外 {
  次の()を待ちます。
 }
});

バックエンド構成の静的リソースアクセスではkoa-static-cacheを使用します。

// 静的リソース処理 app.use(
 KoaStaticCache('./public', {
  プレフィックス: '/public'、
  動的: true、
  gzip: 真、
 })、
);

バックエンド構成リクエストボディ解析はkoa-bodyparserを使用します

bodyParser は 'koa-bodyparser' を必要とします。
app.use(bodyParser());

フロントエンドの依存関係

  • 反応する
  • アント
  • アクシオス

通常のファイルアップロード

後部

バックエンドは、ミドルウェアとして koa-body を使用してオプションを設定し、それを router.post('url',middleware,callback) に渡すだけです。

バックエンドコード

 // アップロード設定 const uploadOptions = {
// ファイル形式をサポートする multipart: true,
 恐ろしい:
  // ディレクトリをパブリックフォルダに直接アップロードします。簡単にアクセスできるように、フォルダの後に / を追加することを忘れないでください
  アップロードディレクトリ: path.join(__dirname, '../../pulbic/'),
  // ファイル拡張子を保持する keepExtensions: true,
 },
};
router.post('/upload', 新しい KoaBody(uploadOptions), (ctx, next) => {
 // アップロードされたファイルを取得します。const file = ctx.request.files.file;
 定数 fileName = file.path.split('/')[file.path.split('/').length-1];
 ctx.body = {
   コード:0,
   データ:{
    url:`public/${fileName}`
   },
   メッセージ: '成功'

 }
});

フロントエンド

ここでは、formData 送信メソッドを使用します。フロントエンドは、<input type='file'/> を通じてファイルセレクターにアクセスし、onChange イベント e.target.files[0] を通じて選択されたファイルを取得します。次に、取得したファイルを取得するための FormData オブジェクトを作成します。formData.append('file',targetFile)

フロントエンドコード

   const アップロード = () => {
   const [url, setUrl] = useState<文字列>('')
   const handleClickUpload = () => {
     const fileLoader = document.querySelector('#btnFile') を HTMLInputElement として定義します。
     if (isNil(fileLoader)) {
       戻る;
     }
     ファイルローダーをクリックします。
   }
   const handleUpload = async (e: any) => {
     //アップロードされたファイルを取得します。const file = e.target.files[0];
     const フォームデータ = 新しいフォームデータ()
     formData.append('ファイル', ファイル);
     // ファイルをアップロード const { data } = await uploadSmallFile(formData);
     コンソールにログ出力します。
     setUrl(`${baseURL}${data.url}`);
   }
   戻る (
     <div>
       <input type="file" id="btnFile" onChange={handleUpload} style={{ display: 'none' }} />
       <Button onClick={handleClickUpload}>小さいファイルをアップロード</Button>
       <画像ソース={url} />
     </div>
   )
 }

その他のオプション

  • input+form フォームのアクションをバックエンドページに設定します。enctype="multipart/form-data", type='post'
  • アップロード用のファイルデータを読み取るためにfileReaderを使用することは互換性があまりありません

大きなファイルのアップロード

ファイルをアップロードする際、ファイルが大きすぎるためにリクエストがタイムアウトになる場合があります。この場合、断片化方式を使用できます。簡単に言うと、ファイルは小さな断片に分割され、サーバーに送信されます。これらの小さな断片は、どのファイルに属し、どの位置にあるかを識別します。すべての小さな断片が送信された後、バックエンドはマージを実行してこれらのファイルを完全なファイルにマージし、送信プロセス全体を完了します。

フロントエンド

  • ファイルの取得方法は前と同じなので、詳細は省略します。
  • デフォルトのフラグメント サイズ、ファイル スライスを設定します。各スライス名は filename.index.ext で、ファイル全体が送信され、要求がマージされるまで再帰要求が行われます。
  const handleUploadLarge = async (e: any) => {
     //アップロードされたファイルを取得します。const file = e.target.files[0];
     // ファイル セグメントの場合、await uploadEveryChunk(file, 0);
   }
   定数アップロード毎チャンク = (
     ファイル: ファイル、
     インデックス: 番号、
   ) => {
     コンソールログ(インデックス);
     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 = 新しいファイル([blob]、blobName);
     フォームデータを作成します。
     formData.append('file', blobFile);
     アップロードLargeFile(formData).then((res) => {
       // 再帰アップロード uploadEveryChunk(file, ++index);
     });
   };

後部

バックエンドは2つのインターフェースを提供する必要がある

アップロード

アップロードした各チャンクを、後で簡単にマージできるように、対応する名前のフォルダに保存します。

const アップロードステンシルプレビューオプション = {
マルチパート: true、
恐ろしい:
 uploadDir: path.resolve(__dirname, '../../temp/'), // ファイル保存アドレス keepExtensions: true,
 最大フィールドサイズ: 2 * 1024 * 1024、
},
};

router.post('/upload_chunk', 新しい KoaBody(uploadStencilPreviewOptions), 非同期 (ctx) => {
試す {
 const ファイル = 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 = {
  コード: 0,
  メッセージ: 「ファイルが正常にアップロードされました」
 };
} キャッチ (e) {
 ctx.body = {
  コード: -1、
  メッセージ: `ファイルのアップロードに失敗しました: ${e.toString()}`、
 };
}
});

マージ

フロントエンドからのマージ要求に従って、運ばれた名前を使用して、大きなファイルブロックの一時キャッシュ内でその名前に属するフォルダーを検索します。チャンクをインデックス順に読み取った後、fse.appendFileSync(path,data) を使用してファイルをマージし (順番に追加とはマージを意味します)、一時保存フォルダーを削除してメモリ領域を解放します。

router.post('/merge_chunk', 非同期(ctx) => {
 試す {
  const { ファイル名 } = ctx.request.body;
  定数 fname = ファイル名.split('.')[0];
  TEMP_DIR は path.resolve(__dirname, '../../temp');
  const static_preview_url = '/public/previews';
  STORAGE_DIR は path.resolve(__dirname, `../..${static_preview_url}`);
  TEMP_DIR を path に結合します。
  const チャンク = fse.readdir(chunkDir);
  チャンク
   .sort((a, b) => a - b)
   .map((チャンクパス) => {
    // ファイルをマージする fse.appendFileSync(
     パス.join(STORAGE_DIR, ファイル名),
     fse.readFileSync(`${chunkDir}/${chunkPath}`)、
    );
   });
  // 一時フォルダを削除します fse.removeSync(chunkDir);
  // 画像にアクセスするためのURL
  const url = `http://${ctx.request.header.host}${static_preview_url}/${fileName}`;
  ctx.body = {
   コード: 0,
   データ: { url },
   メッセージ: '成功'、
  };
 } キャッチ (e) {
  ctx.body = { コード: -1、メッセージ: `マージに失敗しました: ${e.toString()}` };
 }
});

履歴書のダウンロード

大きなファイルの送信中に、ページの更新や一時的な障害により送信が失敗した場合、ファイルを最初から再度送信する必要があり、ユーザーにとって非常に不快な体験となります。そのため、転送に失敗した場所をマークしておき、次回はここに直接転送する必要があります。localStorage で読み書きする方法を採用しています。

  const handleUploadLarge = async (e: any) => {
    //アップロードされたファイルを取得します。const file = e.target.files[0];
    const レコード = JSON.parse(localStorage.getItem('uploadRecord') を any として);
    if (!isNil(レコード)) {
      // 便宜上、ここでは衝突の問題は考慮しません。ファイルが同じかどうかを判断するには、ハッシュファイル方式を使用できます。 // 大きなファイルの場合は、ハッシュ(ファイル + ファイルサイズ)方式を使用して、2 つのファイルが同じかどうかを判断できます if (record.name === file.name) {
        戻り値 uploadEveryChunk(file, record.index);
      }
    }
    // ファイル セグメントの場合、await uploadEveryChunk(file, 0);
  }
  定数アップロード毎チャンク = (
    ファイル: ファイル、
    インデックス: 番号、
  ) => {
    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 = 新しいファイル([blob]、blobName);
    フォームデータを作成します。
    formData.append('file', blobFile);
    アップロードLargeFile(formData).then((res) => {
      // 各データが正常に転送された後、レコードの場所が記録されます localStorage.setItem('uploadRecord',JSON.stringify({
        名前:ファイル名、
        インデックス:インデックス+1
      }))
      // 再帰アップロード uploadEveryChunk(file, ++index);
    });
  };

ファイル識別

ファイルの MD5、ハッシュなどを計算できます。ファイルが大きすぎる場合、ハッシュ化に時間がかかる場合があります。 ファイルの一部を取り出してファイルサイズでハッシュし、ローカルサンプリング比較を行うことができます。crypto-jsライブラリを使用してmd5を計算し、FileReaderでファイルを読み取るコードは次のとおりです。

// md5 を計算して、すでに存在するかどうかを確認します。const sign = tempFile.slice(0, 512);
   const signFile = 新しいファイル(
    [sign、(tempFile.size は不明) は BlobPart]、
    ''、
   );
   const リーダー = 新しい FileReader();
   reader.onload = 関数 (イベント) {
    const バイナリ = イベント?.ターゲット?.結果;
    const md5 = binary && CryptoJs.MD5(binary を文字列として).toString();
    定数レコード = localStorage.getItem('upLoadMD5');
    if (isNil(md5)) {
     const file = blobToFile(blob, `${getRandomFileName()}.png`);
     uploadPreview(ファイル、0、md5)を返します。
    }
    const file = blobToFile(blob, `${md5}.png`);
    if (isNil(レコード)) {
     // このmd5を直接アップロードして記録します
     uploadPreview(ファイル、0、md5)を返します。
    }
    レコードを解析します。
    (recordObj.md5 == md5)の場合{
     // 記録された位置からアップロードを開始します // ブレークポイントからアップロードを再開します return uploadPreview(file, recordObj.index, md5);
    }
    uploadPreview(ファイル、0、md5)を返します。
   };
   リーダー。バイナリ文字列として読み取ります。(signFile)

要約する

これまでファイルのアップロードについてはあまり知りませんでした。卒業プロジェクトのこの機能を通じて、ファイルをアップロードするためのフロントエンドとバックエンドのコードについて予備的な理解が得られました。これらの方法はオプションの一部にすぎず、すべてを網羅しているわけではないかもしれません。今後の学習でさらに向上していきたいと思います。
ナゲットでブログを書くのは初めてです。インターンシップに参加して、自分の知識が足りないことに気づきました。ブログを続けることで、自分の知識体系を整理し、学習の過程を記録したいと思っています。また、問題が見つかったときにマスターからアドバイスをもらえることを願っています。ありがとう

上記はReact+Koaでファイルアップロードを実装した例の詳細です。React+Koaでファイルアップロードを実装する詳細については、123WORDPRESS.COMの他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • Alibaba Cloud OSSにファイルをアップロードするReact実装例
  • React quill の画像アップロードは、サーバーにアップロードするためにデフォルトの変換から base64 に変更されました。
  • React Nativeはfetchを使って写真をアップロードします
  • ReactでQiniuに写真をアップロードするためのサンプルコード
  • 画像アップロード用の React+react-dropzone+node.js サンプルコード
  • React Nativeはネットワーク上の写真をサーバーにアップロードする例を実装します
  • 画像アップロード機能を実装するためのReactNativeサンプルコード
  • React+ajax+javaで写真のアップロードとプレビューの機能を実現
  • ノードベースの React 画像アップロード コンポーネント実装のサンプル コード
  • ファイルのアップロードの進行状況を示す React の例

<<:  上位Nを見つけるためのMySQLグループソートの詳細な説明

>>:  CentOS7 で Jenkins+Maven+Git 継続的インテグレーション環境を構築する方法

推薦する

Linux manコマンドの具体的な使い方

01. コマンドの概要Linux には充実したヘルプ マニュアルが用意されています。コマンドのパラメ...

Vue.js プロジェクトの開始方法

目次1. Node.jsとVue 2. ローカル開発環境でフロントエンドのVueプロジェクトを実行す...

Xmeter APIインターフェーステストツールの使用状況の分析

XMeter API は、以下のサービスを含む、JMeter に基づくワンストップのオンライン イン...

ウェブページを最適化してメモリとCPUの使用率を削減

一部の Web ページは大きく見えなくても開くのに非常に時間がかかる場合があります。一方、他の We...

Vue コンポーネント (Vuex を含む) 間の値の転送に関する簡単な説明

目次父から息子へ:息子から父へ: Vuex を使用せずにコンポーネント間で値を渡す方法は、親から子、...

VMware 仮想マシンの NAT モードを構成する方法

この記事では、VMware仮想マシンのNAT構成プロセスを詳しく説明します。具体的な内容は次のとおり...

React で Antd の Form コンポーネントを使用してフォーム機能を実装する方法

1. 構造部品1. フォームには、入力コントロール、標準フォーム フィールド、ラベル、ドロップダウン...

VMware 仮想マシンでの Centos8 ブリッジの静的 IP 設定方法

1. ネットワーク接続方法がブリッジされていることを確認する物理ネットワーク接続ステータスのコピーを...

MySQLクエリ条件のnot inとinの違いと理由

まずSQLを書く SELECT DISTINCT from_id タラから cod.from_id ...

dockerがredisを再起動するとmysqlデータが失われる問題を解決する

公式ドキュメント:したがって、mysql は次のように起動する必要があります。 docker run...

Linux システムのユーザー管理コマンドの概要

ユーザーとグループの管理1. ユーザーとグループの基本概念ユーザーとグループ:システム上のすべてのプ...

js を使用して USB スキャナー データを取得する方法

この記事では、USBバーコードスキャナデータを取得するjsの具体的なプロセスを参考までに紹介します。...

MySQL データ操作 - DML ステートメントの使用

例示するDML(データ操作言語)とは、データベースの追加、削除、変更を行うための操作命令のことです。...

フロントエンドインタビューに必要なホモロジーとクロスドメインの詳細な説明

序文ご存知のとおり、ブラウザの相同性戦略とクロスドメイン方式も、フロントエンド面接で頻繁に遭遇する問...