web端oss直传方案之vue+elementUI+OSS实践篇(附各种踩坑)

文章目录

  • 解决思路
  • 实践
    • 工具类uploadOss.js
    • 封装上传组件NewUpload
    • 调用上传组件
  • 遇到的问题
    • 从oss获取下载链接错误
    • 分片上传报错 - ETag配置
    • 取消上传
    • STS token 常见问题
      • 有效期
      • 多个Token是否同时有效
  • 总结

      以前的项目上传及下载都是web端上传至服务端,服务器端再上传至OSS,小文件这种方案可以接受,但文件大了性能就会超级糟糕(浏览器崩溃也是常态)!所以呢,不得不探索web端直传oss方案。
      探索过程中若采用最简单方式-将oss配置到前端,appId和appSecret会全部暴露!所以本文采用STS临时访问凭证访问OSS(需要服务器端提供stsToken接口),STS相关文档请查看官网。

解决思路

  1. 引入ali-oss依赖,版本号:"ali-oss": "^6.17.1"
  2. 定义oss工具js文件,uploadOss.js
  3. 封装ElementUI的el-upload组件NewUpload
  4. 在界面中引用NewUpload组件并使用

工程目录

├── src 
│   ├── api  # 服务端API
│   │   ├── oss.js     # 定义服务端API:stsToken 
│   └── components  # 封装组件 
│       ├── Upload       
│       	├── index.vue  
│       	├── uploadOss.js  
├── views # 视图 
│   ├── pay   
│   │   ├── index.vue 
├── package.json # 依赖 

实践

工具类uploadOss.js


该工具类包含三个方法:

  • 分片上传
  • 获取oss文件临时链接
  • 设置取消上传标志位
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,
}

封装上传组件NewUpload

<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>

遇到的问题

从oss获取下载链接错误

使用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/*"
            ]
        }
    ]
}

分片上传报错 - ETag配置

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;
}

STS token 常见问题

RAM角色和STS Token常见问题

有效期

STS Token的有效期最小值为900秒,最大值为角色最大会话时间设置的值,默认值为3600秒。

多个Token是否同时有效

STS Token在过期之前都是有效的,无论是否创建了新的STS Token。

总结

每一次解决问题,都开启了一片新天地~ 还真是那句话:学无止境

你可能感兴趣的:(前端,阿里云OSS,vue,vue.js,前端,elementui,阿里云)