上传背景介绍
在项目需求中,关于图片、视频、文件等上传文件,一般不是直接放置在自己的后台服务器上,一般都会购买云服务进行存储。譬如阿里云的oss对象存储。
那么,前端开发项目中,涉及到上传的功能时,我们不是把文件上传到自己的后台服务器,而是阿里云上面去,然后拿到文件的访问地址,例如图片的地址,再传递给后台保存下来,保存的是一个阿里云存储地址。
那么,前端如何实现阿里云oss文件上传功能呢?
官方文档上说了:(阿里云oss文档地址)
1、使用阿里云上传的SDK来上传到阿里云oss
2、不使用sdk的方式,直接使用post表单提交到阿里云oss去
3、小程序(忽略)
其实,也就两种。要么使用阿里云给的sdk,他封装的上传方法等;要么使用表单提交,像提交到我们自己服务器一样。
上传方式1:表单直传
Web端通过表单上传方式直接上传数据到OSS。
官方说有如下三种实现方式。
1、在客户端通过JavaScript代码完成签名,然后通过表单直传数据到OSS。详情请参见JavaScript客户端签名直传。
2、在服务端完成签名,然后通过表单直传数据到OSS。详情请参见服务端签名后直传。
3、在服务端完成签名,并且服务端设置了上传后回调,然后通过表单直传数据到OSS。OSS回调完成后,再将应用服务器响应结果返回给客户端。详情请参见服务端签名直传并设置上传回调。
我司采用的是第2中,服务端签名后,前端在直传oss。
可以理解为:上传前,需要通过阿里云给的账号生成钥匙,然后前端拿到钥匙后去开阿里云oss的门,不然,阿里云怎么你有没有给他交钱呢?服务端签名,就是把这个生成钥匙的过程放到了自己后台服务上,让他们去把钥匙给我们。我们前端自己不保管这么敏感的资料。
封装的上传示例代码如下:
// fileUpload.js
import { v4 as uuidv4 } from 'uuid'
import axios from 'axios'
let basePath = 'www.yourapi.com' // 你的服务器接口域名
/**
* @description: 文件附件上传
* file: 文件raw对象
* successCallback: 成功的回调函数
* errCallBack: 错误的回调函数
* progressCallback: 上传进度的回调函数
* dir: 上传阿里云目标文件夹 eg:图片image,视频video等
*/
const upload = function(file, successCallback = new Function(), errCallBack = new Function(), progressCallback = new Function(), dir = 'image') {
let fileName = file.name
axios({
method: 'get',
url: basePath + '/aliyun/get', // 请求签名接口,找后台要
params: {
dir: dir // 'image' // 这里的参数,对应的就是上传到那个文件夹下面,找后台要
}
})
.then(res => {
// 拿到签名信息后,组装表单数据,作参考,具体的字段找后台要
let obj = res.data.data
let config = {}
config.host = obj['host']
config.policyBase64 = obj['policy']
config.accessid = obj['accessId']
config.signature = obj['signature']
config.expire = parseInt(obj['expire'])
config.callbackbody = obj['callback']
config.dir = obj['dir']
let fd = new FormData(),
uuid = uuidv4(),
key = config.dir + uuid
fd.append('key', key)
fd.append('success_action_status', '200')
fd.append('x-oss-object-acl', 'public-read')
fd.append('x-oss-meta-fullname', fileName)
fd.append('OSSAccessKeyId', config.accessid)
fd.append('policy', config.policyBase64)
fd.append('signature', config.signature)
fd.append('success_action_status', '200')
fd.append('file', file)
if (config.host.indexOf('http:') > -1) {
var protocol = window.location.protocol || 'http:'
var subUrl = config.host.substring(5, config.host.length)
config.host = protocol + subUrl
}
// 数据组装完成后,发送上传请求到阿里云oss
axios({
url: config.host,
method: 'POST',
data: fd,
processData: false,
cache: false,
contentType: false,
// 这里,我们可以做上传经度
onUploadProgress: function(progressEvent) {
if (progressEvent.lengthComputable) {
let percent = (progressEvent.loaded / progressEvent.total) * 100 || 0
progressCallback({
percent: percent
})
}
}
})
.then(() => {
// 拿到结果后,做其他操作
let size = file.size > 1000000 ? parseFloat(file.size / 1000000).toFixed(2) + 'M' : parseFloat(file.size / 1000).toFixed(2) + 'KB'
successCallback({
attachment: fileName,
aliyunAddress: key,
size: size,
host: config.host
})
})
.catch(err => {
errCallBack(err)
})
})
.catch(err => {
errCallBack(err)
})
}
export default upload
那么,在element-ui upload组件中使用自定义上传功能:
<template>
<div class="text-msg-pic-upload">
<el-upload
:class="{ display: uploadDisabled }"
list-type="picture-card"
ref="upload"
action
multiple
:http-request="handleUpload"
:auto-upload="autoUpload"
:limit="limit"
:file-list="tempFileList"
:on-exceed="handleExceed"
:on-success="handleSuccess"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:before-upload="beforeUpload"
:on-preview="handlePictureCardPreview"
accept="jpg,.jpeg,.png,.JPG,.JPEG"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip" v-if="tipsFlag">{{ tips }}</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
<div class="Upload_pictures">
<ul class="el-upload__tip cBBBDBF" style="color: #BBBDBF;">
<li>支持PNG、JEPG格式 ,不超过2MB。</li>
</ul>
</div>
</div>
</template>
<script>
import upload from '@/utils/fileUpload.js'
export default {
name: 'UploadImageDemo',
props: {
width: {
type: String,
default: '240px'
},
autoUpload: {
type: Boolean,
default: true
},
limit: {
type: Number,
default: 3
},
limitType: {
type: Array,
default: () => ['image/jpeg', 'image/png', 'image/jpg']
},
disabled: {
type: Boolean,
default: false
},
imgList: {
type: Array,
default: () => []
},
tipsFlag: {
type: Boolean,
default: false
},
tips: {
type: String,
default: ''
}
},
data() {
return {
// 上传文件列表,el-upload使用,临时保存数据。
tempFileList: this.imgList,
host: '', // 阿里云上传服务器地址根路径
uploadDisabled: false,
dialogImageUrl: '',
dialogVisible: false
}
},
watch: {
// 解决第二渲染接口, 图片还保留着原来的问题 JerryYi
imgList: {
immediate: true,
handler(val) {
this.tempFileList = val
}
}
},
computed: {
upText() {
return this.autoUpload ? '上传文件' : '选择文件'
}
},
created() {
},
mounted() {},
methods: {
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
beforeUpload(file) {
// console.log('beforeUpload', file)
let types = this.limitType
const isImage = types.includes(file.type)
const isLt20M = file.size / 1024 / 1024 < 10
if (!isImage) {
this.$message({
message: types.length == 0 ? '上传图片只能是 PNG 格式!' : '上传图片只能是 JPG、PNG 格式!',
type: 'warning'
})
return false
}
if (!isLt20M) {
this.$message.error('上传图片大小不能超过 1MB!')
return false
}
return isImage && isLt20M
},
// 自定义上传操作
handleUpload(op) {
let dir = `images`
upload(
op.file,
res => {
let temp = {
name: res.attachment,
url: res.host + '/' + res.aliyunAddress
}
this.host = res.host
op.onSuccess(temp)
},
err => {
console.log(err)
},
res => {
op.onProgress(res)
},
dir
)
},
// 上传成功后触发
handleSuccess(response, file, fileList) {
this.filterFileFn(fileList)
},
// 返回给接口要用的格式
filterFileFn(fileList) {
let filterArr = fileList
.filter(item => !item.status || item.status !== 'ready') // 过滤未上传的文件
.map(item => {
let url = item.response ? item.response.url : item.url
return {
url: url, // item.url || item.response.url
name: item.name
}
})
// console.log('fileList', fileList)
this.$emit('onSuccessFiles', filterArr)
},
// 监听移除文件列表
handleRemove(file, fileList) {
if (file.status === 'success') {
this.filterFileFn(fileList)
}
},
handleExceed(files, fileList) {
this.$message({ message: `当前限制选择 ${this.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`, type: 'warning' })
},
beforeRemove() {
// return this.$confirm(`确定移除 ${file.name}?`)
}
}
}
</script>
<style>
.text-msg-pic-upload .el-upload--picture-card,
.text-msg-pic-upload .el-upload-list--picture-card .el-upload-list__item {
width: 62px;
height: 62px;
line-height: 72px;
}
.display .el-upload--picture-card {
display: none;
}
</style>
使用:
<UploadImage :limit="9" :imgList="fileImgList" @onSuccessFiles="onSuccessImgFiles" />
// ...........其他略
data(){
return{
fileImgList: [{ name: 'test', url: 'http://testworldunion.oss-cn-shanghai.aliyuncs.com/scrm/1000/test/450821d6-13e1-464e-bc32-569fd277be2c.jpg' }] //图片列表
}
},
methods: {
// 监听图片上传
onSuccessImgFiles(files) {
console.log('onSuccessImgFiles', files)
this.fileImgList = files
}
}
一句话: 使用el-upload的自定义上传,结合我们封装的函数,实现上传功能。
上传方式二:阿里云oss SDK上传
这种方式,需要安装依赖包或引入js文件。
npm install ali-oss
同样的,我们封装一下上传:
import { v4 as uuidv4 } from 'uuid'
import axios from 'axios'
let basePath = 'www.yourapi.com' // 你的服务器接口域名
const OSS = require('ali-oss')
/**
* 阿里云oss sdk文件上传
* @param {*} file 文件流
* @param {*} successCallback 成功回调
* @param {*} errCallBack 失败回调
* @param {*} bucketName 阿里云桶名(可以指定多个桶名)
* @param {*} dir 上传文件夹路径 譬如images
*/
export function bucketUpload(file, successCallback = new Function(), errCallBack = new Function(), bucketName = '你的阿里云桶名', dir = 'image') {
let fileName = file.name
let pathName = window.location.host
let bucketNameTemp = bucketName
let requestData = {
bucket: bucketNameTemp,
dir: dir
}
// 先获取上传要的资料签名
axios({
method: 'post',
url: basePath + '/aliyunsts', // 找后台要接口,返回new OSS需要的参数
headers: {
'Content-Type': 'application/json'
},
data: requestData // 这里的参数,看后台要什么,沟通确定
})
.then(res => {
let obj = res.data || {}
let config = {}
// console.log(obj)
config.host = obj.OssUrl
// 实例化一个上传客户端
const client = new OSS({
// yourRegion填写Bucket所在地域。Region填写为oss-cn-hangzhou。
region: obj.OssRegion,
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
accessKeyId: obj.AccessKeyId,
accessKeySecret: obj.AccessKeySecret,
// 从STS服务获取的安全令牌(SecurityToken)。
stsToken: obj.SecurityToken,
// 填写Bucket名称。
bucket: obj.BucketName
})
try {
// 填写Object完整路径。Object完整路径中不能包含Bucket名称。
// 您可以通过自定义文件名(例如exampleobject.txt)或文件完整路径(例如exampledir/exampleobject.txt)的形式实现将数据上传到当前Bucket或Bucket中的指定目录。
// data对象可以自定义为file对象、Blob数据或者OSS Buffer。
// 为保证唯一性,通过uuid将文件名替换
let uuid = uuidv4() + fileName.substring(fileName.lastIndexOf('.'))
if (dir.substring(dir.length - 1, 1) !== '/') {
dir += '/'
}
const result = client.put(dir + uuid, file)
result
.then(res => {
console.log(res)
let size = file.size > 1000000 ? parseFloat(file.size / 1000000).toFixed(2) + 'M' : parseFloat(file.size / 1000).toFixed(2) + 'KB'
successCallback({
attachment: fileName,
aliyunAddress: res.url,
size: size,
host: config.host
})
})
.catch(err => {
errCallBack(err)
})
} catch (e) {
console.log(e)
}
})
.catch(err => {
errCallBack(err)
})
}
对应的el-upload中的自定义上传方法就改了:
handleUpload(op) {
let bucketName = 'myaliyunossbucketname' // 桶名
let dir = `images`
bucketUpload(
op.file,
res => {
let temp = {
name: res.attachment,
url: res.aliyunAddress
}
this.host = res.host
op.onSuccess(temp)
},
err => {
console.log(err)
},
bucketName,
dir
)
这种方式,有点缺陷,就是不能使用上传进度。
如果要使用上传进度,就需要使用分片上传功能才行。
我们简单上传使用的这个put方法。不支持进度功能。进度功能,需要使用另外一个方法: