“取一杯天上的水,照着明月人世间晃呀晃” 手机电话来了,一个陌生的本地电话号码,我接了电话。
“小飞吗?”
“是的”
“我是xx项目的曹总,管理端的商品图片上传有点慢,可以优化吗?”
“曹总,好的,我这边帮您处理下。” 挂了电话后,我赶紧拿出了我的电脑。
这个项目是公司新接的一个电商项目,图片上传的功能是一般是用的比较多的。
“存储图片用的是阿里云,上传速度应该不会这么慢,是不是曹总那边带宽问题?” 我边想边打开 Google
浏览器,输入管理端登录地址,登录管理员账号。
打开商品管理,新增商品,上传一张小图片,秒传,上传一张几M的就有点慢。那应该不是带宽问题。我这边带宽是百兆带宽。有人会问,“你不是流量限速了吗?怎么会有百兆带宽",因为我现在在公司加班(蹭流量)。
排除了是本地带宽问题,按键盘 F12 ,打开浏览器调试模式,点击上传,发现只调用了一个接口,还是请求后台的。
打开 IDEA,ctrl+shift+f
搜索图片上传的代码
一般像阿里云、七牛云上传,都是直传,直传就是不经过我们应用服务器,直接上传到阿里云或七牛云存储服务器。
问题就很明显了,因为这个项目的服务器带宽是有限制的,图片上传是先上传到我们应用服务器,然后应用服务器再上传到阿里云服务器。
直传
下图是阿里云存储直传的流程图
当用户要上传一个文件到OSS,而且希望将上传的结果返回给应用服务器时,需要设置一个回调函数,将请求告知应用服务器。用户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户。
注意一下,阿里云存储和七牛云存储还有点区别,如果你不设置回调,阿里云就不会返回你图片URL。
上传密钥申请
- endpoint
- accessKey、secretKey
创建用户,需要手机号验证
- bucket
- fileUrl (bucket+enpoint)
比如 bucket = demo ,enpoint = oss-cn-hangzhou.aliyuncs.com
那 fileUrl = demo.oss-cn-hangzhou.aliyuncs.com
后端代码
集成阿里云上传SDK
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.0</version>
</dependency>
初始化 ossClient
private OSS getOssClient() {
return new OSSClientBuilder().build(endpoint, accessKey, secretKey);
}
获取上传token
/**
* 获取上传token
* @param dir 文件指定目录 比如 product/demo.jpg 就传 product
*/
public Map getToken(String dir) {
OSS client = getOssClient();
// 过期时间
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConditions = new PolicyConditions();
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = client.generatePostPolicy(expiration, policyConditions);
byte[] binaryData = new byte[0];
try {
binaryData = postPolicy.getBytes("utf-8");
} catch (UnsupportedEncodingException e) {
throw new UploadException("阿里云上传失败:", e);
}
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = client.calculatePostSignature(postPolicy);
Map<String, String> respMap = new LinkedHashMap<>();
respMap.put("OSSAccessKeyId", param.accessKey);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", param.fileUrl);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
return respMap;
}
控制层代码
@RequestMapping(value = "/getToken")
@ResponseBody
public AjaxResult getUploadToken(String dir,String suffix) {
Map map = (Map)FileUploadUtil.getToken(UploadPlatform.Ali, dir);
JSONObject jsonObject = new JSONObject();
jsonObject.put("callbackUrl","http://demo.com/callback");
jsonObject.put("callbackBody","filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
jsonObject.put("callbackBodyType","application/x-www-form-urlencoded");
String s = jsonObject.toString();
String encode = Base64.encode(s);
map.put("callback",encode);
map.put("key",map.get("dir")+"/"+RandomUtil.randomString(16)+"."+suffix);
return AjaxResult.success(map);
}
/**
* 回调
*/
@RequestMapping(value = "/callback")
@ResponseBody
public Map callback(HttpServletRequest request) {
String fileName = request.getParameter("filename");
String size = request.getParameter("size");
String mimeType = request.getParameter("mimeType");
String height = request.getParameter("height");
String width = request.getParameter("width");
Map map = new HashMap();
map.put("url",fileName);
return map;
}
前端代码
upload.js
import request from '@/utils/request'
// 获取上传token
export function getToken(query) {
return request({
url: '/upload/getToken',
method: 'get',
params: query
})
}
图片上传组件
<template>
<div>
<el-upload
:action="host"
:before-upload="beforeUpload"
:data="reqData"
:file-list="fileList"
:limit="1"
:on-error="uploadError"
:on-exceed="handleExceed"
:on-remove="handleRemove"
:on-success="uploadSuccess"
list-type="text"
>
<el-button size="small" slot="trigger" type="primary">点击上传</el-button>
<div class="el-upload__tip" slot="tip">只能上传 apk 文件,且不超过10M</div>
</el-upload>
</div>
</template>
<script>
import {getToken} from "@/api/system/upload.js";
import md5 from 'blueimp-md5'
export default {
props: {
value: {type: String},
dir: {
type: String,
default: "default"
},
fileName: {type: String}
},
data() {
return {
fileList: [],
host: 'http://xxxx.oss-cn-beijing.aliyuncs.com/',
reqData: {},
fileUrl: "",
}
},
created() {
if (this.value) {
this.fileUrl = this.host + this.value;
this.fileList = [{name: this.fileName, url: this.fileUrl}]
console.log(this.fileList)
}
},
methods: {
// 获取阿里云数据
async getAliyunToken(query) {
const {data} = await getToken(query);
this.reqData.OSSAccessKeyId = data.OSSAccessKeyId;
this.reqData.policy = data.policy;
this.reqData.signature = data.signature;
this.reqData.success_action_status = 200;
this.reqData.callback = data.callback;
},
emitInput(url, name) {
this.$emit("uploadFile", url, name);
},
handleRemove(file, fileList) {
this.emitInput('', '');
},
uploadSuccess(response, file, fileList) {
if (response.url) {
this.fileUrl = this.host + response.url;
}
this.$emit("uploadFile", this.reqData.key);
},
handleExceed(files, fileList) {
this.$message.warning(
`当前限制选择 1 个文件,如需更换,请删除上一个文件再重新选择!`
);
},
uploadError(err, file, fileList) {
this.$message({
message: "上传出错,请重试!",
type: "error",
center: true
});
},
async beforeUpload(file) {
console.log(file);
// todo:上传文件的类型控制....
// const isVideo = file.type === 'video/mp4' || file.type === 'video/ogg' || file.type === 'video/flv' || file.type === 'video/avi' || file.type === 'video/wmv' || file.type === 'video/rmvb';
// const isLt10M = file.size / 1024 / 1024 < 10;
// if (!isVideo) {
// this.$message.warning('请上传正确格式的视频!');
// return false;
// } else {
// if (!isLt10M) {
// this.$message.warning('上传视频文件大小不能超过 10MB!');
// return false;
// }
// }
var index1 = file.name.lastIndexOf(".");
var index2 = file.name.length;
var suffix = file.name.substring(index1 + 1, index2);
let curr = (+new Date()).toString()
let random = Math.random() * 10000
let md5Str = md5(`${curr}${random}${file.name}`)
this.reqData.key = this.dir + '/' + md5Str + "." + suffix;
let query = {dir: this.dir};
await this.getAliyunToken(query)
console.log(this.reqData.key);
},
}
}
</script>
我这边是用 el-upload
集成的 ,后端人员也可以直接用 postman
进行测试