SpringBoot+Vue實(shí)現(xiàn)大文件分塊上傳
1. 項(xiàng)目背景
由于用戶需求,需要上傳大量圖片,只能通過上傳壓縮包的形式上傳,可是壓縮包過大時,又會出現(xiàn)上傳超時的情況,故需要將壓縮包分塊上傳,然后解壓縮圖片、若圖片過大則再對圖片進(jìn)行壓縮。
2. 分塊上傳
分塊上傳我在用的時候發(fā)現(xiàn)有兩種:第一種:分塊合并接口全由后端接口生成;第二種:前端分塊,后端上傳
開始用的第一種,結(jié)果發(fā)現(xiàn)生成的文件沒有內(nèi)容,應(yīng)該是向文件內(nèi)寫的方法有問題;所以又用的第二種;
2.1. 前端分頁
js文件:
import http from './index'
export const white = (data) => http({
url: '/api/file/upload',
method: 'post',
data
})
頁面:
<template>
<div>
<el-form ref="editForm" :model="formFileds">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item>
<el-input></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<el-button type="primary">
查詢
</el-button>
<el-button type="primary" @click="add()">
增加
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div>
<!-- <button type="button" v-on:click="selectFile()" class="btn btn-white btn-default btn-round">
<i class="ace-icon fa fa-upload"></i>{{ text }}
</button> -->
<input class="hidden" type="file" ref="file" v-on:change="uploadFile()" v-bind:id="inputId + '-input'">
</div>
<!--表格內(nèi)容-->
<el-table ref="list" :data="tableData" style="width: 100%;margin-bottom: 5=1px;" border stripe highlight-current-row
:default-sort="{ prop: 'sx', order: 'sx' }">
<!-- v-for="item in tableData" v-bind:key="item.index" -->
<el-table-column fixed type="selection" width="45">
</el-table-column>
<el-table-column property="name" label="名稱" align="center">
</el-table-column>
<!-- <el-form-item label="發(fā)布狀態(tài)" prop="fbzt">
<el-switch ></el-switch>
</el-form-item> -->
<el-table-column property="fbzt" label="發(fā)布狀態(tài)" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.fbzt" disabled></el-switch>
</template>
</el-table-column>
<el-table-column property="sx" label="順序" align="center">
</el-table-column>
<el-table-column fixed="right" label="操作" align="center">
<template slot-scope="scope">
<!-- <el-button circle icon="el-icon-refresh" title="重置密碼" type="success" size="small"></el-button>
<el-button circle icon="el-icon-phone" title="變更手機(jī)號" type="success" size="small"></el-button> -->
<el-button circle icon="el-icon-edit-outline" type="primary" @click="Compile()" title="編輯" size="small">
</el-button>
<el-button circle icon="el-icon-delete" type="danger" title="刪除" size="small"
@click="rowDel(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<!--分頁-->
<el-pagination :page-sizes="[10, 20, 30, 40, 100]" :page-size="10" :total="100"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<Add v-if="isShowAddDialog" :isShowAddDialog="isShowAddDialog" @dialogClose="dialogClose"></Add>
<Compile v-if="isShowComDialog" :isShowComDialog="isShowComDialog" @dialogClose="dialogClose"></Compile>
</div>
</template>
<script>
import Add from "./auctionProcessAdd.vue";
import Compile from "./auctionProcesscomple.vue";
import { getAllJmxz,deleteJmxz } from '@/api/systemAdministrator/auctionProcess';
import {white} from '@/api/white'
import axios from "axios";
export default {
name: "auctionProcess",
components: {
Add,
Compile,
},
data() {
return {
editProps: '',
text: '',
inputId: '',
hex_md5: '',
formFileds: {
},
tableData: [
{
id: 0,
name: '',
fbzt: '',
sx: 0,
nr: "",
bz: '',
},
],
pageSize: 10,
total: 0,
currentPage: 1,
editProps: '',
isShowAddDialog:false,
isShowComDialog:false,
}
},
methods: {
/**
* 點(diǎn)擊【上傳】
*/
// selectFile () {
// let _this = this;
// $("#" + _this.inputId + "-input").trigger("click");
// },
/**
* 上傳文件
*/
uploadFile () {
let _this = this;
// 1. 獲取 input 中被選中的文件
let file = _this.$refs.file.files[0];
// 2. 生成文件標(biāo)識,標(biāo)識多次上傳的是不是同一個文件
// let key = md5(file.name + file.size + file.type);
// let key10 = parseInt(key, 16);
// let key62 = Tool._10to62(key10);
let time = Date.now();
// console.log(time);
let fileName = file.name;
let temp = fileName.split('.');
let key62 = time + '`' + temp[0];
// console.log(key62);
// 判斷文件格式 (非必選,根據(jù)實(shí)際情況選擇是否需要限制文件上傳類型)
// let suffixs = _this.suffixs;s
let suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();
// if (!(!suffixs || JSON.stringify(suffixs) === "{}" || suffixs.length === 0)) {
// let validateSuffix = false;
// for (let s of suffixs) {
// if (s.toLocaleLowerCase() === suffix) {
// validateSuffix = true;
// break;
// }
// }
// if (!validateSuffix) {
// Toast.warning("文件格式不正確!只支持上傳:" + suffixs.join(","));
// $("#" + _this0.inputId + "-input").val("");
// return;
// }
// }
// 3. 文件分片開始
// 3.1 設(shè)置與計算分片必選參數(shù)
let shardSize = 20 * 1024 * 1024; // 20M為一個分片
let shardIndex = 1; // 分片索引,1表示第1個分片
let size = file.size; // 文件的總大小
let shardTotal = Math.ceil(size / shardSize); // 總分片數(shù)
// 3.2 拼接將要傳遞到參數(shù), use 非必選,這里用來標(biāo)識文件用途。
let param = {
'shardIndex': shardIndex,
'shardSize': shardSize,
'shardTotal': shardTotal,
'use': _this.use,
'name': file.name,
'suffix': suffix,
'size': file.size,
'key': key62
};
// 3.3 傳遞分片參數(shù),通過遞歸完成分片上傳。
_this.upload(param);
},
/**
* 遞歸上傳分片
*/
upload (param) {
let _this = this;
let shardIndex = param.shardIndex;
let shardTotal = param.shardTotal;
let shardSize = param.shardSize;
// 3.3.1 根據(jù)參數(shù),獲取文件分片
let fileShard = _this.getFileShard(shardIndex, shardSize);
// 3.3.2 將文件分片轉(zhuǎn)為base64進(jìn)行傳輸
let fileReader = new FileReader();
// 讀取并轉(zhuǎn)化 fileShard 為 base64
fileReader.readAsDataURL(fileShard);
// readAsDataURL 讀取后的回調(diào),
// 將 經(jīng)過 base64 編碼的 分片 整合到 param ,發(fā)送給后端,從而上傳分片。
fileReader.onload = function (e) {
let base64 = e.target.result;
param.shard = base64;
console.log('shard',param.shard)
// Loading.show();
let params = new FormData();
params = param;
// params.append=(param);
// const params2 = JSON.stringify(params);
// console.log('params2',params2);
white(params).then((res) => {
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!');
// console.log(params);
// axios.post('/api/file/upload', params).then((res)=> {
// Loading.hide();
let resp = res.data;
// 上傳結(jié)果
// 當(dāng)前分片索引小于 分片總數(shù),繼續(xù)執(zhí)行分派,反之 則表示全部上傳成功。
if (shardIndex < shardTotal) {
// 上傳下一個分片
param.shardIndex = param.shardIndex + 1;
// this.$message.warning('正在上傳');
_this.upload(param);
} else {
// 文件上傳成功后的回調(diào)
// _this.afterUpload(resp);
return ;
// this.$message.success('上傳成功');
}
// $('#' + _this.inputId + '-input').val('');
});
};
},
// axios.post('/api/file/upload',params).then((res)=>{
// // white(param).then((res) => {
// // _this.$ajax.post( "localhost:8080/admin/upload", param).then((res)=> {
// // Loading.hide();s
// let resp = res.data;
// // 上傳結(jié)果
// // 當(dāng)前分片索引小于 分片總數(shù),繼續(xù)執(zhí)行分派,反之 則表示全部上傳成功。
// if(shardIndex < shardTotal) {
// // 上傳下一個分片
// param.shardIndex = param.shardIndex + 1;
// _this.upload(param);
// } else {
// // 文件上傳成功后的回調(diào)
// _this.afterUpload(resp);
// }
// $("#" + _this.inputId + "-input").val("");
// });
/**
* 文件分片函數(shù)
*/
getFileShard (shardIndex, shardSize) {
let _this = this;
let file = _this.$refs.file.files[0];
let start = (shardIndex - 1) * shardSize; // 當(dāng)前分片起始位置
let end = Math.min(file.size, start + shardSize); // 當(dāng)前分片結(jié)束位置
let fileShard = file.slice(start, end); // 從文件中截取當(dāng)前的分片數(shù)據(jù)
return fileShard;
},
rowEdit(row) {
if(row.fbzt == '已提交'){
row.fbzt = 'true';
}
else{
row.fbzt = 'false';
}
// //當(dāng)前選中行
this.rowDataS = row;
console.log(1)
},
dialogClose() {
this.isShowEditDialog = false;
this.isShowAddDialog = false;
this.isShowComDialog = false;
},
handleEdit(id) {
this.$refs.editForm.validate(isValid => {
if (!isValid) return;
// 保存編輯后的數(shù)據(jù)
Object.assign(this.tableData[id], this.formFileds);
this.isShowEditDialog = false;
// 考慮到可能編輯了日期-需要重新排序
// ***注意:手動排序傳參和表格的default-sort屬性格式不太一樣
this.$refs.list.sort('date', 'descending');
this.$message.success('編輯成功');
});
},
getList(){
this.param = 'pageno=' + this.currentPage + '&countonepage=' + this.pageSize;
getAllJmxz(this.param).then((res) => {
if(res){
console.log(res.data);
console.log(res.msg);
this.tableData = res.data.list;
this.total = res.data.total;
for(let item of this.tableData){
if(item.fbzt == '已提交'){
item.fbzt = true;
}
else{
item.fbzt = false;
}
}
}
})
},
add() {
console.log("add");
this.isShowAddDialog = true;
},
Compile() {
console.log("Compile");
this.isShowComDialog = true;
},
rowDel(id) {
// this.formFileds = row;
console.log(id);
this.$confirm('確定要刪除當(dāng)前行嗎?', '刪除', {
comfirmButtonText: '確定',
cancelButtonText: '取消'
}).then(() => {
deleteJmxz(id).then((res) => {
if(res){
console.log(res.data);
console.log(res.msg);
this.tableData.splice(id, 1);
this.$message.success('刪除成功');
this.getList();
}
})
});
},
// 選中當(dāng)前行-當(dāng)前行的復(fù)選框被勾選
setCurRowChecked(row) {
this.$refs.list.clearSelection();
this.$refs.list.toggleRowSelection(row);
},
},
mounted(){
this.getList();
// console.log(this.tableData);
// console.log(new Date().toLocaleString());
}
}
</script>
<style scoped lang="less">
.el-form {
padding: 0 10px;
}
.el-date-editor {
width: 100% !important;
}
</style>
2.2. 后端合并
接收類:
package com.example.springboot.domain;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
@Data
public class FileDto {
/**
* id
*/
private String id;
/**
* 相對路徑
*/
private String path;
/**
* 文件名
*/
private String name;
/**
* 后綴
*/
private String suffix;
/**
* 大小|字節(jié)B
*/
private Integer size;
/**
* 用途
*/
private String use;
/**
* 創(chuàng)建時間
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createdAt;
/**
* 修改時間
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updatedAt;
private Integer shardIndex;
private Integer shardSize;
private Integer shardTotal;
private String key;
/**
* base64
*/
private String shard;
}
Controller:
package com.example.springboot.controller;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ZipUtil;
import com.example.springboot.common.Base64ToMultipartFile;
import com.example.springboot.common.FileViewer;
import com.example.springboot.common.ReturnResult;
import com.example.springboot.domain.FileDto;
import net.coobird.thumbnailator.Thumbnails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@RequestMapping("/file")
@RestController
public class UploadController {
private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
private static final String storePath = "E:/usr/local/apps/pmxt-download/merge/";
@PostMapping("/upload")
public ReturnResult upload(@RequestBody FileDto fileDto) throws Exception {
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd/");
String format = simpleDateFormat.format(date);
LOG.info("日期信息:"+format);
String tem = storePath + format + fileDto.getName();
File f1 = new File(tem);
if (f1.exists()){
f1.delete();
}
LOG.info("上傳文件開始");
String use = fileDto.getUse();
String key = fileDto.getKey();
String suffix = fileDto.getSuffix();
String shardBase64 = fileDto.getShard();
MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);
// 保存文件到本地
String fullPath = storePath + format;
if (!new File(fullPath).exists()){
new File(fullPath).mkdirs();
}
String path = new StringBuffer(fullPath)
.append(key.split("`")[1])
.append(".")
.append(suffix)
.toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
System.out.println(path);
String localPath = new StringBuffer(path)
.append(".")
.append(fileDto.getShardIndex())
.toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
System.out.println(localPath);
File dest = new File(localPath);
shard.transferTo(dest);
System.out.println(dest.getAbsolutePath());
LOG.info("保存文件記錄開始");
fileDto.setPath(path);
// 合并分片
if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
this.merge(fileDto);
}
// ResponseDto<Object> responseDto = new ResponseDto<>();
// responseDto.setContent("http://127.0.0.1:8080/f/"+ format + key + "-" + fileName);
// return responseDto;
return ReturnResult.buildSuccessResult("true");
}
public void merge(FileDto fileDto) throws Exception {
LOG.info("合并分片開始");
String path = fileDto.getPath(); //http://127.0.0.1:9000/file/f/course\6sfSqfOwzmik4A4icMYuUe.mp4
Integer shardTotal = fileDto.getShardTotal();
File newFile = new File(path);
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加寫入
FileInputStream fileInputStream = null;//分片文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
try {
for (int i = 0; i < shardTotal; i++) {
// 讀取第i個分片
fileInputStream = new FileInputStream(new File( path + "." + (i + 1))); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
}
} catch (IOException e) {
LOG.error("分片合并異常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
LOG.info("IO流關(guān)閉");
} catch (Exception e) {
LOG.error("IO流關(guān)閉", e);
}
}
LOG.info("合并分片結(jié)束");
// 刪除分片
LOG.info("刪除分片開始");
File file = null;
String filePath = "";
for (int i = 0; i < shardTotal; i++) {
filePath = path + "." + (i + 1);
file = new File(filePath);
System.gc();
boolean result = file.delete();
LOG.info("刪除{},{}", filePath, result ? "成功" : "失敗");
}
LOG.info("刪除分片結(jié)束");
}
@PostMapping("/unzip")
public ReturnResult unzip(String path){
return ReturnResult.buildSuccessResult(ZipUtil.unzip(path, "E:/usr/local/apps/pmxt-download/merge"));
}
@PostMapping("/selectBySize")
public ReturnResult selectBySize(String path) {
List<String> flist = FileViewer.getListFiles(path,true);
for (String s : flist) {
File file = new File(s);
if (file.isDirectory()) {
return ReturnResult.buildFailureResult("文件不存在");
} else {
if(file.length()>1048576*3){
ImgUtil.scale(
FileUtil.file(s),
FileUtil.file(s),
0.5f//縮放比例
);
// try {
// Thumbnails.of(s)
// .scale(0.5f)
// .outputQuality(0.5f)
// .toFile(s);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
}
}
}
return ReturnResult.buildSuccessResult(true);
}
}
工具類:64位編碼轉(zhuǎn)文件
package com.example.springboot.common;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.Base64;
public class Base64ToMultipartFile implements MultipartFile {
private final byte[] imgContent;
private final String header;
public Base64ToMultipartFile(byte[] imgContent, String header) {
this.imgContent = imgContent;
this.header = header.split(";")[0];
}
@Override
public String getName() {
// TODO - implementation depends on your requirements
return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
}
@Override
public String getOriginalFilename() {
// TODO - implementation depends on your requirements
return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
}
@Override
public String getContentType() {
// TODO - implementation depends on your requirements
return header.split(":")[1];
}
@Override
public boolean isEmpty() {
return imgContent == null || imgContent.length == 0;
}
@Override
public long getSize() {
return imgContent.length;
}
@Override
public byte[] getBytes() throws IOException {
return imgContent;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(imgContent);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
new FileOutputStream(dest).write(imgContent);
}
public static MultipartFile base64ToMultipart(String base64) {
System.out.println(base64);
String[] baseStrs = base64.split(",");
// Encoder decoder = Base64.getEncoder();
Base64.Decoder decoder = Base64.getDecoder();
byte[] b;
b = decoder.decode(baseStrs[1]);
for(int i = 0; i < b.length; ++i) {
if (b[i] < 0) {
b[i] += 256;
}
}
return new Base64ToMultipartFile(b, baseStrs[0]);
}
}
返回類(若返回為字符串或者已有返回類,則這個類沒用):
package com.example.springboot.common;
import lombok.Data;
@Data
public class ReturnResult {
// 狀態(tài)碼
private int code;
// 消息
private String msg;
// 數(shù)據(jù)
private Object data;
private ReturnResult(){
}
private static ReturnResult buildResult(int code, String msg, Object data) {
ReturnResult result = new ReturnResult();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static ReturnResult buildSuccessResult(String msg, Object data) {
return buildResult(200, msg, data);
}
public static ReturnResult buildSuccessResult(Object data) {
return buildSuccessResult("success", data);
}
public static ReturnResult buildFailureResult(int code, String msg, Object data) {
return buildResult(code, msg, data);
}
public static ReturnResult buildFailureResult(String msg, Object data) {
return buildFailureResult(500, msg, data);
}
public static ReturnResult buildFailureResult(String msg) {
return buildFailureResult(500, msg, null);
}
}
參考:https://windcoder.com/dawenjianfenpianshangchuanjavabanjiandanshixian
3. 壓縮與解壓縮
@PostMapping("/unzip")
public ReturnResult unzip(String path){
return ReturnResult.buildSuccessResult(ZipUtil.unzip(path, "E:/usr/local/apps/pmxt-download/merge"));
}
參考:http://hutool.cn/docs/index.html#/core/%E5%B7%A5%E5%85%B7%E7%B1%BB/%E5%8E%8B%E7%BC%A9%E5%B7%A5%E5%85%B7-ZipUtil
4. 圖片壓縮
這里我的實(shí)現(xiàn)主要有兩種:Google圖片處理工具與hutool工具,我都用了一下,發(fā)現(xiàn)前者將3700kb質(zhì)量比例雙壓縮后為1295kb,后者僅是比例壓縮就將3700kb壓縮為了1100kb,但是前者對圖片的處理方式更多,所以用那種,根據(jù)自身需求選擇
文件夾處理工具類:
package com.example.springboot.common;
import java.io.File;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class FileViewer {
public static List<String> getListFiles(String path, boolean isdepth) {
List<String> lstFileNames = new ArrayList<String>();
File file = new File(path);
return FileViewer.listFile(lstFileNames, file, "jpg", isdepth);
}
private static List<String> listFile(List<String> lstFileNames, File f,
String suffix, boolean isdepth) {
// 若是目錄, 采用遞歸的方法遍歷子目錄
if (f.isDirectory()) {
File[] t = f.listFiles();
for (int i = 0; i < t.length; i++) {
if (isdepth || t[i].isFile()) {
listFile(lstFileNames, t[i], suffix, isdepth);
}
}
} else {
String filePath = f.getAbsolutePath();
if (!suffix.equals("")) {
int begIndex = filePath.lastIndexOf("."); // 最后一個.(即后綴名前面的.)的索引
String tempsuffix = "";
if (begIndex != -1) {
tempsuffix = filePath.substring(begIndex + 1,
filePath.length());
if (tempsuffix.equals(suffix) || tempsuffix.equals("bmp") || tempsuffix.equals("jpeg") || tempsuffix.equals("png") || tempsuffix.equals("gif")) {
lstFileNames.add(filePath);
}
}
} else {
lstFileNames.add(filePath);
}
}
return lstFileNames;
}
// 遞歸取得文件夾(包括子目錄)中所有文件的大小
public static long getFileSize(File f) throws Exception// 取得文件夾大小
{
long size = 0;
File flist[] = f.listFiles();
for (int i = 0; i < flist.length; i++) {
if (flist[i].isDirectory()) {
size = size + getFileSize(flist[i]);
} else {
size = size + flist[i].length();
}
}
return size;
}
public static String FormetFileSize(long fileS) {// 轉(zhuǎn)換文件大小
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + "B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + "K";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + "M";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + "G";
}
return fileSizeString;
}
}
遍歷文件夾下所有圖片,過大則進(jìn)行壓縮:文章來源:http://www.zghlxwxcb.cn/news/detail-467569.html
@PostMapping("/selectBySize")
public ReturnResult selectBySize(String path) {
List<String> flist = FileViewer.getListFiles(path,true);
for (String s : flist) {
File file = new File(s);
if (file.isDirectory()) {
return ReturnResult.buildFailureResult("文件不存在");
} else {
if(file.length()>1048576*3){
ImgUtil.scale(
FileUtil.file(s),
FileUtil.file(s),
0.5f//縮放比例
);
// try {
// Thumbnails.of(s)
// .scale(0.5f)
// .outputQuality(0.5f)
// .toFile(s);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
}
}
}
return ReturnResult.buildSuccessResult(true);
}
參考:hutool圖片處理工具
Google圖片處理工具文章來源地址http://www.zghlxwxcb.cn/news/detail-467569.html
到了這里,關(guān)于SpringBoot+Vue實(shí)現(xiàn)大文件分塊上傳的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!