图片的分片上传-node篇

  限制图片上传大小 

        当我们想实现图片上传到服务器,再让服务器返回一个url,我们把这个url设置到img的src属性实现图片的回显。这种场景非常常见。如果用户上传的图片稍微大一些会导致什么

当用户上传的图片稍微大一些时,可能会导致以下问题:

  1. 上传时间延长: 大图文件会占用更多的网络带宽和服务器资源,因此上传时间可能会延长。用户在上传大图时可能会感受到明显的等待时间。

  2. 服务器资源压力: 大图文件需要服务器更多的存储空间和处理能力。如果同时有多个用户上传大图,服务器可能会面临存储压力和处理请求的压力。

  3. 图片回显时间延长: 当服务器处理大图时,生成图片URL可能会变得更慢,导致图片回显的时间延长。用户可能需要等待更长的时间才能看到上传的图片。

所以一般我们会限制用户上传图片的大小 

分片上传的优势:

分段上传是一种将大文件划分为多个较小的片段,然后逐个上传的文件上传方式。这种方法有几个优势:

  1. 断点续传: 分段上传可以实现断点续传的功能。如果在上传过程中出现网络故障或其他原因导致上传中断,用户只需重新上传中断的那一部分,而不需要重新上传整个文件。

  2. 减小内存消耗: 对于大文件,一次性将整个文件加载到内存中可能导致内存不足或性能下降。分段上传可以降低单个请求的内存消耗,因为每个片段的大小相对较小。

  3. 提高上传速度: 分段上传可以提高上传速度。由于每个片段是独立上传的,可以并发上传多个片段,从而加速整个文件的上传过程。

  4. 降低传输中断的影响: 当文件被划分为多个片段时,如果其中一个片段上传失败,仅影响该片段的上传,而不会影响整个文件的上传。

  5. 适应不稳定网络环境: 在网络不稳定的情况下,分段上传可以更好地应对,因为即使某个片段上传失败,用户仍然有机会通过重新上传单个片段来完成整个文件的上传。

 那么如何实现?

我们先看简单篇的处理方式,不涉及到并发上传。

这里使用element-ui提供的upload组件,但是它不支持分片上传,需要额外处理。而且一旦选择文件就会自动上传,而我们很多时候喜欢手动触发上传。看下面处理方式:

手动上传没用采用官网的方式用this.$refs.upload.submit();因为 submit()之后仍然会触发action里面的请求,这个不能适合分片上传;我又试了http-request说是可以覆盖默认的上传行为,可以自定义上传的实现,按理说可以实现我想要的分片,但是submit()时候即触发http-request又触发了action,而且还是在我使用了auto-upload情况下,仍然是我一选择文件就给我上传完了。不知道大家又遇到过吗?于是我直接通过点击事件触发upload主函数,绕过upload组件的自动提交,这会带来的坏处是上传文件的失败与否都不再是el-upload的范畴里面也就是不会触发对应回调函数,同样 :file-list="fileList"里面的fileList也不会帮你自动添加和删除了,选取文件后要给fileList复制操作,上传完了要fileList.pop(),而且不会触发beforeRemove",不过手动点击’ב删除操作会触发beforeRemove但是仍然移除不了fileList[0]。不知道的神奇bug又增多了;handleChange也只会在你选取文件的时候触发一次,文件上传后成功与否都不会执行。所以我们必定要在upload主函数下功夫去处理不可测的问题。

<template>
  <div>
    <el-upload
      ref="upload"
      drag //支持拖拽
      action=""
      :on-change="handleChange"
      :before-remove="beforeRemove"
      :on-error="handleError"
      :list-type="listType" 
      :accept="accept"  //接受上传的文件类型
      :file-list="fileList" //存储上传的文件数组
      :limit="maxNumber" //限制上传文件个数
      :auto-upload="false" //阻止el-upload自动上传
      :on-exceed="handlerExceed" //超出limit时调用
    >
    </el-upload>

   <el-button type="success"  @click="submitUpload">
        上传
    </el-button>
  </div>
</template>

<script>
export default {
  props: {
    listType: {
      type: String,
      default: "picture"
    },
    accept: {
      type: String,
      default: "image/*"
    },
    maxSize: {
      type: Number,
      default: 2
    },
    maxNumber: {
      type: Number,
      default: 1
    }
  },

  data () {
    return {
      fileList:[]
    }
  },
  methods: {
//正常触发
    handlerExceed(){
      this.$message({
          message: `只限制张${this.maxNumber}图片!`,
          type: "warning"
        });
    },
     submitUpload () {
      if(this.fileList.length===0) {
        return this.$message({
          message: "你还未选定图片",
          type: "warning"
        });
      }
      if (this.fileList[0].size > this.maxSize * 1024 * 1024) {
        return this.$message({
          message: "上传失败!图片最大为" + this.maxSize  + "M!请重新上传",
          type: "warning"
        });
      }
      //上传
      this.uploadHandler(this.fileList)
      //上传完成后移除
      this.fileList.pop()
      //
    },
    //实际不会触发了
    handleError (err, file, fileList) {
      this.$message({
        message: err,
        type: "error"
      });
    },
    //手动点击关闭按钮时触发
     beforeRemove(file,fileList){
//手动移除
      this.fileList.pop()
      return false 
    },	
     // 实际只在添加文件触发
    handleChange(file, fileList){
      if (file.size > this.maxSize * 1024 * 1024) {
        this.$message({
          message: "图片最大为" + this.maxSize + "M!",
          type: "warning"
        });
      }
//手动复制
      this.fileList=fileList
    },
   
  }
}
</script>

