前端大文件直传阿里云OSS(适配uniapp和Element)

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://www.cnblogs.com/JankinLiu/p/15940293.html

这几天接到一个需求是这样的,用户需要在客户端上传视频,一般大小都在50M以上。

最开始我们的方案是先把文件上传到后端,后端再上传到阿里云OSS的。
由于文件过大,文件上传非常慢。为了用户体验增加了等待进度条,但这时又出现了新的问题,进度条100%但是后端没及时成功响应。因为后端虽然接收了文件,但阿里云OSS还没有返回响应,这个耗时也是非常久。于是考虑使用前端直传阿里云OSS。

oss配置

首先我们需要对OSS进行以下的配置:

image.png

如果开发小程序的话需要配置uploadFile合法域名(oss的域名)。

uniapp代码

uniapp端的话可以考虑使用这个插件。
阿里云oss文件直传-无需后台签名 - DCloud 插件市场

插件修改

修改了插件内的部分代码
uploadFile.js

const env = require('./config.js'); //配置文件,在这文件里配置你的OSS keyId和KeySecret,timeout:87600;

const base64 = require('./base64.js'); //Base64,hmac,sha1,crypto相关算法
require('./hmac.js');
require('./sha1.js');
const Crypto = require('./crypto.js');




const getPolicyBase64 = function() {
	let date = new Date();
	date.setHours(date.getHours() + env.timeout);
	let srcT = date.toISOString();
	const policyText = {
		"expiration": srcT, //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了 
		"conditions": [
			["content-length-range", 0, 5000 * 1024 * 1024] // 设置上传文件的大小限制,5mb
		]
	};

	const policyBase64 = base64.encode(JSON.stringify(policyText));
	console.log(policyBase64);
	return policyBase64;
}

const getSignature = function(policyBase64) {
	const accesskey = env.AccessKeySecret;

	const bytes = Crypto.HMAC(Crypto.SHA1, policyBase64, accesskey, {
		asBytes: true
	});
	const signature = Crypto.util.bytesToBase64(bytes);
	console.log(signature);
	return signature;
}

const aliyunServerURL = env.uploadImageUrl; //OSS地址,需要https
const accessid = env.OSSAccessKeyId;
const policyBase64 = getPolicyBase64();
const signature = getSignature(policyBase64); //获取签名
const uploadFile = {
	url: aliyunServerURL, //开发者服务器 url
	policyBase64,
	accessid,
	signature,
}

module.exports = uploadFile;

使用

html代码

<view class="upVideo" :style="{paddingTop:titleHeight+'px'}" @click="chooseVideo()" v-if="video_type != 'link'">
        <image class="upvideo" src="/static/upvideo.png" mode=""></image>
        <image class="hasVido" :src="fromData.cover_image_url" mode="" v-if="fromData.cover_image_url"></image>
        <view class="addBox loading-box" v-if="loading">
                <view class="loading">
                        <u-circle-progress active-color="#333333" width="240" duration="100" :percent="percent">
                                <view class="u-progress-content">
                                        <template v-if="percent==100">
                                                <template v-if="state == 'fail'">
                                                        <view class="u-progress-dot fail"></view>
                                                        <text class='u-progress-info'>上传失败</text>
                                                </template>
                                                <template v-else>
                                                        <view class="u-progress-dot success"></view>
                                                        <text class='u-progress-info'>上传完成</text>
                                                </template>
                                        </template>
                                        <template v-else>
                                                <view class="u-progress-dot"></view>
                                                <text class='u-progress-info'>上传{{percent}}%</text>
                                        </template>
                                </view>
                        </u-circle-progress>
                </view>
        </view>
        <view class="addBox" v-else>
                <image class="jv" src="/static/jv.png" mode=""></image>
                <view class="text">上传视频</view>
                <!-- <view class="texts">MP4格式(手机拍摄),最大50M</view> -->
                <view class="texts">MP4/3GP/M3U8格式(手机拍摄),时长30~120秒</view>
        </view>
