背景
最近突然接到了一個產品的需求,有點特別,在這里給大家分享一下,需求如下
- 提交表單,同時要上傳模型資源
- 模型文件是大文件,要顯示上傳進度,同時可以刪除
- 模型文件要上傳到服務器,表單數據同步到數據庫
- 同時要同步上傳后的模型地址到數據庫
- 后端使用Minio做文件管理
設計圖如下
一開始以為是一個簡單的表單上傳,發現並不是,這是大文件上傳啊,但很快又發現,不單單是上傳大文件,還有將文件信息關聯到表單。
基於這個奇葩的情況,我和后端兄弟商量了一下,決定使用如下方案
實現方案
分2步走
- 點擊上傳時,先提交表單信息到數據庫,接着后端返回一個表單的id給我
- 當所有文件上傳完成后,再調用另外一個服務,將上傳完成后的地址和表單id發送給后端
如此,便完成了上面的需求
了解一下Mino
這里大家先了解一下Minio的js SDK文檔
里面有2個很重要的接口,今天要用到
一個是給文件生成用於put方法上傳的地址 |
---|
![]() |
一個是獲取已經上傳完成后的文件的get下載地址 |
---|
實現步驟
這里是使用原生的 ajax請求進行上傳的,至於為什么,后面會有說到
1.創建存儲桶
創建一個Minio上傳實例
var Minio = require('minio') this.minioClient = new Minio.Client({ endPoint: '192.168.172.162', //后端提供 port: 9000, //端口號默認9000 useSSL: true, accessKey: 'Q3AM3UQ867SPQQA43P2F', //后端提供 secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' }); this.userBucket = 'yourBucketName' //這里后端需要提供給你,一個存儲桶名字 復制代碼
2.選擇文件
這里使用input標簽選擇文件,點擊選擇文件的時候,調用一下input的click方法,就可以打開本地文件夾
<el-form-item label="資源文件"> <el-button style="marginRight:10px;" @click="selectFile()" size="mini" >選擇文件</el-button> <input :accept="acceptFileType" multiple="multiple" type="file" id="uploadInput" ref="uploadInput" v-show="false" @change="getAndFormatFile()" > <i class="tip">僅支持.gbl、.gltf、.fbx、.obj、.mtl、.hdr、.png、.jpg格式的文件</i> <i class="tip">單個文件的大小限制為128MB</i> </el-form-item> 復制代碼
selectFile() { let inputDOM = this.$refs.uploadInput inputDOM.click(); }, 復制代碼
接着就是對文件進行格式化
//格式化文件並創建上傳隊列 getAndFormatFile(){ let files = this.$refs.uploadInput.files const userBucket = this.userBucket if(files.length > 6) { this.$message({ message: `最大只能上傳6個文件`, type: 'warning' }) return } files.forEach((file, index) => { if ((file.size / 1024 / 1024).toFixed(2) > 128) { //單個文件限制大小為128MB this.$message({ message: `文件大小不能超過128MB`, type: 'warning' }) return } //創建文件的put方法的url this.minioClient.presignedPutObject(userBucket, file.name, 24 * 60 * 60, (err, presignedUrl) => { if (err) { this.$message({ message: `服務器連接超時`, type: 'error' }) return err } let fileIcon = this.getFileIcon(file) let fileUploadProgress = '0%' //文件上傳進度 this.fileInfoList.push({ file, //文件 fileIcon, //文件對應的圖標 className fileUploadProgress, //文件上傳進度 filePutUrl: presignedUrl, //文件上傳put方法的url fileGetUrl: '', //文件下載的url }) }) }) this.fileList = [...this.fileInfoList] }, 復制代碼
3.創建上傳隊列
這里定義了一個創建文件上傳請求的方法,使用原生的XMLHttpRequest
,它接受以下參數
file
:要上傳的文件filePutUrl
:文件上傳的put方法地址customHeader
: 自定義的頭信息onUploadProgress
:文件上傳的進度監聽函數onUploaded
:文件上傳完成的監聽函數onError
:文件上傳出錯的監聽函數
//創建上傳文件的http createUploadHttp(config){ const {file, filePutUrl, customHeader, onUploadProgress, onUploaded, onError} = config let fileName = file.name let http = new XMLHttpRequest(); http.upload.addEventListener("progress", (e) => { //監聽http的進度。並執行進度監聽函數 onUploadProgress({ progressEvent: e, uploadingFile: file }) }, false) http.onload = () => { if (http.status === 200 && http.status < 300 || http.status === 304) { try { //監聽http的完成事件,並執行上傳完成的監聽函數 const result = http.responseURL onUploaded({ result, uploadedFile: file}) } catch(error) { //監聽錯誤 onError({ error, errorFile: file}) } } } http.open("PUT", filePutUrl, true); //加入頭信息 Object.keys(customHeader).forEach((key, index) =>{ http.setRequestHeader(key, customHeader[key]) }) http.send(file); return http //返回該http實例 } 復制代碼
4.開始上傳
//上傳文件到存儲桶 async handleUplaod(){ let _this = this if(this.fileInfoList.length < 1) { this.$message({ message: `請先選擇文件`, type: 'warning' }) return } //先上傳文件的基本表單信息,獲取表單信息的id try{ const {remark, alias} = _this.uploadFormData let res = await uploadModelSourceInfo({remark, serviceName: alias}) _this.modelSourceInfoId = res.message }catch(error){ if(error) { _this.$message({ message: `上傳失敗,請檢查服務`, type: 'error' }) return } } //開始將模型資源上傳到遠程的存儲桶 this.fileList.forEach((item, index) => { const {file, filePutUrl} = item let config = { file, filePutUrl, customHeader:{ "X-FILENAME": encodeURIComponent(file.name), "X-Access-Token": getToken() }, onUploadProgress: ({progressEvent, uploadingFile}) => { let progress = (progressEvent.loaded / progressEvent.total).toFixed(2) this.updateFileUploadProgress(uploadingFile, progress) }, onUploaded: ({result, uploadedFile}) => { this.updateFileDownloadUrl(uploadedFile) }, onError: ({error, errorFile}) => { } } let httpInstance = this.createUploadHttp(config) //創建http請求實例 this.httpQueue.push(httpInstance) //將http請求保存到隊列中 }) }, //更新對應文件的上傳進度 updateFileUploadProgress(uploadingFile, progress) { this.fileInfoList.forEach((item, index) => { if(item.file.name === uploadingFile.name){ item.fileUploadProgress = (Number(progress)*100).toFixed(2) + '%' } }) }, //更新上傳完成文件的下載地址 updateFileDownloadUrl(uploadedFile){ const userBucket = this.userBucket this.fileInfoList.forEach((item, index) => { if(item.file.name === uploadedFile.name){ this.minioClient.presignedGetObject(userBucket, uploadedFile.name, 24*60*60, (err, presignedUrl) => { if (err) return console.log(err) item.fileGetUrl = presignedUrl }) } }) }, 復制代碼
5 上傳完成后,同步文件地址給后端
在watch里監聽文件列表,當所有的文件進度都是100%時,表示上傳完成,接着就可以同步文件信息
watch:{ fileInfoList: { handler(val){ //1.3所有文件都上傳到存儲桶后,將上傳完成后的文件地址、文件名字同步后端 if(val.length < 1) return let allFileHasUpload = val.every((item, index) => { return item.fileGetUrl.length > 1 }) if(allFileHasUpload) { this.allFileHasUpload = allFileHasUpload const {modelSourceInfoId} = this if(modelSourceInfoId.length < 1) { return } const url = process.env.VUE_APP_BASE_API + "/vector-map/threeDimensionalModelService/invokeMapService" const files = val.map((ite, idx) => { return { fileName: ite.file.name, fileUrl: ite.fileGetUrl } }) this.syncAllUploadedFile(url, files, modelSourceInfoId) } }, deep: true } }, //同步已上傳的文件到后端 syncAllUploadedFile(url, files, modelSourceInfoId){ let xhr = new XMLHttpRequest() xhr.onload = () => { if (xhr.status === 200 && xhr.status < 300 || xhr.status === 304) { try { const res = JSON.parse(xhr.responseText) if(res && res.code === 200){ this.$message({ message: '上傳完成', type: 'success' }) this.$emit('close') this.fileInfoList = [] this.fileList = [] this.httpQueue = [] } } catch(error) { this.$message({ message: '上傳失敗,請檢查服務', type: 'error' }) } } } xhr.open("post", url, true) xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('X-Access-Token', getToken()) //將前面1.1獲取文件信息的id作為頭信息傳遞到后端 xhr.setRequestHeader('ThreeDimensionalModel-ServiceID', modelSourceInfoId) xhr.send(JSON.stringify(files)) }, 復制代碼
6.刪除文件
刪除文件時要注意
- 刪除本地的文件緩存
- 刪除存儲桶里面的文件
- 停止當前文件對應的http請求
//刪除文件,並取消正在文件的上傳 deleteFile(fileInfo, index){ this.httpQueue[index] && this.httpQueue[index].abort() this.httpQueue[index] && this.httpQueue.splice(index, 1) this.fileInfoList.splice(index, 1) this.fileList.splice(index, 1) this.removeRemoteFile(fileInfo) }, //清空文件並取消上傳隊列 clearFile() { this.fileInfoList.forEach((item, index) => { this.httpQueue[index] && this.httpQueue[index].abort() this.httpQueue[index] && this.httpQueue.splice(index, 1) this.removeRemoteFile(item) }) this.fileInfoList = [] this.httpQueue = [] this.fileList = [] }, //刪除遠程文件 removeRemoteFile(fileInfo){ const userBucket = this.userBucket const { fileUploadProgress, file} = fileInfo const fileName = file.name const complete = fileUploadProgress === '100.00%' ? true : false if(complete){ this. minioClient.removeObject(userBucket, fileName, function(err) { if (err) { return console.log('Unable to remove object', err) } console.log('Removed the object') }) }else{ this.minioClient.removeIncompleteUpload(userBucket, fileName, function(err) { if (err) { return console.log('Unable to remove incomplete object', err) } console.log('Incomplete object removed successfully.') }) } }, 復制代碼
完整代碼
這里的完整代碼是我直接從工程里拷貝出來的,里面用到了一些自己封裝的服務和方法 比如 后端的接口、AES解密、獲取Token、表單驗證等
import{uploadModelSourceInfo, uploadModelSource, getMinioConfig} from '@/api/map' import AES from '@/utils/AES.js' import { getToken } from '@/utils/auth' import * as myValiDate from "@/utils/formValidate"; 復制代碼
/** * 文件說明 * @Author: zhuds * @Description: 模型資源上傳彈窗 分為3個步驟 1.先將文件的基本表單信息上傳給后端,獲取文件信息的ID 2.然后將文件上傳存儲桶 3.等所有文件都上傳完成后,再將上傳完成后的文件信息傳遞給后端,注意,此時的請求頭要戴上第1步獲取的文件信息id * @Date: 2/28/2022, 1:13:20 PM * @LastEditDate: 2/28/2022, 1:13:20 PM * @LastEditor: */ <template> <div class="upload-model"> <el-dialog :visible.sync="isVisible" @close="close()" :show-close ="false" :close-on-click-modal="false" top="10vh" v-if="isVisible" :destroy-on-close="true" > <div slot="title" class="header-title"> <div class="icon"></div> <span>上傳模型資源</span> <i class="el-icon-close" @click="close()"></i> </div> <el-form :label-position="labelPosition" label-width="80px" :model="uploadFormData" ref="form" :rules="rules" > <el-form-item label="別名"> <el-input size="small" v-model="uploadFormData.alias"></el-input> </el-form-item> <el-form-item label="備注"> <el-input type="textarea" v-model="uploadFormData.remark" size="small"></el-input> </el-form-item> <el-form-item label="資源文件"> <el-button style="marginRight:10px;" @click="selectFile()" size="mini" >選擇文件</el-button> <input :accept="acceptFileType" multiple="multiple" type="file" id="uploadInput" ref="uploadInput" v-show="false" @change="getAndFormatFile()" > <i class="tip">僅支持.gbl、.gltf、.fbx、.obj、.mtl、.hdr、.png、.jpg格式的文件</i> <i class="tip">單個文件的大小限制為128MB</i> </el-form-item> </el-form> <div class="file-list" v-show="fileInfoList.length > 0"> <div class="file-item" v-for="(item, index) in fileInfoList" :key="index"> <div class="icon"></div> <div class="name">{{item.file.name}}</div> <div class="size">{{(item.file.size/1024/1024).toFixed(2)}}MB </div> <div class="progress"> <div class="bar" :style="{width: item.fileUploadProgress}"></div> </div> <div class="rate">{{item.fileUploadProgress}}</div> <div class="delete-btn" @click="deleteFile(item, index)">x</div> </div> </div> <div class="custom-footer"> <button class="info" @click="close()">取 消</button> <button class="success" @click="handleUplaod()">上傳</button> </div> </el-dialog> </div> </template> <script> import{uploadModelSourceInfo, uploadModelSource, getMinioConfig} from '@/api/map' import AES from '@/utils/AES.js' import { getToken } from