JS practice: learn about 5 file upload scenarios in one article (React + Koa Implementation)

JS practice: learn about 5 file upload scenarios in one article (React + Koa Implementation)

preface

Today, I'd like to share with you five common file upload scenarios, from simple to deep, and take them carefully (the sample code warehouse also comes with a server implemented by koa, which is responsible for saving the code. This article will briefly introduce the implementation of the front-end part)

text

The front-end code shown in this article uses React as the carrier, but the ability of file upload itself has nothing to do with React

1. Single file upload

The single file is very simple. I believe everyone knows something

  • /fe/src/tests/Single.tsx

The main core logic is to use the < input type = "file" > tag to select and upload files

import React, { ChangeEvent, useState } from 'react'
import { group } from '../utils/msg'

interface UploadProps {
  url: string
  body: FormData
}

export const uploadREQ = ({ url, body }: UploadProps) => {
  return fetch(url, {
    method: 'POST',
    body,
  })
}

const Single = () => {
  const [filePath, setFilePath] = useState('')

  const onInputFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log('selected file:', e.target.files[0])
  }

  const upload = () => {
    const input = document.createElement('input')
    input.type = 'file'
    input.addEventListener('change', e => {
      const file = (e.target as HTMLInputElement).files[0]
      console.log('selected file:', file)

      const formData = new FormData()
      formData.append('file', file, `1_single_${file.name}`)

      const url = 'http://localhost:3001/upload/single'

      uploadREQ({ url, body: formData }).then(async res => {
        const result = await res.json()
        group(`[response] ${url}`, () => {
          console.log(result)
        })
        setFilePath(result.url)
      })
    })
    input.click()
  }

  return (
    <div>
      <h1>File upload - 1: Single file upload</h1>
      <input
        id="input-file"
        type="file"
        onChange={onInputFileChange}
      />
      <input
        id="input-file2"
        type="file"
        accept=".png,.jpg"
        onChange={onInputFileChange}
      />
      <button id="btn-file3" onClick={upload}>
        Click to upload
      </button>
      <h4>
        file path:{' '}
        <a target="_blank" href={filePath}>
          {filePath}
        </a>
      </h4>
    </div>
  )
}

export default Single

Leaving aside some unimportant code, we can see that the following sentences are the core code

  • e.target.fils or inputElement.files to get the selected file
const file = (e.target as HTMLInputElement).files[0]
  • Wrap the file in a FormData and pass it to the server as the body of the post request
const formData = new FormData()
formData.append('file', file, `1_single_${file.name}`)

uploadREQ({ url, body: formData })

2. Multi file upload

  • /fe/src/tests/Multiple.tsx

In fact, multiple files are also very simple. You can select multiple files by adding a multiple attribute to < input type = "file" multiple >, which is essentially the same as a single file

import React, { ChangeEvent } from 'react'
import { group } from '../utils/msg'
import { uploadREQ } from './Single'

interface UploadFilesProps {
  url: string
  files: File[]
  prefix: string
  fromDir?: boolean
}

export const uploadFiles = ({
  url,
  files,
  prefix,
  fromDir = false,
}: UploadFilesProps) => {
  const formData = new FormData()

  files.forEach(file => {
    const fileName = fromDir
      ? // @ts-ignore
        file.webkitRelativePath.replace(/\//g, `@${prefix}_`)
      : `${prefix}_${file.name}`

    formData.append('files', file, fileName)
  })

  console.log('upload files:', formData.getAll('files'))

  return uploadREQ({ url, body: formData })
}

const Multiple = () => {
  const onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files)

    const url = 'http://localhost:3001/upload/multiple'
    uploadFiles({
      url,
      files,
      prefix: '2_multiple',
    }).then(async res => {
      const result = await res.json()
      group(`[response] ${url}`, () => {
        console.log(result)
      })
    })
  }

  return (
    <div>
      <h1>File upload - 2: Multi file upload</h1>
      <input
        id="input-files"
        type="file"
        multiple
        onChange={onFileChange}
      />
    </div>
  )
}

export default Multiple

3. Upload multiple files by directory

  • /fe/src/tests/Directory.tsx

Uploading by directory, especially the WebKit directory attribute, is generally supported by most browsers, although it is non-standard

import React, { ChangeEvent } from 'react'
import { group } from '../utils/msg'
import { uploadFiles } from './Multiple'

const Directory = () => {
  const onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files)

    const url = 'http://localhost:3001/upload/multiple'
    uploadFiles({
      url,
      files,
      prefix: '3_directory',
      fromDir: true,
    }).then(async res => {
      const result = await res.json()
      group(`[response] ${url}`, () => {
        console.log(result)
      })
    })
  }

  return (
    <div>
      <h1>File upload - 3: Upload by directory</h1>
      <input
        id="input-files"
        type="file"
        // @ts-ignore
        webkitdirectory="true"
        onChange={onFileChange}
      />
    </div>
  )
}

export default Directory

4. Multi file composite compressed package upload

  • /fe/src/tests/Zip.tsx

