首页>>前端>>Vue->前端大文件上传,即以流的方式上传

前端大文件上传,即以流的方式上传

时间:2023-11-30 本站 点击:0

前言

在上传较大的文件时,将文件切割成多个小块,然后每次只发送一小块,等到全部传输完毕之后,服务端将接受的多个小块进行合并,组成上传的文件,这就是前端上传大文件的方式,也就是所谓的以流的方式上传

下面会介绍如下几个快内容

前端代码如何编写

后端代码如何编写(node)

vue 中如何处理

使用插件如何处理

1. 前端代码实现

这里先不通过 vue,而是通过原生的 html、js 的方式实现上传,如此更加容易理解逻辑,等后面再将其转换成 vue 写法 文件上传通过 axios ,所以,可以先配置其 baseurl,我这里为axios.defaults.baseURL =http://localhost:3000;

html 代码

<div id="app">  <form action="">    <input type="file" name="" id="uploadInput" />    <button id="uploadBtn">上传</button>  </form></div>

1.1 选择上传文件

为 文件域 添加 change 事件,当用户选择要上传的文件后,将文件信息赋值给一个变量,方便上传文件时使用

document    .getElementById("uploadInput")    .addEventListener("change", handleFileChange);let file = null;  // 文件被更改  function handleFileChange(event) {    const file = event.target.files[0];    if (!file) return;    window.file = file;  }

1.2 文件上传

文件上传分为如下几个步骤

① 创建切片

② 上传切片

③ 全部上传成功后,告诉后端,后端将所有的切片整合成一个文件

首先编写几个函数,用于切片的处理及上传,最后再组合到一起实现完整功能

1.2.1 创建切片

// 创建切片  const createFileChunks = function (file, size = 1024*100) {      // 创建数组,存储文件的所有切片      let fileChunks = [];      for (let cur = 0; cur < file.size; cur += size) {        // file.slice 方法用于切割文件,从 cur 字节开始,切割到 cur+size 字节        fileChunks.push(file.slice(cur, cur + size));      }      return fileChunks;    };

createFileChunks 方法接收两个参数

要进行切片的文件对象

切片大小,这里设置默认值为 1024*100,单位为字节

1.2.2 拼接 formData

上传的时候,通过 formData 对象组装要上传的切片数据

