上传背景介绍

在项目需求中,关于图片、视频、文件等上传文件,一般不是直接放置在自己的后台服务器上,一般都会购买云服务进行存储。譬如阿里云的oss对象存储。

那么,前端开发项目中,涉及到上传的功能时,我们不是把文件上传到自己的后台服务器,而是阿里云上面去,然后拿到文件的访问地址,例如图片的地址,再传递给后台保存下来,保存的是一个阿里云存储地址。

那么,前端如何实现阿里云oss文件上传功能呢?

vue ios 上传音频 vue上传文件到oss_vue.js

官方文档上说了:(阿里云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的门,不然,阿里云怎么你有没有给他交钱呢?服务端签名,就是把这个生成钥匙的过程放到了自己后台服务上,让他们去把钥匙给我们。我们前端自己不保管这么敏感的资料。

具体可以看这里https://help.aliyun.com/document_detail/31926.htm?spm=a2c4g.11186623.0.0.3627344eM9Gwj8#concept-en4-sjy-5db

封装的上传示例代码如下:

// 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上传

具体可以看这里:https://help.aliyun.com/document_detail/64047.htm?spm=a2c4g.11186623.0.0.119f3967Xq1Eb8#concept-64047-zh

这种方式,需要安装依赖包或引入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
      )

这种方式,有点缺陷,就是不能使用上传进度。

如果要使用上传进度,就需要使用分片上传功能才行。

vue ios 上传音频 vue上传文件到oss_前端_02


我们简单上传使用的这个put方法。不支持进度功能。进度功能,需要使用另外一个方法:

vue ios 上传音频 vue上传文件到oss_vue.js_03