前言:上传文件功能在开发是常见的。如果上传文件的过大,那么一般的上传功能就会有很多问题,接口超时,中途失败,上传时间过久等。所以就需要针对大文件单独处理上传。
该文章代码基于vue3+koa2环境演示,文末附带服务端代码链接
文件上传
前端
话不多说,我们直接开始动手,首先我们先简单写一个页面,然后打印文件信息
<template>
<el-upload
:file-list="upload.fileList"
class="upload-demo"
action="#"
:auto-upload="false"
ref="upload"
:on-change="uploadChange"
>
<el-button type="primary">选择文件</el-button>
</el-upload>
<div><el-button @click="submitFile">上传</el-button></div>
</template>
<script setup>
import { reactive } from 'vue'
import axios from "axios"
const upload = reactive({
//文件列表
fileList: [],
//存储当前文件
currentFile: null,
//当前文件名
name: '',
//存储切片后的文件数组
fileArr: [],
//切片总份数
total:0
})
/**
* 文件change事件
* @param {*} file 当前文件
* @param {*} files 文件数组
*/
const uploadChange = (file, files) => {
//防止change事件多次触发
if (file.status !== 'ready') return
console.log('file', file)
console.log('files', files)
console.log(111)
upload.fileList = files
upload.currentFile = file
upload.name = file.name
}
</script>
切片
切片核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,文件的 slice 方法可以返回原文件的某个切片。
根据打印出来文件信息中的size属性,我们可以使用数组的操作方法slice来进行切片操作。然后我们在上传按钮绑定事件,打印看下切片后的文件数据信息。
//增加如下代码
/**
* 示例为切片为4M作为实验
*/
const createFileChunk = (file, size = 1024 * 1024 * 4) => {
//定义一个数组用来存储每一份切片
const fileChunkList = []
//存储索引,以cur和cur+size作为开始和结束位置利用slice方法进行切片
let cur = 0
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) })
cur += size
}
upload.total = fileChunkList.length
return fileChunkList
}
const submitFile = async () => {
console.log(upload.currentFile)
//文件为空时进行逻辑处理
if (!upload.fileList.length) return
const fileChunkList = createFileChunk(upload.currentFile.raw)
console.log(fileChunkList)
}
可以看到文件已经被切割为四份,每一份均为4M,如果剩余不足4M则会取余下的。切割完成之后我们不能直接马上上传,我们需要考虑下之后可能发生情况,例如中途有个传输失败,或者有个超时咋办,我们需要给每一份切片一个标识。最简单的办法就是当前文件名加上索引。
在submitFile()添加以下代码
// 对每一份切片进行一个标识,用于后续传输判断是否全部传输完成
upload.fileArr = fileChunkList.map(({ file }, index) => ({
chunk: file,
hash: upload.name + '-' + index // 文件名 数组下标
}))
console.log('文件重新命名', upload.fileArr)
切片上传且合并切片
接着我们就可以进行文件的上传。上传跟普通上传一样,就是多次上传并且把所需标识传递给后端即可。这时候就要跟后端商量如何判断这份切片是否上传完成,全部上传完成之后又怎样。笔者这里是自己弄的,所以以code返回码为201标识这份切片上传完成,200则说明全部切片上传完成,可以调用合并切片接口。
// 上传切片
const uploadChunks = async () => {
let count = 0
//设置请求头和监听上传的进度
let configs = {
headers: {
'Content-Type': 'multipart/form-data'
},
//设置超时时间
timeout:600000,
//上传进度展示
onUploadProgress(e) {
//lengthComputable表示该资源是否具有可以计算的长度
if (e.lengthComputable) {
//如果有业务需求需要做到进度条的可视化可以依据这个函数里面来操作
console.log('内置监听上传进度', e.loaded / e.total)
}
}
}
const requestList = upload.fileArr
.map(({ chunk, hash }) => {
const formData = new FormData()
formData.append('file', chunk)
formData.append('hash', hash)
formData.append('filename', upload.currentFile.name)
formData.append('total', upload.total)
return { formData }
})
.map(({ formData }) => {
const res = axios.post('http://localhost:3001/upload/bigFile', formData, configs).then(
(res) => {
// 上传成功后的处理
// console.log('文件上传', res.data)
if (res.data.code === 200) {
//根据所设置的返回码得知所有的切片已上传完成,进行合并操作
mergeRequest(upload.currentFile.name)
}
},
(err) => {
// 出现错误时的处理
console.log(err)
}
)
// console.log(res)
return res
})
console.log('requestList', requestList)
}
submitFile完整代码
const submitFile = async () => {
console.log(upload.currentFile)
//文件为空时进行逻辑处理
if (!upload.fileList.length) return
const fileChunkList = createFileChunk(upload.currentFile.raw)
console.log(fileChunkList)
// 对每一份切片进行一个标识,用于后续传输判断是否全部传输完成
upload.fileArr = fileChunkList.map(({ file }, index) => ({
chunk: file,
hash: upload.name + '-' + index // 文件名 数组下标
}))
console.log('文件重新命名', upload.fileArr)
//并发上传多个请求
await uploadChunks()
}
后端
后端代码是基于之前的文章基础继续编写的。如果不好直接上手的话建议先搭个koa服务。
服务端负责接受前端传输的切片,并在接收到所有切片后合并所有切片。
//npm install fs-extra
//fs-extra 是fs 的扩展,继承了 fs 所有方法并为这些方法添加了 promise 语法
const fse = require("fs-extra");
在upload.js继续编写,分切上传接口和普通上传接口大同小异。我们需要动态建立一个文件夹,存储切片,然后每次前端调用前端上传切片接口的时候需要判断该切片是否已经上传过。
切片上传接口
// 上传文件的目录地址
const UPLOAD_DIR = path.resolve(__dirname, "../../bigFile/upload");
//用于存储已经上传完的文件名,用于判断是否全部已经上传完成
let fileObj = {};
router.post("/bigFile", async (ctx, next) => {
//设置请求过期时间
ctx.request.socket.setTimeout(6 * 3600);
// 文件转移
// koa-body 在处理完 file 后会绑定在 ctx.request.files
const file = ctx.request.files.file;
const body = ctx.request.body;
// console.log("文件", ctx.request.body);
const fileNameArr = ctx.request.body.hash.split(".");
//获取当前是第几份切片
const lastIndex = fileNameArr[1].split("-")[1];
// console.log('当前第几份切片',lastIndex)
// console.log('修改完',fileNameArr)
// 存放切片的目录
const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`;
if (!fse.existsSync(chunkDir)) {
// 没有目录就创建目录
// 创建大文件的临时目录
await fse.mkdirs(chunkDir);
}
//读取当前文件夹的所有文件,以此用来判断切片是否已经全部上传完成,如果当前切片已经上传完成则不操作并返回说明
const files = await fse.readdirSync(chunkDir);
// console.log("当前目录所有文件", files);
// 原文件名.index - 每个分片的具体地址和名字
// console.log('chunkDir',chunkDir)
// console.log('fileNameArr',fileNameArr)
const dPath = path.join(chunkDir, fileNameArr[1]);
// 将分片文件从 bigFile 中移动到本次上传大文件的临时目录
//move(src,dest,options,callback)
/**
* src:它是一个字符串,其中包含要移动的文件的路径(源路径)。
* dest:它是一个字符串,其中包含文件将被移动到的路径(目标路径)。
* options:这是一个对象,其属性覆盖可以为true或false。默认情况下,为:false。如果将其设置为true,则如果目标文件夹中存在具有相同名称的文件,则该文件将被覆盖。
* callback:当执行了move()函数时,将调用该函数。这将导致错误或成功。这是一个可选参数,我们也可以使用promise代替回调函数。
*/
await fse.moveSync(file.filepath, dPath, { overwrite: true });
//检测该对象里面是否包含对应的文件名的数组,检测里面的分切是否重复,没有则放进数组,给后面根据数组长度来判断是否已全部上传
if (Array.isArray(fileObj[body.filename])) {
fileObj[body.filename].indexOf(body.hash) !== -1
? null
: fileObj[body.filename].push(body.hash);
} else {
fileObj[body.filename] = [];
fileObj[body.filename].push(body.hash);
}
console.log("文件数组", fileObj);
if (body.total == fileObj[body.filename].length) {
ctx.body = { msg: "切片全部上传完成", code: 200 };
} else {
ctx.body = { msg: `切片${body.hash}上传完成`, code: 201 };
}
});
合并文件接口
合并文件主要是利用readFileSync来读取当前切片,然后利用appendFileSync来同步追加到文件中
readFileSync用于读取文件并返回其内容。appendFileSync用于将给定数据同步追加到文件中。如果不存在,则创建一个新文件。
// 合并文件
router.post("/mergeFile", async (ctx, next) => {
console.log("merge");
//传入本来的文件名,合并文件
const { name } = ctx.request.body;
const fname = name.split(".")[0];
const chunkDir = path.join(UPLOAD_DIR, fname);
console.log("chunkDir", chunkDir);
// console.log('chunkDir', chunkDir)
const chunks = await fse.readdir(chunkDir);
// console.log('chunks',chunks)
chunks
.sort((a, b) => a - b)
.map((chunkPath) => {
// 合并文件
let data = fse.readFileSync(`${chunkDir}/${chunkPath}`);
fse.appendFileSync(path.join(UPLOAD_DIR, name), data);
});
// 删除临时文件夹
fse.removeSync(chunkDir, (err) => {
console.log("success");
//合并成功后将原本数据清空
fileObj[name] = [];
if (err) throw err;
});
// 返回文件地址
ctx.body = {
msg: "合并成功",
url: `http://localhost:3001/bigFile/upload/${name}`,
};
});
功能基本实现了。可能有些读者觉得基本确实实现了,但是还有个我们没有测试到,那就是我们现在是本地环境,在本地服务器上的文件上传其实就是文件的复制粘贴移动,没有经过网络传输,所以会显得特别快。我们需要弄到服务器上,经过线上环境测试。
部署到服务器
因为这篇文章重点是文件上传,笔者这里是利用宝塔可视化面板来操作。所以我这里大概简略说明。如果想深入了解或者其他方式部署的读者建议寻找专门的文章。
- 首先需要一个服务器,我这里选择的是宝塔面板作为可视化服务器界面,创建一个站点。
- 在软件商店里面下载PM2管理器,MYSQL,PHP等服务。
接下来我们测试下线上环境效果如何:
存在问题
虽然最基本的切片上传,合并文件没问题,但是在使用过程中会出现各种各样问题。所以我们考虑到这些因素并去解决。
控制请求数量
细心的读者会发现,如果切片数量过多,我们上传的时候所有接口会一起请求,这会给服务器造成压力,也会让上传变慢,因为网速会被分摊,这样很容易造成接口超时。这时候我们就需要控制请求数量。
前端:
增加如下代码
/**
* @param {Array<String>} url 请求地址
* @param {Number} n 控制并发数量
* @param {String} params 接口传递参数
* @param {Object} configs 接口配置
*/
const ajax = (url, n, params,configs) => {
const length = params.length
const result = []
let flag = 0 // 控制进度,表示当前位置
let sum = 0 // 记录请求完成总数
return new Promise((resolve, reject) => {
// 先连续调用n次,就代表最大并发数量
while (flag < n) {
next()
}
function next() {
const cur = flag++ // 利用闭包保存当前位置,以便结果能顺序存储
if (cur >= length) return
axios
.post(url,params[cur]['formData'],configs)
.then((res) => {
result[cur] = cur // 保存结果。为了体现顺序,这里保存索引值
if (++sum >= length) {
resolve(result)
} else {
next()
}
})
.catch(reject)
}
})
}
uploadChunks方法更改为:
// 上传切片
const uploadChunks = async () => {
/..../
const requestList = upload.fileArr.map(({ chunk, hash }) => {
const formData = new FormData()
formData.append('file', chunk)
formData.append('hash', hash)
formData.append('filename', upload.currentFile.name)
formData.append('total', upload.total)
return { formData }
})
// http://42.193.168.102:8001/upload/bigFile
ajax('http://42.193.168.102:8001/upload/bigFile', 2,requestList,configs).then((res) => {
console.log('并发',res)
})
}
为了让测试清晰一点,将前面的切片大小由4M改为1M,笔者所选的文件为5M左右,效果如下图:设置接口请求数量为2,所以每次最大请求数为2,有一个完成就将已完成的任务退出队列,将新的任务加入队列以此执行全部。
断点传续
再说到断点传续就是信号中断后(掉线或关机等),下次能够从上次的地方接着传送(一般指下载或上传). 在解决断点传续之前,我们需要先解决前面的小问题,在切片上传接口增加已经上传过的文件判断,如果该切分已经上传过,则不执行文件操作。
切片上传接口代码修改:
router.post("/bigFile", async (ctx, next) => {
//设置请求过期时间
ctx.request.socket.setTimeout(6 * 3600);
// 文件转移
// koa-body 在处理完 file 后会绑定在 ctx.request.files
const file = ctx.request.files.file;
const body = ctx.request.body;
// [ name, index, ext ] - 分割文件名
const fileName = ctx.request.body.hash;
const fileNameArr = ctx.request.body.hash.split(".");
//获取当前是第几份切片
const lastIndex = fileNameArr[1].split("-")[1];
// 存放切片的目录
const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`;
if (!fse.existsSync(chunkDir)) {
// 没有目录就创建目录
// 创建大文件的临时目录
await fse.mkdirs(chunkDir);
}
//读取当前文件夹的所有文件,以此用来判断切片是否已经全部上传完成,如果当前切片已经上传完成则不操作并返回说明
const files = fse.readdirSync(chunkDir);
//检测该对象里面是否包含对应的文件名的数组,检测里面的分切是否重复
if (fileObj[body.filename] instanceof Set) {
//以当前目录存在的文件为主,所以每次重新设置已存在的文件数
fileObj[body.filename] = new Set();
files.map((x) => {
fileObj[body.filename].add(x);
});
//检测如果已经上传过就不再上传
//先判断当前目录是否拥有该文件,再判断当前全局变量中Set结构数据是否存在该文件(这里主要测试一开始上传切片后,然后删掉某个切片)
if (
files.indexOf(body.hash) !== -1 &&
fileObj[body.filename].has(fileName)
) {
return (ctx.body = {
msg: `切片${body.hash}已经上传过,此次操作不进行上传`,
code: 200,
});
} else {
await fileObj[body.filename].add(body.hash);
}
} else {
fileObj[body.filename] = new Set();
fileObj[body.filename].add(body.hash);
}
const dPath = path.join(chunkDir, fileName);
// 将分片文件从 bigFile 中移动到本次上传大文件的临时目录
await fse.move(file.filepath, dPath, { overwrite: true });
if (body.total == fileObj[body.filename].size) {
ctx.body = { msg: "切片全部上传完成", code: 200 };
} else {
ctx.body = { msg: `切片${body.hash}上传完成`, code: 201 };
}
});
为测试我们先将文件上传一次ps:注释掉合并文件接口,然后删掉其中两份文件,启用合并文件接口,然后再上传一次。
秒传
这里我们会发现我们哪怕是已经存在的文件,但是上传时间还是跟没存在的文件一样。这是因为上传接口是传输formData,在你发送请求在后端接收到之前,都需要先将数据传输到网络。所以我们需要新的接口来判断文件是否已存在。也就是所谓秒传。
后端:
//检测文件是否已经存在,进行秒传
router.get("/checkFile", async (ctx, next) => {
const fileNameArr = ctx.request.query.name.split(".");
// 存放切片的目录
const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`;
if (!fse.existsSync(chunkDir)) {
// 没有目录就创建目录
// 创建大文件的临时目录
await fse.mkdirs(chunkDir);
}
const files = fse.readdirSync(chunkDir);
const name = ctx.request.query.name;
console.log(name);
console.log(files);
if (files.indexOf(name) !== -1) {
ctx.body = { msg: `${name}文件已经上传过`, code: 200 };
} else {
ctx.body = { msg: `${name}文件未上传`, code: 205 };
}
});
前端:修改ajax方法里面代码
const ajax = (url, n, params, configs) => {
const length = params.length
const result = []
let flag = 0 // 控制进度,表示当前位置
let sum = 0 // 记录请求完成总数
return new Promise((resolve, reject) => {
// 先连续调用n次,就代表最大并发数量
while (flag < n) {
next()
}
function next() {
const cur = flag++ // 利用闭包保存当前位置,以便结果能顺序存储
if (cur >= length) return
axios
.get('http://42.193.168.102:8001/upload/checkFile', { params: { name: params[cur]['formData'].get('hash') } })
.then((res) => {
result[cur] = cur // 保存结果。为了体现顺序,这里保存索引值
//下面有几段代码相似是因为接口请求是异步,如果不在回调里面判断,会出现合并判断通过但是有些文件还没上传的情况
if (res.data.code === 200) {
if (++sum >= length) resolve(result)
next()
} else {
if (res.data.code === 205) {
axios.post(url, params[cur]['formData'], configs).then((mearg) => {
if (++sum >= length) resolve(result)
next()
})
} else {
throw Error('接口错误')
}
}
})
}
})
}
我们再重新上传两次看看效果
接下来我们测试有些文件未上传有些已上传的效果。我们需要先注释掉合并文件接口,然后删掉部分文件。
实验遇到一些疑问和示例
某份分片不完整的场景
在上面功能其实断点传续已经差不多了。但是笔者又想到一个场景,如果某份切片不完整的话该怎么办。(ps:此处仅为模拟场景,需要假设上传是按照里面字节数据是从头到尾逐字节上传。)
首先我们需进行axios请求的取消操作。
- 在
script定义全局变量let cancel;和const CancelToken = axios.CancelToken - 在
uploadChunks方法里面增加如下代码
let configs = {
headers: {
'Content-Type': 'multipart/form-data',
},
//+++start
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c
}),
//+++end
/...../
}
3.生成一个按钮,绑定取消请求事件
<div><el-button @click="abortHttp">取消请求</el-button></div>
//取消请求
const abortHttp = () => {
console.log('取消请求')
cancel()
}
ps:实验的时候记得换为线上服务的地址,记得更新线上服务的代码。不然本地服务响应太快。切片大小更改为8M每份,便于时间充裕来取消。
问题:
实验的时候发现这样一个场景:假设你网速在500k左右,这份切片4M,大概要8秒左右上传完成,但是你在3,4秒内取消,在服务器文件夹会发现,过一会文件还是上传上去了。如果是8M就不会有这个情况。这里猜测取消请求之后,网络传输并未实时中断,还会持续一小会。如果刚好传输完成,那么还是会经过后端服务操作。但是无返回结果(因为已取消).如果有大佬知道原因麻烦告知下。
模拟文件不完整
前提:假设文件上传是按顺序逐字节上传。
1.前端在请求检查文件接口(checkFile) 的时候,需要加上当前切片的文件大小。
//uploadChunks方法
const requestList = upload.fileArr.map(({ chunk, hash }) => {
const formData = new FormData()
/.../
+++ formData.append('fileSize',chunk.size)
return { formData }
})
//ajax方法,增加传递的参数
axios.get('http://42.193.168.102:8001/upload/checkFile', { params: { name: form.get('hash'),fileSize:form.get('fileSize') } })
2.后端在 检查文件接口(checkFile) 增加判断当前切片目录下的文件和将要上传的切片文件大小是否一致。如果不一致则返回特定的返回码。
//检测文件是否已经存在,进行秒传
router.get("/checkFile", async (ctx, next) => {
const fileNameArr = ctx.request.query.name.split(".");
// 存放切片的目录
const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`;
if (!fse.existsSync(chunkDir)) {
await fse.mkdirs(chunkDir);
}
const files = fse.readdirSync(chunkDir);
const name = ctx.request.query.name;
const fileSize = ctx.request.query.fileSize;
try {
const fileMsg = fse.statSync(chunkDir + `/${name}`);
// console.log('文件信息',fileMsg)
//文件大小不等说明文件缺失部分
if (fileMsg.size != fileSize)
return (ctx.body = {
msg: `${name}文件未上传完成,剩余${fileSize - fileMsg.size}B`,
code: 207,
size: fileSize - fileMsg.size,
});
} catch (error) {
console.log("目录下无该文件信息", error);
}
if (files.indexOf(name) !== -1) {
ctx.body = { msg: `${name}文件已经上传过`, code: 200 };
} else {
ctx.body = { msg: `${name}文件未上传`, code: 205 };
}
});
3.前端根据特定的返回码,在上传切片的时候判断,通过则计算还剩下未上传的文件,并切割上传剩余部分。
const ajax = (url, n, params, configs) => {
/代码省略.../
function next() {
const cur = flag++ // 利用闭包保存当前位置,以便结果能顺序存储
if (cur >= length) return
let form = params[cur]['formData']
axios
.get('http://42.193.168.102:8001/upload/checkFile', { params: { name: form.get('hash'),fileSize:form.get('fileSize') } })
.then((res) => {
result[cur] = cur // 保存结果。为了体现顺序,这里保存索引值
// next()
//下面有几段代码相似是因为接口请求是异步,如果不在回调里面判断,会出现合并判断通过但是有些文件还没上传的情况
if (res.data.code === 200) {
if (++sum >= length) resolve(result)
next()
} else {
if (res.data.code === 205||res.data.code===207) {
//返回码207则说明文件缺失一部分,计算还缺失的部分,进行文件切割再上传
if(res.data.code===207){
form.append('iscomplete',0)
form.set('file',form.get('file').slice(form.get('fileSize')-res.data.size))
}
axios.post(url, params[cur]['formData'], configs).then((mearg) => {
if (++sum >= length) resolve(result)
next()
})
}else {
throw Error('接口错误')
}
}
})
}
})
}
4.后端在 bigFile接口 中进行判断,以iscomplete为标识判断该分切片是否为完整,如果不完整则将目录下已有的相同文件名的文件和上传的这份文件进行合并。
router.post("/bigFile", async (ctx, next) => {
/代码省略.../
//检测该对象里面是否包含对应的文件名的数组,检测里面的分切是否重复,没有则放进数组,给后面根据数组长度来判断是否已全部上传
if (fileObj[body.filename] instanceof Set) {
//以当前目录存在的文件为主,所以每次重新设置已存在的文件数
fileObj[body.filename] = new Set();
files.map((x) => {
fileObj[body.filename].add(x);
});
if (
files.indexOf(body.hash) !== -1 &&
//多加一层判断
+++ fileObj[body.filename].has(fileName)&&body.iscomplete==undefined
) {
return (ctx.body = {
msg: `切片${body.hash}已经上传过,此次操作不进行上传`,
code: 200,
});
} else {
await fileObj[body.filename].add(body.hash);
}
} else {
fileObj[body.filename] = new Set();
fileObj[body.filename].add(body.hash);
}
const dPath = path.join(chunkDir, fileName);
if (body.iscomplete == 0) {
let content = fse.readFileSync(file.filepath);
console.log('content',content)
fse.appendFileSync(dPath, content);
console.log('2222',fse.statSync(dPath))
} else {
await fse.move(file.filepath, dPath, { overwrite: true });
}
// console.log("文件数组", body.total, files.length);
console.log(fileObj[body.filename], files);
if (body.total == fileObj[body.filename].size) {
ctx.body = { msg: "切片全部上传完成", code: 200 };
} else {
ctx.body = { msg: `切片${body.hash}上传完成`, code: 201 };
}
});
实验过程:
实验上没问题。但是这一切的大前提还是文件上传是逐字节上传,而且上面的方法目前除了TXT格式打开不是乱码删除末尾部分重新上传后是原来的文件。
其他打开是乱码的切片删除部分后再重新上传合并文件就会错误。究其原因就是文件改变了,我们去验证一番。
验证切分修改后的差异
txt格式
后端再写一个接口,利用md5来加密会可以方便比较两个文件的差异
npm install md5-node --save
const md5 = require('md5-node');
router.get("/checkFileSame", async (ctx, next) => {
let filePath1 = await fse.readFile(`${UPLOAD_DIR}/1.txt-0`);
let filePath2 = await fse.readFile(`${UPLOAD_DIR}/2.txt-0`);
console.log(md5(filePath1));
console.log(md5(filePath2));
ctx.body = {
msg: "success",
code: 200,
};
});
其他格式
这里笔者以酷狗文件作为测试文件
我们修改下读取的文件名,再调一次接口:
最后我们可以得出结论,如果是txt这种纯文本格式,进行切片修改再补全是不会改变源文件,其他格式就会。至于深层次原因如果有大佬知道麻烦评论区告知下,万分感谢
总结:关于大文件上传功能的分享就到这了。很多地方其实还没去实验。例如类似网盘一份文件暂停之后又重新在当前进度继续上传,网上没找到类似的文章。
如果这篇文章帮助到你了,那将万分荣幸。如果有哪里写的不对或者不好请大佬们指出。(* ̄︶ ̄)。最后可以的话能否给个小小的赞。