简单几步,教你简单如何实现大文件上传

在现代 Web 应用中,文件上传是一个常见且重要的功能。为了更高效地处理大文件上传以及应对网络不稳定等情况,文件切片上传和断点续传技术应运而生。这里简单的讲述一下大文件上传的机处理制,后文有完整代码。

一、文件上传

前端部分

前端主要负责读取本地文件、对文件进行切片处理、将切片处理成 FormData 对象并发送给后端。

1. 读取本地文件并切片

index.html 文件中,通过 HTML 的 <input type="file"> 元素让用户选择本地文件。当用户选择文件后,通过 addEventListener 监听 change 事件,获取文件对象。然后调用 createChunk 函数对文件进行切片处理,默认每个切片大小为 5MB。

js 复制代码
/**
 * 对文件进行切片处理
 * @param {File} file - 要切片的文件对象
 * @param {number} size - 每个切片的大小,默认为 5MB(5 * 1024 * 1024 字节)
 * @returns {Array<Blob>} - 包含所有文件切片的数组
 */
function createChunk(file, size = 5 * 1024 * 1024) {
    // 用于存储切片结果的数组
    const chunkList = [];
    // 当前切片的起始位置,初始化为 0
    let cur = 0;
    // 循环切片,只要当前位置小于文件大小,就继续切片
    while (cur < file.size) {
        // 使用 File 对象的 slice 方法进行切片
        // slice 方法接收两个参数,分别是开始位置和结束位置,范围是左闭右开区间
        // 这里从当前位置 cur 开始,截取长度为 size 的文件片段
        const chunk = file.slice(cur, cur + size);
        // 将切片后的文件片段添加到切片列表中
        chunkList.push(chunk);
        // 更新当前位置,指向下一个切片的起始位置
        cur += size;
    }
    // 返回包含所有切片的数组
    return chunkList;
}

2. 处理切片并封装成 FormData 对象

将切片列表传入 handleChunk 函数,该函数会为每个切片添加相关信息,如 chunkNamefileName 等,并将其封装成 FormData 对象。

js 复制代码
/**
 * 处理切片列表,为每个切片添加相关信息,并封装成 FormData 对象
 * @param {Array<Blob>} chunkList - 文件切片列表
 * @returns {Array<Object>} - 包含 FormData 对象和切片索引的数组
 */
function handleChunk(chunkList) {
    // 为每个切片添加额外信息,如 chunkName、fileName、index 等
    const handleChunkList = chunkList.map((chunk, index) => {
        return {
            // 当前切片的文件对象
            file: chunk,
            // 当前切片的大小
            size: chunk.size,
            // 切片的名称,格式为 文件名-索引
            chunkName: `${fileObj.file.name}-${index}`,
            // 原始文件的名称
            fileName: fileObj.file.name,
            // 当前切片的索引
            index
        };
    });

    // 将每个添加了额外信息的切片封装成 FormData 对象
    const formDataChunkList = handleChunkList.map(({ file, chunkName, fileName, index }) => {
        // 创建一个新的 FormData 对象
        const formData = new FormData();
        // 向 FormData 对象中添加文件切片
        formData.append('file', file);
        // 向 FormData 对象中添加原始文件名
        formData.append('fileName', fileName);
        // 向 FormData 对象中添加切片名
        formData.append('chunkName', chunkName);
        // 返回包含 FormData 对象和切片索引的对象
        return { formData, index };
    });

    // 返回封装好的 FormData 对象列表
    return formDataChunkList;
}

3. 发送切片到后端

使用 axios 库将封装好的 FormData 对象发送给后端。通过 Promise.all 确保所有切片都上传完成后,再发送合并请求。

js 复制代码
/**
 * 上传切片到后端,所有切片上传完成后发送合并请求
 * @param {Array<Object>} handleChunkList - 处理后的切片列表,每个元素包含文件切片及相关信息
 */
