# 大文件上传

# 前言

  1. 大文件上传因为某些不可避免的因素(如网络带宽、网络异常等情况),常规方法将一个文件通过blob形式字节流批量上传将会带来严重的性能问题
  2. 分片上传将会是一个不错的方案,将文件切割成多个块,依次上传,通过标记文件流key,可以和后台进行完美交互,并且可以实现断点续传等

# 常规的文件上传

代码

  // 获取文件输入框和上传按钮  
  const fileInput = document.getElementById('fileInput');  
  const uploadButton = document.getElementById('uploadButton');  
    
  // 添加事件监听器  
  fileInput.addEventListener('change', (event) => {  
  const file = event.target.files[0];  
    
  // 如果用户选择了一个文件  
  if (file) {  
    
  // 创建一个Blob对象,代表要上传的文件  
  const blob = new Blob([file], { type: file.type });  
    
  // 创建一个FormData对象,将文件和表单数据添加到其中  
  const formData = new FormData();  
  formData.append('file', file);  
    
  // 创建一个XMLHttpRequest对象,用于发送HTTP请求  
  const xhr = new XMLHttpRequest();  
  
  // 设置请求头,包括文件类型、大小等信息  
  xhr.open('POST', '/upload', true);  
  
  // 设置请求参数,包括表单数据和文件数据  
  xhr.send(formData);  
  
  // 添加上传进度监听器  
  xhr.upload.addEventListener('progress', (event) => {  
    if (event.lengthComputable) {  
      const percentComplete = Math.round((event.loaded / event.total) * 100);  
      console.log(`上传进度: ${percentComplete.toFixed(2)}%`);  
    }  
  });  
  
  // 添加上传完成监听器  
  xhr.addEventListener('load', (event) => {  
    console.log('文件上传成功');  
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 大文件断点续传

源码 (opens new window) nodejs创建的一个server端

const express = require('express');
const uploader = require('express-fileupload');
const {extname, resolve} = require('path');
const {
  promises: {
    writeFile,
    appendFile,
  },
  existsSync,
} = require('fs');
const app = express();
const port = 3000;

app.use('/', express.static('public'));
app.use(express.json());
app.use(express.urlencoded({
  urlencoded: true,
}));
app.use(uploader());

app.post('/api/upload', async (req, res) => {
  const {name, size, type, offset, hash} = req.body;
  const {file} = req.files;
  console.log(name, size, type, offset, hash);

  const ext = extname(name)
  const filename = resolve(__dirname, `./public/${hash}${ext}`);
  if (offset > 0) {
    if (!existsSync(filename)) {
      res.status(400)
        .send({
          message: '文件不存在',
        });
      return;
    }

    await appendFile(filename, file.data);
    res.send({
      data: 'appended',
    });
    return;
  }

  await writeFile(filename, file.data);
  res.send({
    data: 'created',
  });
});

app.listen(port, () => {
  console.log('Server is running at:', port);
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

客户端

const uploader = document.getElementById('uploader');
const output = document.getElementById('output');
const progress = document.getElementById('progress');

function read(file) {
  const reader = new FileReader();
  return new Promise((resolve, reject) => {
    reader.onload = function () {
      resolve(reader.result);
    }
    reader.onerror = reject;

    reader.readAsBinaryString(file);
  });
}

uploader.addEventListener('change', async (event) => {
  const {files} = event.target;
  const [file] = files;
  if (!file) {
    return;
  }

  uploader.value = null;
  const content = await read(file);
  const hash = CryptoJS.MD5(content);
  const {size, name, type} = file;
  progress.max = size;
  const chunkSize = 64 * 1024;
  let uploaded = 0;
  const local = localStorage.getItem(hash);
  if (local) {
    uploaded = Number(local);
  }
  const breakpoint = 1500 * 1024;
  while (uploaded < size) {
    const chunk = file.slice(uploaded, uploaded + chunkSize, type);
    const formData = new FormData();
    formData.append('name', name);
    formData.append('type', type);
    formData.append('size', size);
    formData.append('file', chunk);
    formData.append('hash', hash);
    formData.append('offset', uploaded);

    try {
      await axios.post('/api/upload', formData);
    } catch (e) {
      output.innerText = '上传失败。' + e.message;
      return;
    }

    uploaded += chunk.size;
    localStorage.setItem(hash, uploaded);
    progress.value = uploaded;
  }

  output.innerText = '上传成功。';
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
最后更新时间: 5/19/2023, 2:31:25 PM