Vue-自定义阿里云OSS前端直传组件

2,115 阅读3分钟

背景

每个OSS的用户都会用到上传服务。Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到OSS。具体流程如下图所示。

image-20210925131158303

和数据直传到OSS相比,以上方法有三个缺点:

  • 上传慢:用户数据需先上传到应用服务器,之后再上传到OSS。网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。
  • 扩展性差:如果后续用户多了,应用服务器会成为瓶颈。
  • 费用高:需要准备多台应用服务器。由于OSS上传流量是免费的,如果数据直传到OSS,不通过应用服务器,那么将能省下几台应用服务器。

后端说明

关于服务端签名,参考链接:

help.aliyun.com/document_de…

前端实现

关于前端直传,参考链接:

help.aliyun.com/document_de…

相关依赖
"ali-oss": "^6.2.1"
定义工具类
// 引入ali-oss
let OSS = require('ali-oss')

/**
 *  data后端提供数据
 *  [accessKeyId] {String}:通过阿里云控制台创建的AccessKey。
 *  [accessKeySecret] {String}:通过阿里云控制台创建的AccessSecret。
 *  [bucket] {String}:通过控制台或PutBucket创建的bucket。
 *  [region] {String}:bucket所在的区域, 默认oss-cn-hangzhou。
 */
export function client(data) {
  return new OSS({
    region: data.region,
    accessKeyId: data.accessKeyId,
    accessKeySecret: data.accessKeySecret,
    stsToken: data.stsToken,
    bucket: data.bucket
  })
}

/**
 * 生成随机文件名称
 * 规则八位随机字符,加下划线连接时间戳
 */
export const getFileNameUUID = () => {
  function rx() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
  }
  return `${+new Date()}_${rx()}${rx()}`
}

创建实例
<template>
  <div>
    <!--上传组件-->
    <el-upload
        v-if="uploadStyle===0"
        action=""
        list-type="picture-card"
        :file-list="fileList"
        :auto-upload="true"
        :on-exceed="handleExceed"
        :before-upload="handleBeforeUpload"
        :http-request="handleUploadFile"
        :on-preview="handleFileCardPreview"
        :on-remove="handleRemove">
      <i class="el-icon-plus"></i>
    </el-upload>
    <el-upload
        v-if="uploadStyle===1"
        class="upload-demo"
        action=""
        list-type="picture"
        :file-list="fileList"
        :auto-upload="true"
        :on-exceed="handleExceed"
        :before-upload="handleBeforeUpload"
        :http-request="handleUploadFile"
        :on-preview="handleFileCardPreview"
        :on-remove="handleRemove">
      <el-button size="small" type="primary">点击上传</el-button>
    </el-upload>
    <el-upload
        v-if="uploadStyle===2"
        class="upload-demo"
        drag
        multiple
        action=""
        :file-list="fileList"
        :auto-upload="true"
        :on-exceed="handleExceed"
        :before-upload="handleBeforeUpload"
        :http-request="handleUploadFile"
        :on-preview="handleFileCardPreview"
        :on-remove="handleRemove">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
    </el-upload>
    <!--进度条-->
    <el-progress
        v-show="showProgress"
        :text-inside="true"
        :stroke-width="15"
        :percentage="progress"
    ></el-progress>
    <!--展示上传的文件-->
    <el-dialog :title="dialogFileUrlTitle"
               :visible.sync="dialogFileUrlVisible"
               :close-on-click-modal="false"
               :close-on-press-escape="false"
               :destroy-on-close="true"
               append-to-body>
      <div v-if="dialogFileFormat==='image'">
        <img width="100%" :src="dialogFileUrl" alt="">
      </div>
      <div v-else-if="dialogFileFormat==='video'">
        <video
            controls
            controlslist="nodownload"
            preload="auto"
            style="width: 98%;text-align: center"
            :src="dialogFileUrl">
        </video>
      </div>
      <div v-else>
        <el-alert
            title="当前格式暂不支持预览!"
            type="error"
            center
            show-icon>
        </el-alert>
      </div>
    </el-dialog>
  </div>
</template>
数据方法
import { client, getFileNameUUID } from '@/utils/ali-oss'
// 获取后端鉴权的接口,具体看自己业务实现。
import { getStsPermission } from '@/api/common/common'

export default {
  name: 'AliUpload',
  created() {
    this.initialize()
  },
  data() {
    return {
      // 上传样式 0:卡片照片墙 1:缩略图 2:拖拽上传文件
      uploadStyle: 0,
      // oss上传所需数据对象
      ossData: {},
      // 文件列表
      fileList: [],
      // 进度条的显示
      showProgress: false,
      // 进度条数据
      progress: 0,
      // 文件参数表单
      fileParams: {
        // 上传的目录
        folder: '/default/'
      },
      // 展示上传
      dialogFileUrlVisible: false,
      dialogFileUrlTitle: '',
      dialogFileUrl: '',
      dialogFileFormat: ''
    }
  },
  model: {
    event: 'complete',
    prop: ''
  },
  // 组件插槽
  props: {
    // 上传位置
    position: {
      required: true
    },
    // 使用样式
    styleType: {},
    // 展示图片
    showFile: {
      type: String
    }
  },
  // 数据监听
  watch: {
    showFile: {
      handler() {
        this.onShowFile()
      }
    },
    position: {
      handler() {
        this.onPosition()
      }
    }
  },
  methods: {
    /** 组件初始化 */
    initialize() {
      this.onPosition()
      this.onShowFile()
      this.uploadStyle = this.styleType
    },

    /** showFile */
    onShowFile() {
      console.log('showFileUrl:' + this.showFile)
      if (this.showFile) {
        let url = this.showFile
        let name = '点击预览文件'
        this.fileList = [{ name, url }]
      } else {
        this.fileList = []
      }
    },

    /** position */
    onPosition() {
      if (this.position === '' || this.position == null) {
        this.$message({ message: '组件初始化失败!缺少[position]', type: 'warning' })
        return
      }
      switch (this.position) {
        case 'default':
          this.fileParams.folder = '/default/'
          break
        case 'system':
          this.fileParams.folder = '/system/'
          break
        case 'post':
          this.fileParams.folder = '/post/'
          break
        case 'circle':
          this.fileParams.folder = '/circle/'
          break
        case 'newsPicture':
          this.fileParams.folder = '/news/picture/'
          break
        case 'newsVideo':
          this.fileParams.folder = '/news/video/'
          break
        case 'liveGift':
          this.fileParams.folder = '/live/gift/'
          break
        default:
          this.$message({ message: '组件初始化失败!未知[position]', type: 'warning' })
          return
      }
    },

    /** 上传文件之前 */
    handleBeforeUpload(file) {
      this.$emit('upload-success', false)
      // 加载OSS配置参数
      return new Promise((resolve, reject) => {
        getStsPermission().then(response => {
          this.ossData = response.data
          resolve(true)
        }).catch(error => {
          this.$message({
            message: '加载上传配置失败!error msg:' + error,
            type: 'warning'
          })
          reject(false)
        })
      })
    },

    /** 文件超出个数限制 */
    handleExceed(files, fileList) {
      this.$message({
        message: '停!不能再多了~',
        type: 'warning'
      })
    },

    /** 文件列表移除文件 */
    handleRemove(file, fileList) {
      this.$message({
        message: '成功移除一个文件~',
        type: 'success'
      })
    },

    /** 点击文件列表中已上传的文件 */
    handleFileCardPreview(file) {
      let fileFormat = this.judgeFileFormat(file.url)
      switch (fileFormat) {
        case 'image':
          this.dialogFileUrlTitle = '图片预览'
          break
        case 'video':
          this.dialogFileUrlTitle = '视频预览'
          break
        default:
          this.$message.error(`当前格式为${fileFormat},暂不支持预览!`)
          return
      }
      this.dialogFileFormat = fileFormat
      this.dialogFileUrl = file.url
      this.dialogFileUrlVisible = true
    },

    /** 根据URL判断文件格式 */
    judgeFileFormat(fileUrl) {
      // 获取最后一个.的位置
      const index = fileUrl.lastIndexOf('.')
      // 获取后缀
      const suffix = fileUrl.substr(index + 1)
      console.log(`当前文件后缀格式为 suffix: ${suffix}`)
      // 获取类型结果
      let result = ''
      // 图片格式
      const imgList = ['png', 'jpg', 'jpeg', 'bmp', 'gif']
      // 进行图片匹配
      result = imgList.find(item => item === suffix)
      if (result) {
        return 'image'
      }
      // 匹配txt
      const txtList = ['txt']
      result = txtList.find(item => item === suffix)
      if (result) {
        return 'txt'
      }
      // 匹配 excel
      const excelList = ['xls', 'xlsx']
      result = excelList.find(item => item === suffix)
      if (result) {
        return 'excel'
      }
      // 匹配 word
      const wordList = ['doc', 'docx']
      result = wordList.find(item => item === suffix)
      if (result) {
        return 'word'
      }
      // 匹配 pdf
      const pdfList = ['pdf']
      result = pdfList.find(item => item === suffix)
      if (result) {
        return 'pdf'
      }
      // 匹配 ppt
      const pptList = ['ppt', 'pptx']
      result = pptList.find(item => item === suffix)
      if (result) {
        return 'ppt'
      }
      // 匹配 视频
      const videoList = ['mp4', 'm2v', 'mkv', 'rmvb', 'wmv', 'avi', 'flv', 'mov', 'm4v']
      result = videoList.find(item => item === suffix)
      if (result) {
        return 'video'
      }
      // 匹配 音频
      const radioList = ['mp3', 'wav', 'wmv']
      result = radioList.find(item => item === suffix)
      if (result) {
        return 'radio'
      }
      // 其他 文件类型
      return 'other'
    },

    /** 执行文件上传 */
    handleUploadFile(file) {
      let that = this
      // 选中目录
      let folder = that.fileParams.folder
      // 环境判断
      let active = that.ossData.active
      // 正式与测试环境存储分离
      if (active !== null && active !== undefined) {
        if (active === 'dev' || active === 'test') {
          folder = '/test' + folder
        }
      }
      // oss-host
      let host = 'https://static.v.xxxxxxxxx.com'

      async function multipartUpload() {
        let temporary = file.file.name.lastIndexOf('.')
        let fileNameLength = file.file.name.length
        let fileFormat = file.file.name.substring(
            temporary + 1,
            fileNameLength
        )
        // 文件路径和文件名 拼接 文件格式
        let fileNameUrl = folder + getFileNameUUID() + '.' + fileFormat
        client(that.ossData).multipartUpload(fileNameUrl, file.file, {
          progress: function(plan) {
            that.showProgress = true
            that.progress = Math.floor(plan * 100)
          }
        }).then(result => {
          that.$message({
            message: '上传成功',
            type: 'success'
          })
          console.log('上传完成 result:' + JSON.stringify(result))
          // 文件上传成功后填充输入框
          let uploadResult = host + fileNameUrl
          that.$emit('complete', uploadResult)
          that.$emit('upload-success', true)
          that.showProgress = false
        }).catch(error => {
          that.$message({
            message: '上传失败!error:' + error,
            type: 'warning'
          })
        })
      }

      multipartUpload()
    }
  }

}

组件使用

<el-form-item label="新闻封面" prop="sImgUrl">
  <a8-ali-upload position="newsPicture"
                 :styleType="2"
                 :showFile="saveParams.sImgUrl"
                 v-model="saveParams.sImgUrl">
  </a8-ali-upload>
  <el-input style="margin-top: 10px" v-model="saveParams.sImgUrl" placeholder="新闻封面链接"/>
</el-form-item>

以上就是在Vue.js下,阿里云OSS WEB直传的具体应用。