function uploadChunks(handleChunkList) {
    // 调用 handleChunk 函数将处理后的切片列表封装成 FormData 对象列表
    const formDataChunkList = handleChunk(handleChunkList);
    // 为每个 FormData 对象创建一个 axios 的 POST 请求
    const requestList = formDataChunkList.map(({ formData, index }) => {
        // 发送 POST 请求到后端的 /upload 接口,携带 FormData 对象
        return axios.post('http://localhost:3000/upload', formData);
    });

    // 使用 Promise.all 并行执行所有切片的上传请求
    Promise.all(requestList).then(res => {
        // 当所有切片上传完成后,发送合并请求到后端的 /merge 接口
        axios.post('http://localhost:3000/merge', {
            // 每个切片的大小
            size: 5 * 1024 * 1024,
            // 原始文件的名称
            fileName: fileObj.file.name
        });
    });
}

前端完整代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入 axios 库,用于发送 HTTP 请求 -->
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
  <!-- 文件选择输入框 -->
  <input type="file" id="input">
  <!-- 上传按钮 -->
  <button id="btn">上传</button>

  <script>
    // 获取文件选择输入框元素
    const input = document.getElementById('input');
    // 获取上传按钮元素
    const btn = document.getElementById('btn');
    // 存储选中的文件对象
    const fileObj = {
      file: null
    };
    // 监听文件选择输入框的 change 事件
    input.addEventListener('change', function(e) {
      // 获取选中的文件
      const [ file ] = e.target.files;
      // 如果没有选择文件,则直接返回
      if (!file) return;
      // 将选中的文件存储到 fileObj 中
      fileObj.file = file;
    });
  
    // 监听上传按钮的 click 事件
    btn.addEventListener('click', function() {
      // 如果没有选择文件,则直接返回
      if (!fileObj.file) return;
      // 对选中的文件进行切片处理
      const chunkList = createChunk(fileObj.file);
      // 处理切片列表
      handleChunk(chunkList);
    });

    // 对文件进行切片处理
    function createChunk(file, size = 5 * 1024 * 1024) {
      const chunkList = [];
      let cur = 0;
      // 循环切片,直到文件全部切完
      while (cur < file.size) {
        // 使用 slice 方法对文件进行切片,参数为开始位置和结束位置,左闭右开
        const chunk = file.slice(cur, cur + size);
        // 将切片添加到切片列表中
        chunkList.push(chunk);
        cur += size;
      }
      return chunkList;
    }
  
    // 处理切片列表
    function handleChunk(chunkList) {
      // 为每个切片添加额外信息,如 chunkName、fileName、index 等
      const handleChunkList = chunkList.map((chunk, index) => {
        return {
          file: chunk,
          size: chunk.size,
          chunkName: `${fileObj.file.name}-${index}`,
          fileName: fileObj.file.name,
          index
        };
      });
      // 上传切片
      uploadChunks(handleChunkList);
    }
  
    // 上传切片
    function uploadChunks(handleChunkList) {
      // 将每个切片封装成 FormData 对象
      const formDataChunkList =  handleChunkList.map(({ file, chunkName, fileName, index }) => {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('fileName', fileName);
        formData.append('chunkName', chunkName);
        return {formData, index};
      });
      // 发送每个切片的上传请求
      const requestList = formDataChunkList.map(({ formData, index }) => {
        return axios.post('http://localhost:3000/upload', formData);
      });
      // 当所有切片上传完成后,发送合并请求
      Promise.all(requestList).then(res => {
        axios.post('http://localhost:3000/merge', {
          size: 5 * 1024 * 1024, 
          fileName: fileObj.file.name
        });
      });
    }
  </script>
</body>
</html>

后端部分

后端主要负责接收前端发送的切片文件、将切片文件存储到本地以及合并切片文件。

1. 接收前端的 FormData 对象

app.js 文件中,使用 http 模块创建服务器。当接收到 /upload 请求时,使用 multiparty 插件解析 FormData 对象。