</view>
//引入插件
import uploadImage from '@/js_sdk/yushijie-ossutil/ossutil/uploadFile.js';
methods: {
    chooseVideo() { // 上传视频
            var that = this;
            uni.chooseVideo({
                    count: 1,
                    compressed: false,
                    sourceType: ['album', 'camera'],
                    success: async function(resVideo) {
                            // let maxSize = (resVideo.size / 1024 / 1024).toFixed(2)
                            // if (maxSize > 50) {
                            // 	uni.showToast({
                            // 		icon: 'none',
                            // 		title: '视频最大为50M',
                            // 	})
                            // 	return false
                            // }
                            console.log(resVideo);
                            const extension = ["mp4", "3gp", "m3u8"]
                            console.log(resVideo);
                            let maxDuration = resVideo.duration
                            // #ifdef H5
                            const type = resVideo.tempFile.type.split('/')[1]

                            var ua = navigator.userAgent.toLowerCase();
                            var isWeixin = ua.indexOf('micromessenger') != -1;
                            if (isWeixin) {
                                    // 微信浏览器内大视频获取duration经常性为0,特此进行处理
                                    maxDuration = await that.getDuration(resVideo);
                            }
                            // #endif
                            // #ifdef MP-WEIXIN
                            const index = resVideo.tempFilePath.lastIndexOf('.')
                            const type = resVideo.tempFilePath.slice(index + 1)
                            // #endif

                            if (!extension.includes(type)) {
                                    uni.showToast({
                                            icon: 'none',
                                            title: '视频格式不支持',
                                    })
                                    return false
                            }

                            if (maxDuration < 30) {
                                    uni.showToast({
                                            icon: 'none',
                                            title: '视频必须大于30秒',
                                    })
                                    return false
                            }
                            if (maxDuration > 120) {
                                    uni.showToast({
                                            icon: 'none',
                                            title: '视频最长为2分钟',
                                    })
                                    return false
                            }
                            const {
                                    url,
                                    policyBase64,
                                    accessid,
                                    signature
                            } = uploadImage


                            that.loading = true
                            that.state = ""
                            that.percent = 0
                            that.fromData.video_url = ""
                            that.fromData.cover_image_url = ""
                            const aliyunFileKey = 'video/' + new Date().getTime() + Math.floor(Math.random() *
                                    150) + '.' + type;
                            let uploadTask = uni.uploadFile({
                                    url, //仅为示例,非真实的接口地址
                                    filePath: resVideo.tempFilePath,
                                    formData: {
                                            'key': aliyunFileKey,
                                            'policy': policyBase64,
                                            'OSSAccessKeyId': accessid,
                                            'signature': signature,
                                            'success_action_status': '200',
                                    },
                                    name: 'file',
                                    success: (uploadFileRes) => {
                                            if (uploadFileRes.statusCode == 200) {
                                                    uni.showToast({
                                                            icon: 'none',
                                                            title: "上传成功"
                                                    })
                                                    console.log(url + aliyunFileKey);
                                                    that.fromData.video_url = url + aliyunFileKey
                                                    that.fromData.cover_image_url = url + aliyunFileKey +
                                                            "?x-oss-process=video/snapshot,t_1000,f_jpg,m_fast"
                                                    that.state = 'success'
                                                    setTimeout(() => {
                                                            that.percent = 0
                                                            that.loading = false
                                                    }, 3000)
                                            } else {
                                                    that.state = 'fail'
                                                    setTimeout(() => {
                                                            that.percent = 0
                                                            that.loading = false
                                                    }, 3000)
                                                    uni.showToast({
                                                            icon: 'none',
                                                            title: '上传失败,请更换视频或重新上传'
                                                    })
                                            }
                                    },
                                    fail: (err) => {
                                            console.log(err);
                                            that.state = "fail"
                                            setTimeout(() => {
                                                    that.percent = 0
                                                    that.loading = false
                                            }, 3000)
                                            uni.showToast({
                                                    icon: 'none',
                                                    title: '上传失败,请更换视频或重新上传'
                                            })

                                    }
                            });


                            uploadTask.onProgressUpdate(that.onProgressUpdate((res) => {
                                    // console.log('上传进度' + res.progress);
                                    // console.log('已经上传的数据长度' + res.totalBytesSent);
                                    // console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
                                    that.percent = res.progress
                            }, 100))
                    }
            })
    },
    onProgressUpdate(func, wait = 50) {
            // 上一次执行该函数的时间
            let lastTime = 0
            return function(...args) {
                    // 当前时间
                    let now = +new Date()
                    // 将当前时间和上一次执行函数时间对比
                    // 如果差值大于设置的等待时间就执行函数
                    if (now - lastTime > wait) {
                            lastTime = now
                            func.apply(this, args)
                    }
            }
    },
}

H5端的话要考虑AccessKeySecret以及OSSAccessKeyId暴露的问题,可以考虑单独用ali-oss.js库单独进行处理,或者做好OSS的域名设置防止跨域请求。

PC端

PC端使用了ali-oss库进行开发

插件修改

/**
 * 阿里云oss上传工具
 */
let OSS = require('ali-oss');
let config = {
    region: 'oss-cn-hangzhou',
    secure:true,
    accessKeyId: '',
    accessKeySecret: '',
    bucket: ''
};

/** 
 * 配置
 */
let init = ()=>{
    return new OSS(config);
}

/** 
 * 生成uuid
 */
