限制图片上传大小
当我们想实现图片上传到服务器,再让服务器返回一个url,我们把这个url设置到img的src属性实现图片的回显。这种场景非常常见。如果用户上传的图片稍微大一些会导致什么
当用户上传的图片稍微大一些时,可能会导致以下问题:
-
上传时间延长: 大图文件会占用更多的网络带宽和服务器资源,因此上传时间可能会延长。用户在上传大图时可能会感受到明显的等待时间。
-
服务器资源压力: 大图文件需要服务器更多的存储空间和处理能力。如果同时有多个用户上传大图,服务器可能会面临存储压力和处理请求的压力。
-
图片回显时间延长: 当服务器处理大图时,生成图片URL可能会变得更慢,导致图片回显的时间延长。用户可能需要等待更长的时间才能看到上传的图片。
所以一般我们会限制用户上传图片的大小
分片上传的优势:
分段上传是一种将大文件划分为多个较小的片段,然后逐个上传的文件上传方式。这种方法有几个优势:
-
断点续传: 分段上传可以实现断点续传的功能。如果在上传过程中出现网络故障或其他原因导致上传中断,用户只需重新上传中断的那一部分,而不需要重新上传整个文件。
-
减小内存消耗: 对于大文件,一次性将整个文件加载到内存中可能导致内存不足或性能下降。分段上传可以降低单个请求的内存消耗,因为每个片段的大小相对较小。
-
提高上传速度: 分段上传可以提高上传速度。由于每个片段是独立上传的,可以并发上传多个片段,从而加速整个文件的上传过程。
-
降低传输中断的影响: 当文件被划分为多个片段时,如果其中一个片段上传失败,仅影响该片段的上传,而不会影响整个文件的上传。
-
适应不稳定网络环境: 在网络不稳定的情况下,分段上传可以更好地应对,因为即使某个片段上传失败,用户仍然有机会通过重新上传单个片段来完成整个文件的上传。
那么如何实现?
我们先看简单篇的处理方式,不涉及到并发上传。
这里使用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博客
以上代码希望给大家带来启发!!