js 复制代码
// 创建一个 HTTP 服务器实例
const server = http.createServer(async (req, res) => {
    // 检查请求的 URL 是否为 /upload
    if (req.url === '/upload') {
        // 创建一个 multiparty 的 Form 实例,用于解析表单数据
        const form = new multiparty.Form();
        // 解析请求中的表单数据
        form.parse(req, async (err, fields, files) => {
            // 处理解析过程中可能出现的错误
            if (err) {
                console.error('解析表单数据时出错:', err);
                return;
            }
            // 从解析结果中获取上传的文件对象
            const [file] = files.file;
            // 从解析结果中获取文件名
            const [fileName] = fields.fileName;
            // 从解析结果中获取切片名
            const [chunkName] = fields.chunkName;

            // 计算存储切片的目录路径
            const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
            // 检查切片存储目录是否存在
            if (!fs.existsSync(chunkDir)) {
                // 如果目录不存在,则创建该目录
                await fs.mkdirs(chunkDir);
            }
            // 将上传的临时文件移动到指定的切片存储目录中
            fs.move(file.path, `${chunkDir}/${chunkName}`, (moveErr) => {
                if (moveErr) {
                    console.error('移动文件时出错:', moveErr);
                    res.statusCode = 500;
                    res.end('切片上传失败');
                } else {
                    // 移动成功,向客户端返回响应
                    res.end('切片上传成功');
                }
            });
        });
    }
});

2. 合并切片文件

当接收到 /merge 请求时,后端会根据前端传递的文件名和切片大小,将存储在本地的切片文件合并成一个完整的文件。

js 复制代码
/**
 * 合并文件切片
 * @param {string} filePath - 合并后的文件路径
 * @param {string} fileName - 文件名
 * @param {number} size - 每个切片的大小
 */
const mergeFileChunk = async (filePath, fileName, size) => {
    // 计算切片存储的目录路径
    const chunkDir = path.resolve(`${UPLOAD_DIR}/${fileName}-chunks`);
    // 读取切片目录下的所有切片文件路径
    let chunkPaths = fs.readdirSync(chunkDir);
    // 对切片文件路径按切片编号进行排序
    chunkPaths.sort((a, b) => a.split('-').pop() - b.split('-').pop());

    // 为每个切片文件创建一个读取并写入到合并文件的操作
    const arr = chunkPaths.map((chunkPath, index) => {
        return popeStream(
            // 切片文件的完整路径
            path.resolve(chunkDir, chunkPath),
            // 创建一个可写流,用于将切片内容写入到合并文件的指定位置
            fs.createWriteStream(filePath, {
                start: index * size,
                end: (index + 1) * size
            })
        );
    });

    // 并行执行所有切片的读取和写入操作,等待所有操作完成
    await Promise.all(arr);
};

// 在服务器请求处理逻辑中,检查请求的 URL 是否为 /merge
if (req.url === '/merge') {
    // 解析 POST 请求中的数据
    const data = await resolvePost(req);
    // 从解析结果中获取文件名和切片大小
    const { fileName, size } = data;
    // 计算合并后文件的完整路径
    const filePath = path.resolve(UPLOAD_DIR, fileName);
    // 调用合并文件切片的函数
    await mergeFileChunk(filePath, fileName, size);
    // 向客户端返回合并成功的响应
    res.end('合并成功');
}

完整代码:

js 复制代码
const http = require('http');// 引入 Node.js 的 http 模块,用于创建 HTTP 服务器
const multiparty = require('multiparty');// 引入 multiparty 模块,用于解析表单数据
const path = require('path');// 引入 path 模块,用于处理文件路径
const fs = require('fs-extra');// 引入 fs-extra 模块,提供更强大的文件操作功能

// 定义上传文件的存储目录
const UPLOAD_DIR = path.resolve(__dirname, 'qiepian');

// 处理 POST 请求的数据
const resolvePost = req => {
  return new Promise(resolve => {
    let chunk = '';
    // 监听请求的数据事件,将数据拼接起来
    req.on('data', data => {
      chunk += data;
    });
    // 监听请求结束事件,将拼接好的数据解析为 JSON 对象
    req.on('end', () => {
      resolve(JSON.parse(chunk));
    });
  });
};

// 合并文件切片
const mergeFileChunk = async (filePath, fileName, size) => {
  // 获取切片存储的目录
  const chunkDir = path.resolve(`${UPLOAD_DIR}/${fileName}-chunks`);
  // 获取切片文件列表
  let chunkPaths = fs.readdirSync(chunkDir);
  console.log(chunkPaths);
  // 对切片文件列表进行排序
  chunkPaths.sort((a, b) => a.split('-').pop() - b.split('-').pop());

  // 循环读取每个切片文件,并写入到合并后的文件中
  const arr = chunkPaths.map((chunkPath, index) => {
    return popeStream(
      path.resolve(chunkDir, chunkPath),
      fs.createWriteStream(filePath, {
        start: index * size,
        end: (index + 1) * size
      })
    );
  });

  // 等待所有切片文件写入完成
  await Promise.all(arr);
};

// 读取文件流并写入到指定的可写流中
const popeStream = (path, writeStream) => {
  return new Promise((resolve) => {
    // 创建可读流
    const readStream = fs.createReadStream(path);
    // 监听可读流的 end 事件,当读取完成后删除该文件
    readStream.on('end', () => {
      fs.unlinkSync(path);
      resolve();
    });
    // 将可读流的数据写入到可写流中
    readStream.pipe(writeStream);
  });
};

// 创建 HTTP 服务器
const server = http.createServer(async (req, res) => {
  // 设置响应头,允许跨域请求
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', '*');
  // 处理 OPTIONS 请求
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  // 处理文件切片上传请求
  if (req.url === '/upload') {
    const form = new multiparty.Form();
    // 解析表单数据
    form.parse(req, async (err, fields, files) => {
      if (err) {
        console.log(err);
        return;
      }
      // 获取上传的文件
      const [file] = files.file;
      // 获取文件名
      const [fileName] = fields.fileName;
      // 获取切片名
      const [chunkName] = fields.chunkName;
      // 获取切片存储的目录
      const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
      console.log(chunkDir);
      // 判断切片存储的目录是否存在,如果不存在则创建
      if (!fs.existsSync(chunkDir)) {
        await fs.mkdirs(chunkDir);
      }
      // 将上传的文件移动到切片存储的目录中
      fs.move(file.path, `${chunkDir}/${chunkName}`);
      // 返回响应信息
      res.end('切片上传成功');
    });
  }

  // 处理文件合并请求
  if (req.url === '/merge') {
    // 解析 POST 请求的数据
    const data = await resolvePost(req);
    const { fileName, size } = data;
    // 获取合并后的文件路径
    const filePath = path.resolve(UPLOAD_DIR, fileName);
    // 合并文件切片
    await mergeFileChunk(filePath, fileName, size);
    // 返回响应信息
    res.end('合并成功');
  }
});

// 启动服务器,监听 3000 端口
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

运行正常的话,当你在前端上传一个文件,就会在后端的文件夹下,发现一个 qiepian 的文件夹,里面就是上传的合并之后文件切片 。

二、文件断点续传

文件断点续传的核心思想是当文件片段上传到一半时,暂停传输,之后继续传输时,前端过滤掉已经传输完毕的片段,只传输剩下的片段。像 QQ 上传文件就是断点续传。

实现步骤

  1. 当文件上传过程中暂停后,前端记录已上传的切片信息。
  2. 当前端点击继续传输按钮后,先发一个校验请求来获取后端已经接收到的文件的片段信息(文件名字和编号)。
  3. 前端根据后端返回的信息,过滤掉已经传输完毕的片段,继续传输剩下的片段。

代码实现思路

在现有代码基础上,可以在后端添加一个接口用于返回已接收的切片信息,前端在继续上传时调用该接口,过滤已上传的切片后再进行上传。

js 复制代码
// 后端:当接收到的请求 URL 为 /check 时,执行以下逻辑,该接口用于返回已接收的切片信息
if (req.url === '/check') {
    // 调用 resolvePost 函数解析 POST 请求的数据,这是一个异步操作,会返回一个 Promise
    const data = await resolvePost(req);
    // 从解析后的数据中提取文件名
    const { fileName } = data;
    // 计算存储该文件切片的目录路径
    const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
    // 检查该切片目录是否存在
    if (fs.existsSync(chunkDir)) {
        // 如果目录存在,读取该目录下的所有切片文件的路径
        const chunkPaths = fs.readdirSync(chunkDir);
        // 对每个切片文件的路径进行处理,提取出切片的编号(文件名中 - 后面的部分),并转换为整数
        const uploadedChunks = chunkPaths.map(chunkPath => parseInt(chunkPath.split('-').pop()));
        // 将已上传的切片编号数组转换为 JSON 字符串,并作为响应返回给前端
        res.end(JSON.stringify(uploadedChunks));
    } else {
        // 如果切片目录不存在,说明还没有该文件的切片上传,返回一个空数组的 JSON 字符串给前端
        res.end('[]');
    }
}