Fourth, we inherit the previous multi file selection. Whether we upload multiple files or directories, we also use another jszip package to generate a compressed package, and then the compressed package can be uploaded as a single file

The first is the compression method

const ZIP = (
  zipName: string,
  files: File[],
  options: JSZip.JSZipGeneratorOptions = {
    type: 'blob',
    compression: 'DEFLATE',
  }
): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    const zip = new JSZip()
    files.forEach(file => {
      const path = (file as any).webkitRelativePath
      zip.file(path, file)
    })
    zip.generateAsync(options).then((bolb: Blob) => {
      resolve(bolb)
    })
  })
}

The following is the core code of the page

const Zip = () => {
  const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files)

    // @ts-ignore
    const dirName = files[0].webkitRelativePath.split('/')[0]
    const zipName = `${dirName}.zip`
    const zipFile = await ZIP(zipName, files)

    const formData = new FormData()
    formData.append('file', zipFile, zipName)

    const url = 'http://localhost:3001/upload/single'
    uploadREQ({
      url,
      body: formData,
    }).then(async res => {
      const result = await res.json()
      group(`[response] ${url}`, () => {
        console.log(result)
      })
    })
  }

  return (
    <div>
      <h1>File upload - 4: Compressed file upload</h1>
      <input
        id="input-files"
        type="file"
        // @ts-ignore
        webkitdirectory="true"
        onChange={onFileChange}
      />
    </div>
  )
}

export default Zip

5. Upload large files in blocks

  • /fe/src/tests/BigFile.tsx

The last part is a little more complicated. We explain it paragraph by paragraph

There are several main ideas to realize large file upload

  1. Generate the file characteristic value (MD5) and query the back-end for the existence of the file
  2. If it does not exist, the file will be chunked and uploaded one by one (multiple blocks can be uploaded concurrently)
  3. Finally, the files are combined at the back end to generate the original file

The front end is still relatively simple. Firstly, since the browser will actually limit the number of http concurrent requests in the same domain, we can implement a concurrent request management system ourselves

