springboot+vue實(shí)現(xiàn)阿里云oss大文件分片上傳的示例代碼
一、前言
上一篇《使用springboot+vue實(shí)現(xiàn)阿里云oss上傳》寫(xiě)了如何使用springboot+vue實(shí)現(xiàn)阿里云oss文件上傳。這種方式雖然通用,但有個(gè)弊端就是當(dāng)上傳大文件時(shí),容易導(dǎo)致文件還未上傳完頁(yè)面請(qǐng)求就超時(shí)了,如果想把oss的文件路徑保存到數(shù)據(jù)庫(kù)則無(wú)法實(shí)現(xiàn)。
針對(duì)這個(gè)阿里云也推出了直傳的方式。這個(gè)方式避免文件傳輸?shù)胶笈_(tái)再轉(zhuǎn)存到oss,大大縮減了上傳時(shí)間,并且支持分片上傳方式,對(duì)于大文件處理非常高效。
其整個(gè)流程是:前端通過(guò)后臺(tái)請(qǐng)求OSS policy
簽名,前端拿到簽名后直接把文件上傳到阿里云oss,上傳成功后OSS通過(guò)回調(diào)把上傳結(jié)果返回給后端,下面我們來(lái)看具體實(shí)現(xiàn)。
二、前端實(shí)現(xiàn)邏輯
我們通過(guò)對(duì)上一篇的上傳組件進(jìn)行一個(gè)改造。OssFileUpload
組件
<template> <div class="upload-file"> <el-upload :multiple="multiple" :accept="accept.join(',')" action="#" :http-request="handleUpload" :before-upload="handleBeforeUpload" :file-list="fileList" :limit="limit" :on-exceed="handleExceed" :show-file-list="false" :data="data" class="upload-file-uploader" ref="fileUpload" > <!-- 上傳按鈕 --> <el-button size="mini" type="primary">選取文件</el-button> <template v-if="multiple"> (按住Ctrl鍵多選)</template> <!-- 上傳提示 --> <div class="el-upload__tip" slot="tip" v-if="showTip && fileList.length<=0"> 請(qǐng)上傳 <template v-if="limit"> 最多 <b style="color: #f56c6c">{{ limit }}個(gè)</b></template> <template v-if="fileSize"> 大小不超過(guò) <b style="color: #f56c6c">{{ fileSize }}MB</b></template> <template v-if="fileType"> 格式為 <b style="color: #f56c6c">{{ fileType.join("/") }}</b></template> 的文件 </div> </el-upload> <!-- 文件列表 --> <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul" height="485"> <li v-for="(file, index) in fileList" :key="file.uid || index " class="el-upload-list__item ele-upload-list__item-content"> <el-link :href="file.url" rel="external nofollow" :underline="false" target="_blank"> <span class="el-icon-document"> {{ file.name }} </span> </el-link> <div class="ele-upload-list__item-content-action"> <el-link :underline="false" @click="handleDelete(index)" type="danger" style="width: 50px">刪除</el-link> </div> </li> </transition-group> </div> </template> <script> import {handleMD5} from '@/utils/md5'; import {getOssPolicy} from "@/api/file"; import {POST} from "@/utils/request"; export default { name: "OssFileUpload", props: { // 是否可多選 multiple: { type: Boolean, default: true, }, // 值 value: [String, Object, Array], // 數(shù)量限制 limit: { type: Number, default: 5, }, // 大小限制(MB) fileSize: { type: Number, default: 5, }, // 文件類型, 例如['png', 'jpg', 'jpeg'] fileType: { type: Array, default: () => ["doc", "xls", "xlsx", "ppt", "txt", "pdf"], }, accept: { type: Array, default: () => [".doc", ".xls", ".xlsx", ".ppt", ".txt", ".pdf"], }, // 是否顯示提示 isShowTip: { type: Boolean, default: true }, // 是否重命名 rename: { type: Boolean, default: false } }, data() { return { number: 0, uploadList: [], fileList: [], data: {} }; }, watch: { value: { handler(val) { if (val) { let temp = 1; // 首先將值轉(zhuǎn)為數(shù)組 const list = Array.isArray(val) ? val : this.value.split(','); // 然后將數(shù)組轉(zhuǎn)為對(duì)象數(shù)組 this.fileList = list.map(item => { if (typeof item === "string") { item = {name: item.name, url: item.url}; } item.uid = item.uid || new Date().getTime() + temp++; return item; }); } else { this.fileList = []; return []; } }, deep: true, immediate: true }, rename: { handler(val) { this.data = {rename: val} }, deep: true, immediate: true } }, computed: { // 是否顯示提示 showTip() { return this.isShowTip && (this.fileType || this.fileSize); }, }, methods: { // 上傳前校檢格式和大小 handleBeforeUpload(file) { // 校檢文件類型 if (this.fileType) { const fileName = file.name.split('.'); const fileExt = fileName[fileName.length - 1]; const isTypeOk = this.fileType.indexOf(fileExt) >= 0; if (!isTypeOk) { this.$modal.msgError(`文件格式不正確, 請(qǐng)上傳${this.fileType.join("/")}格式文件!`); return false; } } // 校檢文件大小 if (this.fileSize) { const isLt = file.size / 1024 / 1024 < this.fileSize; if (!isLt) { this.$modal.msgError(`上傳文件大小不能超過(guò) ${this.fileSize} MB!`); return false; } } this.$modal.loading("正在上傳文件,請(qǐng)稍候..."); this.number++; return true; }, // 文件個(gè)數(shù)超出 handleExceed() { this.$modal.msgError(`上傳文件數(shù)量不能超過(guò) ${this.limit} 個(gè)!`); }, // 刪除文件 handleDelete(index) { this.fileList.splice(index, 1); this.$emit("input", this.listToString(this.fileList)); }, // 上傳結(jié)束處理 uploadedSuccessfully() { if (this.number > 0 && this.uploadList.length === this.number) { this.fileList = this.fileList.concat(this.uploadList); this.uploadList = []; this.number = 0; this.$emit("input", this.listToString(this.fileList)); this.$modal.closeLoading(); } }, /** 上傳按鈕操作 */ handleUpload (file) { // 計(jì)算文件的MD5,根據(jù)文件大小進(jìn)行分片處理 handleMD5(file.file).then(md5 => { const data = { fileName: file.file.name, md5: md5, rename: this.rename }; getOssPolicy(data).then((response) => { let data = response.data let formData = new FormData(); formData.append('OSSAccessKeyId', data.accessKeyId); formData.append('signature', data.signature); formData.append('policy', data.policy); formData.append('key',data.filePath); formData.append('callback',data.callback); formData.append('success_action_status', 200); formData.append('file',file.file); POST(data.host, formData).then((res) => { let data = res.data if (data.code !== 0) { this.$modal.msgError(data.msg); return; } this.uploadList.push({name: data.data.filename, url: data.data.url}); this.uploadedSuccessfully(); this.$modal.msgSuccess("上傳成功"); }); }); }); }, // 對(duì)象轉(zhuǎn)成指定字符串分隔 listToString(list, separator) { let strs = ""; separator = separator || ","; for (let i in list) { strs += list[i].url + separator; } return strs != '' ? strs.substr(0, strs.length - 1) : ''; } } }; </script> <style scoped lang="scss"> .upload-file-uploader { margin-bottom: 5px; } .upload-file-list { max-height: 420px; overflow-y: auto; } .upload-file-list .el-upload-list__item { border: 1px solid #e4e7ed; line-height: 2; margin-bottom: 10px; position: relative; } .upload-file-list .ele-upload-list__item-content { display: flex; justify-content: space-between; align-items: center; color: inherit; } .ele-upload-list__item-content-action .el-link { margin-right: 10px; } </style>
這里使用的是spark-md5
來(lái)計(jì)算文件MD5,測(cè)試下來(lái)比crypto-js
要快不少。
import SparkMD5 from 'spark-md5' // 計(jì)算md5 export function handleMD5(file) { return new Promise((resolve, reject) => { let start = new Date().getTime() const slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice const fileReader = new FileReader() //分片大小 const chunkSize = 2097152 //計(jì)算分片數(shù) const chunks = Math.ceil(file.size / chunkSize) let currentChunk = 0 //注意點(diǎn),網(wǎng)上都是 這一步都是有問(wèn)題的, SparkMD5.ArrayBuffer() const spark = new SparkMD5.ArrayBuffer() fileReader.onload = function (e) { spark.append(e.target.result) currentChunk++ if (currentChunk < chunks) { loadNext() } else { //計(jì)算后的結(jié)果 const md5 = spark.end(false) console.info('md5:' + md5) console.info("take:" + (new Date().getTime() - start) + "ms") resolve(md5) } } fileReader.onerror = function () { console.warn('FileReader error.') reject('compute file md5 error') } const loadNext = () => { const start = currentChunk * chunkSize const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize // 注意這里的 fileRaw fileReader.readAsArrayBuffer(slice.call(file, start, end)) } loadNext() }) }
POST
的定義,使用axios
export const POST = async (url, form) => { try { return await axios.post(`${url}`, form); } catch (error) { return error; } };
使用示例
<template> <div class="app-container"> <el-dialog title="文件上傳" :visible.sync="open" width="700px" append-to-body> <el-form ref="form" :model="form" :rules="rules" label-width="120px"> <el-form-item label="文件標(biāo)識(shí)" prop="code"> <el-input v-model="form.code" clearable maxlength="50" show-word-limit style="width: 250px"/> </el-form-item> <el-form-item label="是否重命名文件" prop="rename"> <el-radio-group v-model="rename"> <el-radio :label="false">否</el-radio> <el-radio :label="true">是</el-radio> </el-radio-group> </el-form-item> <el-form-item label="選擇文件" prop="file" class="is-required"> <!--只能上傳1個(gè)最大100M的視頻文件--> <oss-file-upload ref="upload" :fileType="fileType" :accept="accept" :fileSize="100" :limit="1" :multiple="false" :value="form.files" :rename="rename"/> </el-form-item> <el-form-item label="備注" prop="remark"> <el-input type="textarea" :rows="3" v-model="form.remark" clearable maxlength="500" show-word-limit/> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="submitForm">確 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </el-dialog> </div> </template> <script> import { add } from "@/api/xxx"; // 請(qǐng)求后臺(tái)的接口封裝 import OssFileUpload from "@/components/OssFileUpload"; export default { name: "demo", components: { OssFileUpload }, data() { return { // 自定義允許上傳的文件格式,也可以使用組件里面定義的默認(rèn)格式 fileType: ['avi', 'mp4', 'mov', 'wmv', 'flv', 'mpeg', 'webm'], accept: ['.avi', '.mp4', '.mov', '.wmv', '.flv', '.mpeg', '.webm'], // 是否重命名 rename: false, // 表單參數(shù) form: { code: undefined, remark: undefined, url: undefined, files: [] }, // 表單校驗(yàn) rules: { code: [{required: true, message: "文件標(biāo)識(shí)不能為空", trigger: "blur"}], files: [{type: 'array', required: true, message: "文件不能為空", trigger: "blur"}] } } }, methods: { /** 提交按鈕 */ submitForm: function () { this.$refs["form"].validate(valid => { if (valid) { this.form.files = this.$refs.upload.fileList if (!Array.isArray(this.form.files) || this.form.files.length <= 0) { this.$alert("請(qǐng)上傳附件"); return false; } console.info(this.form) add(this.form).then(() => { this.$modal.msgSuccess("操作成功"); this.open = false; }); } }); }, ... } } </script> <style lang="scss" scoped> ::v-deep { .el-dialog__body { padding: 10px 10px 20px 10px; } } </style>
效果如下:
上傳成功后再上傳其他文件時(shí)效果
三、后端實(shí)現(xiàn)邏輯
controller
/** * 生成OSS policy * * @param request 請(qǐng)求 * @return 結(jié)果 */ @PostMapping("/policy") public Result<OssPolicyInfo> generateOssPolicy(@RequestBody @Valid GetOssPolicyRequest request) { OssPolicyInfo ossPolicyInfo = ossService.getPolicy(request); return Result.success(ossPolicyInfo); } /** * 文件上傳到OSS的回調(diào)處理 * * @param servletRequest 回調(diào)request * @return 驗(yàn)證結(jié)果 */ @PostMapping("/oss/callback") public Result callback(HttpServletRequest servletRequest) { //servletRequest -> map Map<String, String> callbackMap = HttpRequestUtil.commonHttpRequestParamConvert(servletRequest); String filename = callbackMap.get("filename"); String name = filename.substring(filename.lastIndexOf("/") + 1); //文件訪問(wèn)地址 String url = ExternalUrl.OSS_HOST + "/" + filename; callbackMap.put("url", url); callbackMap.put("filename", name); return Result.success(callbackMap); }
service
@Resource private AliyunOssConfig config; private OSSClient getOSSClient() { return (OSSClient) new OSSClientBuilder().build(config.getEndPoint(), config.getKeyId(), config.getSecret()); } @Override public OssPolicyInfo getPolicy(GetOssPolicyRequest request) { OSSClient ossClient = getOSSClient(); try { Long fileId = new SnowFlakeGenerator().nextId(); String fileName = request.getFileName(); if (request.getRename()) { // 重命名 fileName = fileId + "." + FilenameUtils.getExtension(fileName); } String filePath = String.format("file/%s", fileName); // 30s內(nèi)上傳有效 long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); // PostObject請(qǐng)求最大可支持的文件大小為1 GB,即CONTENT_LENGTH_RANGE為1024*1024*1024。 PolicyConditions policyConditions = new PolicyConditions(); policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1073741824); policyConditions.addConditionItem(MatchMode.Exact, PolicyConditions.COND_KEY, filePath); String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions); byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); OssPolicyInfo ossPolicyInfo = new OssPolicyInfo() .setAccessKeyId(config.getKeyId()) .setPolicy(encodedPolicy) .setSignature(postSignature) .setFilePath(filePath) .setHost(ExternalUrl.OSS_HOST) //OSS地址 .setExpire(expireEndTime / 1000); //如果需要回調(diào),默認(rèn)為true if (request.getNeedCallback()) { String callbackUrl = "/oss/callback"; String callbackBody = String.format("filename=${object}&size=${size}&md5=%s", request.getMd5()); JSONObject callback = new JSONObject(); callback.put("callbackUrl", callbackUrl); callback.put("callbackBody", callbackBody); callback.put("callbackBodyType", "application/x-www-form-urlencoded"); String encodedCallbackBody = BinaryUtil.toBase64String(callback.toString().getBytes()); ossPolicyInfo.setCallback(encodedCallbackBody); } return ossPolicyInfo; } catch (Exception e) { log.error("Oss generate post policy fail"); throw new ServerErrorException("服務(wù)器錯(cuò)誤"); } finally { ossClient.shutdown(); } }
AliyunOssConfig
@Data @Configuration @ConfigurationProperties(prefix = "aliyun.oss") public class AliyunOssConfig { private String endPoint; private String keyId; private String secret; private String bucketName; }
GetOssPolicyRequest
@Data public class GetOssPolicyRequest { @NotBlank private String fileName; private String md5; private Boolean needCallback = true; private Boolean rename = false; }
值得注意的是/oss/callback
這個(gè)地址需要使用公網(wǎng),阿里云能回調(diào)成功才行。
到此這篇關(guān)于springboot+vue實(shí)現(xiàn)阿里云oss大文件分片上傳的示例代碼的文章就介紹到這了,更多相關(guān)springboot vue oss大文件分片上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 使用Springboot+Vue實(shí)現(xiàn)文件上傳和下載功能
- 基于SpringBoot和Vue實(shí)現(xiàn)頭像上傳與回顯功能
- Vue?+?SpringBoot?實(shí)現(xiàn)文件的斷點(diǎn)上傳、秒傳存儲(chǔ)到Minio的操作方法
- Java實(shí)現(xiàn)大文件的分片上傳與下載(springboot+vue3)
- springboot整合vue2-uploader實(shí)現(xiàn)文件分片上傳、秒傳、斷點(diǎn)續(xù)傳功能
- 利用Springboot+vue實(shí)現(xiàn)圖片上傳至數(shù)據(jù)庫(kù)并顯示的全過(guò)程
- Vue+Element+Springboot圖片上傳的實(shí)現(xiàn)示例
- Springboot+Vue-Cropper實(shí)現(xiàn)頭像剪切上傳效果
- springboot + vue+elementUI實(shí)現(xiàn)圖片上傳功能
相關(guān)文章
mybatis-spring:@MapperScan注解的使用
這篇文章主要介紹了mybatis-spring:@MapperScan注解的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09在Spring Boot中使用Spark Streaming進(jìn)行實(shí)時(shí)數(shù)據(jù)處理和流式計(jì)算的步驟
這篇文章主要介紹了在Spring Boot中使用Spark Streaming進(jìn)行實(shí)時(shí)數(shù)據(jù)處理和流式計(jì)算,通過(guò)本文的介紹,我們了解了在Spring Boot中使用Spark Streaming進(jìn)行實(shí)時(shí)數(shù)據(jù)處理和流式計(jì)算的詳細(xì)步驟,需要的朋友可以參考下2024-03-03JavaWeb實(shí)現(xiàn)簡(jiǎn)單的自動(dòng)登錄功能
這篇文章主要為大家詳細(xì)介紹了JavaWeb實(shí)現(xiàn)簡(jiǎn)單的自動(dòng)登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08