基于Element-Plus ElUpload组件,实现前端直接上传文件到阿里云OSS服务
2022年4月14日
需求背景
上传文件需要直接通过客户端上传到阿里云oss服务,无需通过服务器中转,减少服务器压力
交互流程示意图

需要开发的内容
- 客户端调用接口从自有的服务端当中获取上传所需要的参数(在有效期内可以重复使用同一套参数值,可以在本地做缓存)
- 自有服务端通过accessKey\Secret值生成前端需要的上传参数(需要设定有效期,超过有效期后参数无效)
- 客户端获取到上传参数后将参数与上传的文件一并提交到阿里云OSS服务器(请求路径由上传参数决定)
1.服务端核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/** * 获取用于客户端直传所需要的参数 * * @return 客户端直传需要的参数 */ @Override public OssClientUploadDto getClientUploadParams(String dir) { try { dir = StrUtil.removePrefix(dir, "/"); String endPoint = StrUtil.removePrefix(properties.getEndpoint(), "https://"); String host = "https://" + properties.getBucket() + "." + endPoint; //默认1小时 long expireTime = 60; long expireEndTime = System.currentTimeMillis() + expireTime * 60 * 1000; Date expiration = new Date(expireEndTime); PolicyConditions cond = new PolicyConditions(); cond.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); String postPolicy = getClient().generatePostPolicy(expiration, cond); byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = getClient().calculatePostSignature(postPolicy); OssClientUploadDto dto = new OssClientUploadDto(); dto.setAccessid(properties.getAccessKey()); dto.setPolicy(encodedPolicy); dto.setSignature(postSignature); dto.setDir(dir); dto.setHost(host); dto.setExpire(String.valueOf(expireEndTime / 1000)); return dto; } catch (Exception e) { log.error(e.getMessage(), e); throw new BusinessException(e.getMessage(), e); } } |
1 2 3 4 5 6 7 8 9 10 11 |
@Getter @Setter public class OssClientUploadDto { private String accessid; private String host; private String policy; private String signature; private String expire; private String dir; } |
还需要自行写一个Controller接受客户端请求后调用上面的方法返回给客户端
2.客户端
此处的客户端代码使用Element-Plus库当中的ElUpload组件为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<el-upload ref="refUpload" :action="conf.action" method="post" name="file" :data="uploadData" :accept="accept" :list-type="listType" :limit="limit" auto-upload :file-list="uploadFiles" :on-success="handleUploadSuccess" :on-preview="handlePictureCardPreview" :on-exceed="handleUploadExceed" :on-remove="handleRemove" :on-error="handleError" :before-upload="handleBeforeUpload" v-bind="$attrs" v-if="showUpload" > <template #default v-if="listType==='picture-card'"> <el-icon> <el-icon-plus/> </el-icon> </template> <template #default v-else> <el-tooltip placement="top" :content="tooltip"> <el-button type="primary" icon="el-icon-plus">{{ uploadButtonText }}</el-button> </el-tooltip> </template> <template #file="{file}" v-if="listType==='picture-card'"> <slot v-if="isImage(file.url)" name="file" :file="file"></slot> </template> </el-upload> |
页面初始化的时候需要调用后台接口获取上传参数,只有上传参数获取成功才渲染上传组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<script setup> import {getClientUploadParams,checkExpire,getRandFileName} from "../oss"; // ... 其余代码自行补充 //标识是否显示上传按钮,针对客户端直传需要等待获取参数后再渲染上传组件,防止data属性无法透传 const showUpload = ref(false) const conf = reactive({ action: '' }) getClientUploadParams().then((uploadParams:any)=>{ uploadData.value = { 'key':'', 'dir': uploadParams['dir'], 'policy': uploadParams['policy'], 'OSSAccessKeyId': uploadParams["accessid"], 'success_action_status' : '200', 'signature': uploadParams["signature"], }; //文件上传路径 conf.action = uploadParams["host"] showUpload.value = true; }); </script> |
选择文件时需要再次校验一下上传参数是否在有效期内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<script setup> import {getClientUploadParams,checkExpire,getRandFileName} from "../oss"; // ... 其余代码自行补充 //文件上传额外参数 const uploadData = ref({ key:'', dir:'', policy:'', OSSAccessKeyId:'', success_action_status:'', signature:'', }) const handleBeforeUpload = async ({type, size,name}: UploadRawFile) => { const mb = size / 1024 / 1024 if (type.startsWith("video") && mb > 500) { $message.error(`上传的视频文件不能大于500M`); return false; } if (type.startsWith("image") && mb > props.maxSize) { $message.error(`上传的图片文件不能大于${props.maxSize}M`) return false } //页面直传方式 //增加参数有效时间校验 if (!checkExpire()) { $message.error(`当前文件上传状态已过期,请重新选择文件`) //重新触发 getClientUploadParams() return false; } //更新上传的文件名 uploadData.value.key = uploadData.value["dir"] + getRandFileName(name) } </script> |
封装的几个js方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
//基于axios封装的接口调用方法,可自行实现 import {request} from 'ueview/utils/http' import dayjs from "dayjs"; const service = '/system/oss' const cachedKey = "_ue_client_up_params" /** * 获取上传参数,默认会使用在指定有效期内不会发起多次请求 */ export async function getClientUploadParams() { if (!checkExpire()) { return await getParamsFromRemote() } else { return JSON.parse(window.sessionStorage.getItem(cachedKey)); } } /** * 检测上传参数是否过期 * @returns {boolean} */ export function checkExpire() { let cached = window.sessionStorage.getItem(cachedKey) || undefined if (!cached) { return false; } cached = JSON.parse(cached); let expire = cached.expire // 可以判断当前expire是否超过了当前时间, 如果超过了当前时间, 就重新取一下,5s 作为缓冲。 let now = Date.parse(new Date()) / 1000; return Number(expire) > now + 5 } export async function getParamsFromRemote() { const now = dayjs().format('YYYYMMDD') const dir = "tmp/" + now + "/" const result = await request({ url: `${service}/getClientUploadParams?dir=${dir}`, method: 'get', }) window.sessionStorage.setItem(cachedKey, JSON.stringify(result)) return result; } /** * 获取随机字符串文件名 * @param fileName * @returns {string} */ export function getRandFileName(fileName) { let suffix = getSuffix(fileName) return randStr(10) + suffix } function randStr(len) { len = len || 32; let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; let maxPos = chars.length; let pwd = ''; for (let i = 0; i < len; i++) { pwd += chars.charAt(Math.floor(Math.random() * maxPos)); } return pwd; } function getSuffix(filename) { let pos = filename.lastIndexOf('.') let suffix = '' if (pos != -1) { suffix = filename.substring(pos) } return suffix; } |
获取文件上传路径
默认阿里云OSS上传的文件不会返回任何内容,只会返回请求状态为200。如果需要获取上传的文件全路径,可以根据上传参数拼装获取
1 2 3 4 5 6 7 |
//文件上传成功回调 const handleUploadSuccess = (res: any, file: UploadFile, files: UploadFiles) => { //上传后不会返回地址,需要前端拼接 let url = conf.action+"/"+uploadData.value.key } |