以前的项目上传及下载都是web端上传至服务端,服务器端再上传至OSS,小文件这种方案可以接受,但文件大了性能就会超级糟糕(浏览器崩溃也是常态)!所以呢,不得不探索web端直传oss方案。
探索过程中若采用最简单方式-将oss配置到前端,appId和appSecret会全部暴露!所以本文采用STS临时访问凭证访问OSS(需要服务器端提供stsToken接口),STS相关文档请查看官网。
"ali-oss": "^6.17.1"
NewUpload
NewUpload
组件并使用工程目录
├── src
│ ├── api # 服务端API
│ │ ├── oss.js # 定义服务端API:stsToken
│ └── components # 封装组件
│ ├── Upload
│ ├── index.vue
│ ├── uploadOss.js
├── views # 视图
│ ├── pay
│ │ ├── index.vue
├── package.json # 依赖
该工具类包含三个方法:
const OSS = require('ali-oss')
import {
stsToken
} from '@/api/oss.js'
let ossConfig = null;
// 取消上传控制项
let isCancel = false;
// 设置客户端请求访问凭证的地址
const OssFunc = async() => {
let res = await stsToken();
ossConfig = res.data;
const client = new OSS({
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。
region: 'oss-cn-hangzhou',
accessKeyId: ossConfig.accessKeyId,
accessKeySecret: ossConfig.accessKeySecret,
stsToken: ossConfig.securityToken,
bucket: ossConfig.bucketName,
timeout: 600000,
// HTTPS (secure: true) or HTTP (secure: false) protocol
secure: false,
refreshSTSTokenInterval: 3000000,
refreshSTSToken: stsToken().then(res => {
if (res.status === 0) {
console.log('成功刷新oss token');
ossConfig.accessKeyId = res.data.accessKeyId;
ossConfig.accessKeySecret = res.data.accessKeySecret;
ossConfig.securityToken = res.data.securityToken;
}
})
});
return client;
};
// 分片上传方法(没有设置分片相关设置,采用默认)
async function put(fileName, file) {
try {
let oss = await OssFunc();
const result = await oss.multipartUpload(fileName, file, {
'headers': {
'Access-Control-Allow-Origin': '*',
},
'progress': (progress) => {
// console.log('progress:', progress)
if (isCancel) {
oss.cancel();
// 复位
isCancel = false;
}
}
});
return result;
} catch (e) {
console.log(e);
}
}
// 设置取消上传标志位为true
async function cancelUpload() {
isCancel = true;
return true;
}
// 获取oss文件临时路径
async function getUrl(name) {
try {
let oss = await OssFunc();
const result = await oss.signatureUrl(name);
return result;
} catch (e) {
console.log(e);
}
}
export {
put,
getUrl,
cancelUpload,
}
<template>
<div class="images-list1">
<el-upload
ref="upload"
:class="className"
action="string"
:data="paramsData"
:limit="fileLimit"
:show-file-list="showFile"
:on-success="handleSuccess"
:on-error="handleUploadError"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:on-preview="handlePreview"
:multiple="fileLimit > 1"
:list-type="listType"
:file-list="fileList"
:drag="dragable"
:http-request="handleUploadFile"
>
<div v-if="className === 'avatar-uploader'">
<img v-if="originData.showUrl" :src="originData.showUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="originData.showUrl" alt="" />
</el-dialog>
</div>
<div v-else-if="className === 'upload-demo'">
<!-- <i v-if="listType === 'picture-card'" class="el-icon-plus"></i>
<i v-else class="el-icon-upload uploIcon"></i> -->
<div v-if="loading">
<span class="el-icon-loading" style="font-size: 18px" />
</div>
<div v-else>
<div v-if="fileLimit > 1">
<el-button plain :size="btnSize" icon="el-icon-plus">{{ btnText }}</el-button>
</div>
<div v-else>
<div v-if="originData.key">
<span class="content">{{ originData.key }}</span>
</div>
<div v-else>
<el-button plain :size="btnSize" icon="el-icon-plus">{{ btnText }}</el-button>
</div>
</div>
</div>
</div>
</el-upload>
<span v-if="loading && className === 'upload-demo'" class="cancel" @click="handleCancel"
>取消上传</span
>
<div v-if="showTip" class="el-upload__tip">允许文件类型:{{ fileTypeName || 'jpg/png' }}</div>
<div v-if="showTip" class="el-upload__tip">文件大小上限:{{ fileLimit || 1 }}M</div>
</div>
</template>
<script>
import {
put,
getUrl,
cancelUpload
} from '@/components/Upload/uploadOss.js'
export default {
name: 'NewUpload',
props: {
// 值
value: [String, Object, Array],
// 大小限制(MB)
fileSize: {
type: Number,
default: 1
},
// 文件类型, 例如["doc", "xls", "ppt", "txt", "pdf"]
fileType: {
type: Array,
default: () => []
},
// 文件列表类型 text/picture/picture-card
listType: {
type: String,
default: 'picture'
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 最大允许上传个数
fileLimit: {
type: Number,
default: 99
},
// 是否显示上传的文件列表
showFile: {
type: Boolean,
default: false
},
// 文件上传样式class
className: {
type: String,
default: 'upload-demo'
},
// 是否允许拖拽上传
dragable: {
type: Boolean,
default: false
},
// 源数据
originData: {
type: [Object, Array],
default: {}
},
// 按钮显示的文案
btnText: {
type: String,
default: '上传'
},
// 按钮大小
btnSize: {
type: String,
default: 'mini'
}
},
data() {
return {
uploadUrl: '', // 上传的图片服务器地址
paramsData: {}, // 上传携带的参数,看需求要不要
fileList: [],
tempFileList: [], // 因为 fileList为只读属性,所以用了一个中间变量来进行数据改变的交互。
imageUrl: '',
loading: false,
dialogVisible: false,
allowUplad: true
}
},
watch: {
value: {
handler: function (newVal) {
this.tempFileList = newVal
},
immediate: true,
deep: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize)
},
fileTypeName() {
let typeName = ''
this.fileType.forEach((item) => {
typeName += `${item},`
})
return typeName
},
fileAccept() {
let fileAccept = ''
this.fileType.forEach((element) => {
fileAccept += `.${element},`
})
return fileAccept
}
},
created() {
if (this.value) {
this.fileList = JSON.parse(JSON.stringify(this.value))
}
var token = null
if (!JSON.parse(sessionStorage.getItem('tokenAll'))) {
token = null
} else {
token = JSON.parse(sessionStorage.getItem('tokenAll')).token
}
this.paramsData = {
token: token
}
},
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
let result = true
if (this.fileType && this.fileType.length > 0 && file) {
const isTypeOk = this.fileType.some((item) => {
let fileExtension = ''
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
}
if (fileExtension && fileExtension.indexOf(item) > -1) {
return true
} else {
return false
result = false
}
})
if (!isTypeOk && file) {
this.$message.error(`文件格式不正确, 请上传${this.fileType.join('/')}格式文件!`)
return false
result = false
}
}
// 校检文件大小
if (this.fileSize && file) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`)
return false
result = false
}
}
return result
},
handleUploadError() {
this.$message.error('上传失败, 请重试')
},
// 文件个数超出
handleExceed() {
this.$message.error(`超出上传文件个数,请检查!`)
},
// 图片预览
handlePreview(file) {
this.dialogVisible = true
},
// 文件上传成功的钩子
handleSuccess(res, file, fileList) {
console.log(res)
},
// 文件列表移除文件时的钩子
handleRemove(file, fileList) {
this.changeFileList(fileList)
},
// 文件列表改变的时候,更新组件的v-model的文的数据
changeFileList(fileList) {
const tempFileList = fileList.map((item) => {
let tempItem = {
name: item.name,
url: item.response ? item.response.payload.imgUrl : item.url
}
return tempItem
})
this.$emit('input', tempFileList)
},
// 自定义上传
handleUploadFile(option) {
if (this.handleBeforeUpload(option.file)) {
this.loading = true
//获取上传后的url
const _name = this.getDate() + '/' + option.file.uid + '.' + option.file.name.split('.')[1]
put(_name, option.file).then((res) => {
if (res) {
getUrl(res.name).then((rel) => {
this.originData.showUrl = rel
this.originData.key = _name
this.$emit('submit', {
class: this.className,
key: _name,
showUrl: rel
})
this.loading = false
this.$refs.upload.clearFiles()
})
} else {
this.$refs.upload.clearFiles()
}
})
}
},
// 取消上传
handleCancel() {
cancelUpload().then((rel) => {
if (rel) {
this.$refs.upload.abort()
this.loading = false
}
})
},
// 获取当前年月日
getDate() {
const date = new Date()
var year = date.getFullYear()
var month = date.getMonth() + 1
var day = date.getDate()
month = month > 9 ? month : '0' + month
day = day < 10 ? '0' + day : day
var today = year + month + day
return today
}
}
}
</script>
包含上传按钮及下载显示
<template>
<div class="uploadBox">
<NewUpload
:style="{ width: row.fileUrl ? '80%' : '100%' }"
class="uploads"
className="upload-demo"
:fileSize="10240"
:fileLimit="1"
:showFile="false"
:isShowTip="false"
listType="text"
:fileType="[]"
:originData="row.originData"
@submit="
(val) => {
uploadFileRes(val, row)
}
"
/>
<div v-if="row.fileUrl" class="close">
<span
class="el-icon-download"
@click="downFile(row.originData.showUrl)"
title="下载文件"
/>
</div>
</div>
</template>
<script>
import NewUpload from '@/components/Upload/index.vue'
import { getUrl } from '@/components/Upload/uploadOss.js'
export default {
components: { NewUpload },
created() {
getUrl(v2.fileUrl).then((rm) => {
obj.showUrl = rm
})
},
method: {
getOssFileUrl() {
getUrl(fileName).then((url) => {
// TODO:获取oss文件临时路径后赋值
})
},
uploadFileRes(data, row) {
row.fileUrl = data.key
},
downFile(rel) {
const downloadElement = document.createElement('a')
downloadElement.href = rel
document.body.appendChild(downloadElement)
downloadElement.click() // 点击下载
document.body.removeChild(downloadElement) // 下载完成移除元素
},
}
}
</script>
使用ali-oss中signatureUrl(name: string, options?: OSS.SignatureUrlOptions): string;
获取oss文件临时访问路径,提示如下错误:
错误1:Access denied by authorizer’s policy.
错误2:You have no right to access this object because of bucket acl.
在官网上寻找解决案例,发现是配置的问题(NOTE:若在阿里云管理平台和在代码中分别配置账户权限,只取交集!),可参考官方解决方案-
教程示例:使用RAM Policy控制OSS的访问权限
{
"Version":"1",
"Statement":[
{
"Effect":"Allow",
"Action":[
"oss:ListObjects"
],
"Resource":[
"acs:oss:*:*:examplebucket"
],
"Condition":{
"StringLike":{
"oss:Prefix":[
"Development",
"Development/*"
]
}
}
},
{
"Effect":"Allow",
"Action":[
"oss:GetObject",
"oss:PutObject",
"oss:GetObjectAcl"
],
"Resource":[
"acs:oss:*:*:examplebucket/Development/*"
]
}
]
}
Please set the etag of expose-headers in OSS
使用OSS分片上传功能上传文件时报“Please set the etag of expose-headers in OSS”错误
官方推荐方案:调用OSS的JS SDK实现取消分块上传及续传
核心点:调用分片上传时,在接口MultipartUploadOptions
中属性progress
进行取消动作
/**
* Upload file with OSS multipart.
*/
multipartUpload(name: string, file: any, options: OSS.MultipartUploadOptions): Promise<OSS.MultipartUploadResult>;
interface MultipartUploadOptions {
/** the number of parts to be uploaded in parallel */
parallel?: number | undefined;
/** the suggested size for each part */
partSize?: number | undefined;
/** the progress callback called after each successful upload of one part */
progress?: ((...args: any[]) => any) | undefined;
/** the checkpoint to resume upload, if this is provided, it will continue the upload from where interrupted, otherwise a new multipart upload will be created. */
checkpoint?: Checkpoint | undefined;
meta?: UserMeta | undefined;
mime?: string | undefined;
callback?: ObjectCallback | undefined;
headers?: object | undefined;
timeout?: number | undefined;
/** {Object} only uploadPartCopy api used, detail */
copyheaders?: object | undefined;
}
RAM角色和STS Token常见问题
STS Token的有效期最小值为900秒,最大值为角色最大会话时间设置的值,默认值为3600秒。
STS Token在过期之前都是有效的,无论是否创建了新的STS Token。
每一次解决问题,都开启了一片新天地~ 还真是那句话:学无止境!