// 前端:继续上传文件,过滤掉已上传的切片,只上传剩余切片、
async function continueUpload() {
    // 发送一个 POST 请求到后端的 /check 接口,携带文件名,用于获取已上传的切片信息
    const response = await axios.post('http://localhost:3000/check', {
        fileName: fileObj.file.name
    });
    // 从响应中提取已上传的切片编号数组
    const uploadedChunks = response.data;
    // 使用 filter 方法过滤掉已经上传的切片,只保留未上传的切片
    const remainingChunks = chunkList.filter((_, index) => !uploadedChunks.includes(index));
    // 调用 handleChunk 函数处理剩余的切片,继续进行上传操作
    handleChunk(remainingChunks);
}

前端代码(index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入 axios 库,用于发送 HTTP 请求 -->
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
  <!-- 文件选择输入框 -->
  <input type="file" id="input">
  <!-- 上传按钮 -->
  <button id="btn">上传</button>
  <!-- 继续上传按钮,用于断点续传 -->
  <button id="continueBtn">继续上传</button>

  <script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');
    const continueBtn = document.getElementById('continueBtn');
    const fileObj = {
      file: null
    };
    // 存储文件切片列表
    let chunkList = [];

    // 监听文件选择事件
    input.addEventListener('change', function(e) {
      const [ file ] = e.target.files;
      if (!file) return;
      fileObj.file = file;
    });

    // 监听上传按钮点击事件
    btn.addEventListener('click', function() {
      if (!fileObj.file) return;
      // 先将文件切片
      chunkList = createChunk(fileObj.file);
      // 处理切片
      handleChunk(chunkList);
    });

    // 监听继续上传按钮点击事件
    continueBtn.addEventListener('click', async function() {
      if (!fileObj.file) return;
      // 发送校验请求,获取已上传的切片信息
      const uploadedChunks = await getUploadedChunks(fileObj.file.name);
      // 过滤掉已经上传的切片
      const remainingChunks = chunkList.filter((_, index) =>!uploadedChunks.includes(index));
      // 处理剩余的切片
      handleChunk(remainingChunks);
    });

    // 切片函数,将文件按指定大小进行切片
    function createChunk(file, size = 5 * 1024 * 1024) {
      const chunkList = [];
      let cur = 0;
      while (cur < file.size) {
        // slice 方法的参数是开始位置和结束位置,左闭右开
        const chunk = file.slice(cur, cur + size);
        chunkList.push(chunk);
        cur += size;
      }
      return chunkList;
    }

    // 处理切片函数,将切片信息封装成对象
    function handleChunk(chunkList) {
      const handleChunkList = chunkList.map((chunk, index) => {
        return {
          file: chunk,
          size: chunk.size,
          chunkName: `${fileObj.file.name}-${index}`,
          fileName: fileObj.file.name,
          index
        };
      });
      // 上传切片
      uploadChunks(handleChunkList);
    }

    // 上传切片函数,将切片数据封装成 FormData 并发送请求
    function uploadChunks(handleChunkList) {
      const formDataChunkList =  handleChunkList.map(({ file, chunkName, fileName, index }) => {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('fileName', fileName);
        formData.append('chunkName', chunkName);
        return {formData, index};
      });
      // 将 formDataChunkList 中的 formData 一份一份发送给服务器
      const requestList = formDataChunkList.map(({ formData, index }) => {
        return axios.post('http://localhost:3000/upload', formData);
      });

      Promise.all(requestList).then(res => {
        // 所有切片上传完成后,发送合并请求
        axios.post('http://localhost:3000/merge', {
          size: 5 * 1024 * 1024, 
          fileName: fileObj.file.name
        });
      });
    }

    // 获取已上传的切片信息
    async function getUploadedChunks(fileName) {
      const response = await axios.get(`http://localhost:3000/check?fileName=${fileName}`);
      return response.data;
    }
  </script>
</body>
</html>

后端代码(app.js

javascript 复制代码
const http = require('http');
const multiparty = require('multiparty');
const path = require('path');
const fs = require('fs-extra');

// 定义切片文件存储目录
const UPLOAD_DIR = path.resolve(__dirname, 'qiepian');

// 处理 post 数据: get 的参数直接 req.url 获取,post 的参数需要解析
const resolvePost = req => {
  return new Promise(resolve => {
    let chunk = '';
    // data 事件,获取请求体数据
    req.on('data', data => {
      chunk += data;
    });
    // 自动触发,获取完数据后触发
    req.on('end', () => {
      resolve(JSON.parse(chunk));  // 将字符串转为对象
    });
  });
};

// 合并切片
const mergeFileChunk = async (filePath, fileName, size) => {
  const chunkDir = path.resolve(`${UPLOAD_DIR}/${fileName}-chunks`);
  let chunkPaths = fs.readdirSync(chunkDir);
  console.log(chunkPaths);
  // 按切片编号排序
  chunkPaths.sort((a, b) => a.split('-').pop() - b.split('-').pop());

  const arr = chunkPaths.map((chunkPath, index) => {
    return popeStream(
      path.resolve(chunkDir, chunkPath),
      fs.createWriteStream(filePath, {
        start: index * size,
        end: (index + 1) * size
      })
    );
  });

  await Promise.all(arr);
};

// 读取文件流
const popeStream = (path, writeStream) => {
  return new Promise((resolve) => {
    const readStream = fs.createReadStream(path); // 读取该文件流
    readStream.on('end', () => {
      fs.unlinkSync(path);  // 删除该文件
      resolve();
    });
    // 将读取的文件流写入到指定的文件流中
    readStream.pipe(writeStream);
  });
};

const server = http.createServer(async (req, res) => {
  // 设置跨域请求头
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', '*');
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  if (req.url === '/upload') {
    const form = new multiparty.Form();
    form.parse(req, async (err, fields, files) => {
      if (err) {
        console.log(err);
        return;
      }

      const [file] = files.file;
      const [fileName] = fields.fileName;
      const [chunkName] = fields.chunkName;
      // 保存片段
      const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
      console.log(chunkDir);

      // 判断文件夹是否存在
      if (!fs.existsSync(chunkDir)) {
        await fs.mkdirs(chunkDir);
      }
      // 将切片文件移动到指定目录
      fs.move(file.path, `${chunkDir}/${chunkName}`);

      res.end('切片上传成功');
    });
  }

  if (req.url === '/merge') {  // 切片发送完成,合并某一文件切片
    const data = await resolvePost(req);
    const { fileName, size } = data;

    const filePath = path.resolve(UPLOAD_DIR, fileName);

    // 将 path 路径对应的文件夹下的所有文件合并
    await mergeFileChunk(filePath, fileName, size);
    res.end('合并成功');
  }

  if (req.url.startsWith('/check')) {
    const query = req.url.split('?')[1];
    const params = new URLSearchParams(query);
    const fileName = params.get('fileName');
    const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
    if (fs.existsSync(chunkDir)) {
      const chunkPaths = fs.readdirSync(chunkDir);
      const uploadedChunks = chunkPaths.map(chunkPath => parseInt(chunkPath.split('-').pop()));
      res.end(JSON.stringify(uploadedChunks));
    } else {
      res.end(JSON.stringify([]));
    }
  }
});

server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

三、总结

通过文件切片上传和断点续传技术,可以有效地提高大文件上传的效率和稳定性。前端通过对文件进行切片处理,将切片分批上传到后端,后端接收切片并在合适的时候进行合并。同时,断点续传功能可以让用户在网络不稳定或其他原因导致上传中断时,继续上传未完成的部分,提升用户体验。

相关推荐
PAK向日葵2 小时前
【算法导论】PDD 0817笔试题题解
算法·面试
加班是不可能的,除非双倍日工资4 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi5 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip5 小时前
vite和webpack打包结构控制
前端·javascript
excel5 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国6 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼6 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy6 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT6 小时前
promise & async await总结
前端
Jerry说前后端6 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化