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

在现代 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');
});

三、总结

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

相关推荐
Adellle3 分钟前
Java进阶
java·后端·面试
李小白664 分钟前
Vue背景介绍+声明式渲染+数据响应式
前端·javascript·vue.js
萌萌哒草头将军17 分钟前
🚀🚀🚀Zod 深度解析:TypeScript 运行时类型安全的终极实践指南
javascript·vue.js·react.js
烛阴36 分钟前
JavaScript yield与异步编程
前端·javascript
知识分享小能手38 分钟前
CSS3学习教程,从入门到精通,CSS3 媒体查询实现响应式布局语法指南(21)
前端·css·学习·css3·html5·媒体
斯~内克1 小时前
深入探索Node.js Koa框架:构建现代化Web应用的2000字实践指南
前端·node.js
AI产品黄叔1 小时前
10分钟搞定高德地图MCP!我用AI解决了约会地点选择难题
前端·ai编程·mcp
Moment1 小时前
从 Webpack 源码来深入学习 Tree Shaking 实现原理 🤗🤗🤗
前端·javascript·webpack
Bigger1 小时前
Mac 命令行及 Linux 使用指南与示例
linux·前端·命令行
mercyT1 小时前
npm i 出现的网络问题
前端·npm·node.js