/**   * 2、拼接 formData   * 参数1:存储文件切片信息的数组   * 参数2:上传时的文件名称   */  const concatFormData = function (fileChunks, filename) {    /**     * map 方法会遍历切片数组 fileChunks中的元素map 方法会遍历切片数组 fileChunks中的元素,     * 数组中有多少个切片,创建几个 formData,在其中上传的文件名称、hash值和切片,并将此 formData     * 返回,最终chunksList中存储的就是多个 formData(每个切片对应一个 formData)     *     */    const chunksList = fileChunks.map((chunk, index) => {      let formData = new FormData();      // 这个'filename' 字符串的名字要与后端约定好      formData.append("filename", filename);      // 作为区分每个切片的编号,后端会以此作为切片的文件名称,此名称也应该与后端约定好      formData.append("hash", index);      // 后端会以此作为切片文件的内容      formData.append("chunk", chunk);      return {        formData,      };    });    return chunksList;  };

1.2.3 上传切片

遍历上面的 chunksList 数组,调用 axios 对每个 formData 信息进行提交

// 3、上传切片    const uploadChunks=async (chunksList)=>{      const uploadList = chunksList.map(({ formData }) =>        axios({          method: "post",          url: "/upload",          data: formData,        })      );      await Promise.all(uploadList);    }

1.2.4 合并切片

当所有切片都已经上传成功后,告诉后端一声

 // 合并切片    const mergeFileChunks = async function (filename) {      await axios({        method: "get",        url: "/merge",        params: {          filename,        },      });    };

1.2.5 方法组合

上面编写了几个函数,下面将几个方法串联起来,实现切片上传功能

为上传按钮绑定单击事件

document    .getElementById("uploadBtn")    .addEventListener("click", handleFileUpload);

handleFileUpload 函数

 // 大文件上传  async function handleFileUpload(event) {    event.preventDefault();    const file = window.file;    if (!file) return;    // 1、切片切割,第二个参数采用默认值    const fileChunks = createFileChunks(file);    // 2、将切片信息拼接成 formData 对象    const chunksList = concatFormData(fileChunks, file.name);    // 3、上传切片    await uploadChunks(chunksList);    // 4、所有切片上传成功后后,再告诉后端所有切片都已完成    await mergeFileChunks(file.name);    console.log("上传完成");  }

1.2.6 完整代码

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>大文件上传</title>  </head>  <body>    <div id="app">      <form action="">        <input type="file" name="" id="uploadInput" />        <button id="uploadBtn">上传</button>      </form>    </div>  </body></html><script src="https://unpkg.com/axios/dist/axios.min.js"></script><script>  axios.defaults.baseURL = `http://localhost:3000`;  let file = null;  // 文件被更改  function handleFileChange(event) {    const file = event.target.files[0];    if (!file) return;    window.file = file;  }  // 1、创建切片  const createFileChunks = (file, size = 1024 * 100) => {    // 创建数组,存储文件的所有切片    let fileChunks = [];    for (let cur = 0; cur < file.size; cur += size) {      // file.slice 方法用于切割文件,从 cur 字节开始,切割到 cur+size 字节      fileChunks.push(file.slice(cur, cur + size));    }    return fileChunks;  };  /**   * 2、拼接 formData   * 参数1:存储文件切片信息的数组   * 参数2:上传时的文件名称   */  const concatFormData = function (fileChunks, filename) {    /**     * map 方法会遍历切片数组 fileChunks中的元素map 方法会遍历切片数组 fileChunks中的元素,     * 数组中有多少个切片,创建几个 formData,在其中上传的文件名称、hash值和切片,并将此 formData     * 返回,最终chunksList中存储的就是多个 formData(每个切片对应一个 formData)     *     */    const chunksList = fileChunks.map((chunk, index) => {      let formData = new FormData();      // 这个'filename' 字符串的名字要与后端约定好      formData.append("filename", filename);      // 作为区分每个切片的编号,后端会以此作为切片的文件名称,此名称也应该与后端约定好      formData.append("hash", index);      // 后端会以此作为切片文件的内容      formData.append("chunk", chunk);      return {        formData,      };    });    return chunksList;  };  // 3、上传切片  const uploadChunks = async (chunksList) => {    const uploadList = chunksList.map(({ formData }) =>      axios({        method: "post",        url: "/upload",        data: formData,      })    );    await Promise.all(uploadList);  };  // 大文件上传  async function handleFileUpload(event) {    event.preventDefault();    const file = window.file;    if (!file) return;    // 1、切片切割,第二个参数采用默认值    const fileChunks = createFileChunks(file);    // 2、将切片信息拼接成 formData 对象    const chunksList = concatFormData(fileChunks, file.name);    // 3、上传切片    await uploadChunks(chunksList);    // 4、所有切片上传成功后后,再告诉后端所有切片都已完成    await mergeFileChunks(file.name);    console.log("上传完成");  }  // 合并切片  const mergeFileChunks = async function (filename) {    await axios({      method: "get",      url: "/merge",      params: {        filename,      },    });  };  document    .getElementById("uploadInput")    .addEventListener("change", handleFileChange);  document    .getElementById("uploadBtn")    .addEventListener("click", handleFileUpload);</script>

2. 后端代码实现

因为后端不是我们主要关注点,所以直接上代码,就不做太过详细的解释了,有以下几点提起注意

因为前端通过 Promise.all 的方式执行所有的请求,所以切片发送的顺序是随机的,也就是说,后端获取的切片并保存切片的顺序可能是随机的,所以切片文件的名称不一定是从小到大排序的,所以读取切片组成文件时,要先按照切片名称从小答案排序,然后再组合,否则文件可能出错,这在上传大文件的时候非常明显

const multiparty = require("multiparty");const EventEmitter = require("events");const express = require("express");const cors = require("cors");const fs = require("fs");const path = require("path");const { Buffer } = require("buffer");const server = express();server.use(cors());const STATIC_TEMPORARY = path.resolve(__dirname, "static/temporary");const STATIC_FILES = path.resolve(__dirname, "static/files");server.post("/upload", (req, res) => {  const multipart = new multiparty.Form();  const myEmitter = new EventEmitter();  const formData = {    filename: undefined,    hash: undefined,    chunk: undefined,  };  let isFieldOk = false,    isFileOk = false;  multipart.parse(req, function (err, fields, files) {    formData.filename = fields["filename"][0];    formData.hash = fields["hash"][0];    isFieldOk = true;    myEmitter.emit("start");  });  multipart.on("file", function (name, file) {    formData.chunk = file;    isFileOk = true;    myEmitter.emit("start");  });  myEmitter.on("start", function () {    if (isFieldOk && isFileOk) {      const { filename, hash, chunk } = formData;      const dir = `${STATIC_TEMPORARY}/${filename}`;      try {        if (!fs.existsSync(dir)) fs.mkdirSync(dir);        const buffer = fs.readFileSync(chunk.path);        const ws = fs.createWriteStream(`${dir}/${hash}`);        ws.write(buffer);        ws.close();        res.send(`${filename}-${hash} 切片上传成功`);      } catch (error) {        console.error(error);      }      isFieldOk = false;      isFileOk = false;    }  });});server.get("/merge", async (req, res) => {  const { filename } = req.query;  try {    let len = 0;    const hash_arr = fs.readdirSync(`${STATIC_TEMPORARY}/${filename}`);    // 将 hash 值按照大小进行排序    hash_arr.sort((n1, n2) => {      return Number(n1) - Number(n2);    });    const bufferList = hash_arr.map((hash) => {      console.log(hash);      const buffer = fs.readFileSync(`${STATIC_TEMPORARY}/${filename}/${hash}`);      len += buffer.length;      return buffer;    });    const buffer = Buffer.concat(bufferList, len);    const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`);    ws.write(buffer);    ws.close();    res.send(`切片合并完成`);  } catch (error) {    console.error(error);  }});function deleteFolder(filepath) {  if (fs.existsSync(filepath)) {    fs.readdirSync(filepath).forEach((filename) => {      const fp = `${filepath}/${filename}`;      if (fs.statSync(fp).isDirectory()) deleteFolder(fp);      else fs.unlinkSync(fp);    });    fs.rmdirSync(filepath);  }}server.listen(3000, () => {  console.log("Server is running at http://127.0.0.1:3000");});

3. vue 改造

当然只需要改造前端代码,后端代码是不用修改的

新建单文件组件

document    .getElementById("uploadInput")    .addEventListener("change", handleFileChange);let file = null;  // 文件被更改  function handleFileChange(event) {    const file = event.target.files[0];    if (!file) return;    window.file = file;  }0
原文:https://juejin.cn/post/7099098828187385886


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/3801.html