注意上面代码只适合一次上传一张图片,主要是提供分片上传的大致思路。可根据需求自行更改。

 核心代码:uploadHandler:

 uploadHandler (fileList) {
      if(!fileList[0]) return
      const that=this
      const file = fileList[0].raw, chunkSize = 1024 * 1024; // 每次限制上传文件的大小为1MB
      async function upload (index) {
        console.log('index:',index)
        const start = index * chunkSize;
        const [filename, ext] = file.name.split('.');

        // 进行切片
        if (start > file.size) {
          // 上传完毕了之后进行切片
          return merge(file.name);
        }
        // 切片为blob
        const blob = file.slice(start, start + chunkSize)
        const blobName = `${filename}.${index}.${ext}`;
        const blobFile = new File([blob], blobName);

        const form = new FormData();
        form.append("file", blobFile)
        try {
          await fetch(`http://localhost:9001/reception/fileUpload/upload`, { method: "POST", body: form })
          upload(++index);
        } catch (err) { }
      }

      async function merge (name) {
        const headers = new Headers();
        headers.append("Content-Type", "application/json")
        try {
          const res = await fetch('http://localhost:9001/reception/fileUpload/merge', { method: "POST", body: JSON.stringify({ name }), headers })
          const restUrl = await res.json();
          // setAvatar(restUrl.url)
         console.log(restUrl.url)
         that.$emit('addPicture', restUrl.url)
        } catch (err) {
          console.log(err)
        }
      }
      upload(0)
    },

前端大致操作就是首先指定分块的大小,再对文件进行切片,为了让后端知道哪个分片是是哪个上传文件的,以及分片顺序是怎样的,所以我们要对每个分片上面放置一些信息又来提示后端。这里我们只对分片的命名上做一些信息的暗含。具体看后端需求。所有的分片上传完成就调/merge接口去拿到最终的url

来浅浅的看一下node.js写的后端吧:

const express = require("express"),
  //多方(multiparty):一个node.js模块,用于解析支持流的多部分表单数据请求
  multiparty = require("multiparty"),
  fse = require("fs-extra"),
  path = require("path"),
  // 文件的操作扩展(fs-extra)
  fs = require("fs"),
  router = express.Router();

const UPLOAD_DIR = path.resolve(__dirname, '../', "upload")

router.post("/upload", async function (req, res) {
  // uploadDir 文件上传的位置目录
  // Form 实例的创建参数是一个包含配置选项的对象,其中包括 uploadDir。这个参数指定了上传文件的临时目录,即在服务器上保存上传文件的临时位置。
  const form = new multiparty.Form({ uploadDir: 'temp' });
  //parse: 解析一个正在到达的包含表单数据的node.js请求。这将使表单根据到达的请求去触发事件。
  // form.parse 将请求参数传入,multiparty会进行相应处理
  // console.log(req)
  form.parse(req);
  /**
   * name 前端formData append的key
   * chunk 前端formData append的value
   */
    // form.on('file',()=>{}) 接收到文件参数时,触发file事件
  form.on('file', async function (name, chunk) {
    // 存放切片的目录 filename.index.ext (存放在/src/upload/originalFilename)
    const chunkDir = `${UPLOAD_DIR}/${chunk.originalFilename.split(".")[0]}`
    if (!fse.existsSync(chunkDir)) {
      // fse.existsSync 可以用于检查文件或目录的存在。
      // fse.mkdirs: 通过fs-extra创建文件目录
      await fse.mkdirs(chunkDir)
    }
    // 按照索引编号index再次命名
    const dPath = path.join(chunkDir, chunk.originalFilename.split(".")[1])
    // 将上传的文件移动到我们的新目录下
    await fse.move(chunk.path, dPath, { overwrite: true })
    // 给出提示
    res.json({ code: 200, msg: "上传成功!" })
  })
})

router.post("/merge", async function (req, res) {
  const { name } = req.body;
  const fname = name.split(".")[0];
  // 拿到资源目录的所有切片
  let chunkDir = path.join(UPLOAD_DIR, fname);
  let chunks = await fse.readdir(chunkDir)

  const LAST_FILE_NAME = path.join(UPLOAD_DIR, name);

  chunks.sort((a, b) => a - b).map(chunk => {
    fs.appendFileSync(LAST_FILE_NAME, fs.readFileSync(`${chunkDir}/${chunk}`));
  })

  // 删除临时文件
  fse.removeSync(chunkDir);

  return res.json({ code: 200, msg: "merge ok!!!", url: `http://localhost:9001/upload/${name}` })
})


module.exports = router;

后端大致思路就是拿到前台上传文件的首块分片时,先在upload文件夹下创建一个专门放置来自同一上传文件的分块的文件夹。当该前台调用/merge接口,根据接口参数指定的文件名字,后端知道哪个上传文件的所有分块都拿到了,于是将这个文件夹下所有的分块文件进行拼接完成后放置upload目录下,最后存放分块的空文件可以删除了。

并发上传可以看Vue+element-ui实现大文件分片上传,可控制同时上传的并发数_el-upload控制并发数-CSDN博客

以上代码希望给大家带来启发!! 

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值