let guid = ()=>{
    let S4 = ()=>{
        return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
    }
    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
}

/** 
 * 修改文件名字
 */
let fileName = (file)=>{
    let arr = file.name.split(".");
    var uuid = "oss"+guid();
    if(arr.length>1){
        return uuid+'.'+arr[arr.length-1];
    }else{
        return uuid;
    }
}

/**
 * 上传文件
 */
let ossPut = (file,callback)=>{
    return new Promise((resolve, reject) => {
			
        let objectName = fileName(file);
        //videos/指的是放在oss的videos目录下
        init().multipartUpload('videos/'+objectName, file,{
					progress(p){ // 获取上传进度,上传进度为0 - 1, 1为上传100%
							callback(p)
					}
				}).then(({res,url}) => {
            if (res && res.status == 200) {
                console.log('阿里云OSS上传文件成功回调', res,url);
                resolve(res,url);
            }
        }).catch((err) => {
            console.log('阿里云OSS上传文件失败回调', err);
            reject(err)
        });
    })
}

/**
 * 下载文件
 */
let ossGet = (name)=>{
    return new Promise((resolve, reject) => {
        init().get(name).then(({res}) => {
            if (res && res.status == 200) {
                console.log('阿里云OSS下载文件成功回调', res);
                resolve(res);
            }
        }).catch((err) => {
            console.log('阿里云OSS下载文件失败回调', err);
            reject(err)
        });
    })
}

export default {ossPut,ossGet}

使用

import ossClient from './api/aliyun.oss.client.js';
Vue.prototype.$ossClient = ossClient;
<el-upload
    class="avatar-uploader"
    accept=".mp4,.3gp,.m3u8"
    :http-request="uploadHttp"
    action
    :show-file-list="false"
    :before-upload="beforeUploadVideo"
    >
    <img v-if="postData.cover_image_url" :src="postData.cover_image_url" class="avatar">
    <div v-else class="upIocn">
            <div v-if="videoFlag == false">
                    <img class="add" src="../assets/add.png" alt="">
                    <div class="tip">上传视频</div>
                    <div class="msg">MP4格式(手机拍摄),最长2分钟,最短30秒</div>
            </div>
    </div>
    <el-progress class="progress" v-if="videoFlag == true" type="circle" :percentage="parseFloat(videoUploadPercent)" />
</el-upload>
methods:{
    beforeUploadVideo(file){
            const that = this
            return new Promise((resolve,reject)=>{
                    let url = URL.createObjectURL(file);
                    let audioElement = new Audio(url);
                    audioElement.addEventListener("loadedmetadata", (_event)=> {
                            console.log(_event);
                            // let maxSize = (file.size / 1024 / 1024).toFixed(2)
                            // if (maxSize > 50) {
                            // 	that.$message({
                            // 		type: 'error',
                            // 		message: '视频最大为50M',
                            // 	})
                            // 	reject(false)
                            // 	return
                            // }
                            let duration = audioElement.duration;
                            console.log('duration',duration);
                            // 大小 时长
                            if (duration < 30) {
                                    //超过需求限制
                                    that.$message({
                                            message: '视频必须大于30秒',
                                            type: 'error'
                                    });
                                    reject(false)
                                    return
                            }
                            if (duration > 120) {
                                    //超过需求限制
                                    that.$message({
                                            message: '视频最长为2分钟',
                                            type: 'error'
                                    });
                                    reject(false)
                                    return
                            }
                            resolve(true)
                    });
            })
    },
    coverImageSuccess(e) {
      this.postData.cover_image_url = e.data.cover_image_url
			this.postData.video_url = e.data.video_url
			this.videoFlag = false
    },
    progress(p){
            let pecent = p * 100
            this.videoFlag = true
            this.videoUploadPercent = pecent.toFixed(0)
    },
    uploadHttp({ file }) {
            this.postData.cover_image_url = ""
            this.postData.video_url = ""
            this.videoFlag = true
            try{
                    this.$ossClient.ossPut(file, this.progress).then(res=>{
                            if(res.statusCode==200){
                                    let video_url = res.requestUrls[0]
                                    let index = video_url.lastIndexOf('?')
                                    video_url = video_url.slice(0, index)

                                    const cover_image_url = video_url + "?x-oss-process=video/snapshot,t_1000,f_jpg,m_fast"
                                    const data = {
                                            video_url,
                                            cover_image_url
                                    }
                                    console.log('data');
                                    this.coverImageSuccess({
                                            data
                                    })
                            }else{
                                    this.$message.error('上传失败,请更换视频或重新上传')
                            }
                    })
            }catch(e){
                    //TODO handle the exception
                    this.$message.error('上传失败,请更换视频或重新上传')
            }

    },    
}