// Concurrent request pool
const asyncPool = async (
  poolLimit: number,
  tasks: any[],
  iteratorFn: (task: any, tasks?: any[]) => Promise<any>
) => {
  const waiting = [];
  const executing = [];
  for (const task of tasks) {
    // Create asynchronous task
    const p = Promise.resolve().then(() => iteratorFn(task, tasks));
    waiting.push(p);

    // The number of tasks exceeds the pool size
    if (poolLimit <= tasks.length) {
      const e = p.then(() =>
        executing.splice(executing.indexOf(e), 1)
      );
      executing.push(e);

      if (executing.length >= poolLimit) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(waiting);
};

The second tool method is to generate eigenvalues according to the file content. spark-md5 is used here

import SparkMD5 from 'spark-md5';

// Calculation file md5
const calcFileMD5 = (file: File): Promise<string> => {
  return new Promise((resolve, reject) => {
    const chunks = getChunks(file);
    let currentChunk = 0;
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();

    fileReader.onload = (e) => {
      spark.append(e.target.result as ArrayBuffer);
      currentChunk++;
      if (currentChunk < chunks) {
        loadNext();
      } else {
        resolve(spark.end());
      }
    };

    fileReader.onerror = (e) => {
      reject(fileReader.error);
      fileReader.abort();
    };

    function loadNext() {
      const start = currentChunk * chunkSize,
        end = Math.min(file.size, start + chunkSize);
      fileReader.readAsArrayBuffer(file.slice(start, end));
    }
    loadNext();
  });
};

Next, enter the main business process. The first method is to request the backend to check whether the file exists

interface ICheckFileExistRes {
  code: number;
  data: {
    isExists: boolean;
    [key: string]: any;
  };
}

// Check whether the file exists
const checkFileExist = (
  name: string,
  md5: string,
  chunks: number
): Promise<ICheckFileExistRes> => {
  const params = qs.stringify({
    n: name,
    m: md5,
    c: chunks,
  });
  const url = `http://localhost:3001/upload/checkExist?${params}`;
  return fetch(url)
    .then((res) => res.json())
    .then((res) => {
      group(`[response] ${url}`, () => {
        console.log(res);
      });
      return res;
    });
};

The second method is to upload files in batches when they do not exist

const chunkSize = 1024 * 1024; // 1MB

const getChunks = (file: File) => {
  return Math.ceil(file.size / chunkSize);
};

interface IUploadChunkProps {
  url: string;
  chunk: any;
  chunkId: number;
  chunks: number;
  fileName: string;
  fileMD5: string;
}

/**
 * Upload file block
 * @param param0
 * @returns
 */
const uploadChunk = ({
  url,
  chunk,
  chunkId,
  chunks,
  fileName,
  fileMD5,
}: IUploadChunkProps) => {
  const formData = new FormData();
  formData.set('file', chunk, `${fileMD5}-${chunkId}`);
  formData.set('chunks', chunks + '');
  formData.set('name', fileName);
  formData.set('timestamp', Date.now().toString());

  return fetch(url, {
    method: 'POST',
    body: formData,
  }).then((res) => res.json());
};

interface IUploadFileProps {
  file: File;
  fileMD5: string;
  chunkIds: string[];
  chunkSize?: number;
  poolLimit?: number;
}

/**
 * Large file upload
 */
const uploadFile = ({
  file,
  fileMD5,
  chunkIds,
  chunkSize = 1 * 1024 * 1024, // 1MB
  poolLimit = 3,
}: IUploadFileProps) => {
  const chunks = getChunks(file);
  return asyncPool(
    poolLimit,
    // @ts-ignore
    [...new Array(chunks).keys()],
    (i: number) => {
      if (chunkIds.includes(i + '')) {
        return Promise.resolve();
      }
      const start = i * chunkSize;
      const end = i + 1 === chunks ? file.size : start + chunkSize;
      const chunk = file.slice(start, end);
      return uploadChunk({
        url: 'http://localhost:3001/upload/chunk',
        chunk,
        chunkId: i,
        chunks,
        fileName: file.name,
        fileMD5,
      });
    }
  );
};

The last part is the main component code

const BigFile = () => {
  const inputRef = useRef<HTMLInputElement>();

  const upload = async () => {
    // Get basic file information
    const file = inputRef.current.files[0];
    const fileMD5 = await calcFileMD5(file);
    console.log('select file:', file);
    console.log('fileMD5:', fileMD5);

    // Check whether the file exists
    const res = await checkFileExist(
      file.name,
      fileMD5,
      getChunks(file)
    );
    console.log('res', res);

    // Re upload file
    if (res.code && res.data.isExists) {
      console.log(`file exist: ${res.data.url}`);
    } else {
      const result = await uploadFile({
        file,
        fileMD5,
        chunkIds: res.data.chunkIds as string[],
      });
      console.log('result', result);
    }
  };

  const clear = () => {
    inputRef.current.value = '';
  };

  return (
    <div>
      <h1>File upload - 5: Large file upload</h1>
      <input id="input-files" type="file" ref={inputRef} />
      <button onClick={upload}>Upload</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
};

export default BigFile;

epilogue

In fact, there is no difficult part in the concept. Interested students can go to the code warehouse to see the implementation, or pull down and run by themselves

Other resources

Reference connection

TitleLink
It's enough to upload files and understand these 8 scenarioshttps://mp.weixin.qq.com/s/uWqRhJc22iq_ek9cUbL3zQ
Koa official documentshttps://koa.bootcss.com/
Using Fetch - MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch
koajs/multer - Githubhttps://github.com/koajs/multer
Upload multiple files using FormDatahttps://blog.csdn.net/wang704987562/article/details/80304471
FormData.append()https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/append
express (using multer) Error: Multipart: Boundary not found, request sent by POSTMANhttps://stackoverflow.com/questions/49692745/express-using-multer-error-multipart-boundary-not-found-request-sent-by-pos
Why isn't the FileList object an array?https://stackoverflow.com/questions/25333488/why-isnt-the-filelist-object-an-array
Property does not exist on type 'DetailedHTMLProps, HTMLDivElement>' with React 16https://stackoverflow.com/questions/46215614/property-does-not-exist-on-type-detailedhtmlprops-htmldivelement-with-react
JSZip official documenthttps://stuk.github.io/jszip/
How does JavaScript extract ZIP files online?https://mp.weixin.qq.com/s?__biz=MzI2MjcxNTQ0Nw==&mid=2247491236&idx=1&sn=80cc4629a22045f647bce87b6c28e083&scene=21
How to better understand middleware and onion modelhttps://mp.weixin.qq.com/s?__biz=MzI2MjcxNTQ0Nw==&mid=2247486886&idx=1&sn=63bffec358b77986558e868d1adc2183&scene=21
How to upload large files concurrently in JavaScript?https://mp.weixin.qq.com/s?__biz=MzI2MjcxNTQ0Nw==&mid=2247491853&idx=1&sn=aa59ee95df84a81f7b4700fa4fec9436&scene=21
satazor/js-spark-md5 - Githubhttps://github.com/satazor/js-spark-md5
How to implement concurrency control in JavaScript?https://mp.weixin.qq.com/s?__biz=MzI2MjcxNTQ0Nw==&mid=2247490704&idx=1&sn=18976b9c9fe2456172c394f1d9cae88b&scene=21#wechat_redirect
NPM cool Library: qs, parsing URL query stringhttps://segmentfault.com/a/1190000012874916
ljharb/qs - Githubhttps://github.com/ljharb/qs
Escape encoding of special characters in URLhttps://blog.csdn.net/pcyph/article/details/45010609
How to upload large files concurrently in JavaScript- Github issuehttps://gist.github.com/semlinker/b211c0b148ac9be0ac286b387757e692
koajs/koa-body - Githubhttps://github.com/koajs/koa-body

Complete code example

https://github.com/superfreeeee/Blog-code/tree/main/front_end/javascript/js_upload

Tags: Javascript React TypeScript koa upload

Posted by Kitara on Sat, 24 Jul 2021 06:30:52 +0930