前言

在Vue3项目中,如果我们想上传图片一般可以利用element-ui中的el-upload,为了避免代码的重复,我们可以自己封装一个图片上传组件。

其中,主要实现思想为前端利用el-upload组件选择上传的图片,并利用其http-request属性来自定义函数来实现文件上传请求:该请求函数使用七牛云的对象存储,在通过后端得到的上传凭证token后来实现文件上传。

后端代码

使用express框架,获取七牛云上传凭证并响应给前端

项目结构

 - routes
     |- token.js
     |- index.js
 - app.js
 - config.js
 - package.json

安装七牛云的SDK:

 npm i qiniu

获取上传凭证

编写获取上传凭证的相关代码:

 /* config.js */
 ​
 const qiniu = require('qiniu')
 ​
 // 创建上传凭证
 const accessKey = '*****' // 这里填写七牛云的accessKey
 const secretKey = '*****'// 这里填写七牛云的secretKey
 const mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
 const options = {
   scope: '*****', // 这里填写七牛云空间名称
   expires: 60 * 60 * 24 * 7 // 这里是凭证的有效时间,默认是一小时
 }
 const putPolicy = new qiniu.rs.PutPolicy(options)
 const uploadToken = putPolicy.uploadToken(mac)
 ​
 module.exports = {
   uploadToken
 }

配置路由

token.js

 const tokenRouter = require('express').Router()
 const qnconfig = require('../config') // 引入七牛云配置
 ​
 tokenRouter.get('/qiniu', (req, res, next) => {
   res.status(200).send(qnconfig.uploadToken)
 })
 ​
 module.exports = tokenRouter

index.js

 const token = require('./token')
 ​
 module.exports = routes = (app) => {
   app.use('/token', token) // 可以通过/token/qiniu的方式获取上传凭证
 }

项目启动

 const express = require('express')
 const bodyparse = require('body-parser')
 const routers = require('./route')
 ​
 // 创建服务
 const app = express()
 // 解析数据
 app.use(bodyparse.json())
 ​
 // 路由
 routes(app)
 ​
 // 监听3000端口
 app.listen(3000, () => {
   console.log('this server are running on localhost:3000!')
 })

使用命令node app.js启动项目,这时访问http://localhost:3000/token/qiniu即可获取上传凭证了。

前端代码

配置跨域

由于前后端项目运行在不同的端口,因此需要解决跨域问题,这里在vite.config.js中解决如下:

 server: {
   proxy: {
     '/api': {
       target: 'http://localhost:3000',
       changeOrigin: true,
       rewrite: (path) => path.replace(/^/api/, '')
     }
   }
 }

父组件使用

我们希望子组件上传图片得到一串url后父组件能接受到,并且在展示上传图片时其尺寸应能指定或者有默认值。

 <template>
   <Upload :url="imageUrl" @upload="changeUrl" />
 </template>
 ​
 <script setup>
 import Upload from '@/components/Upload.vue'
 import { ref } from 'vue'
     
 const imageUrl = ref('')
 ​
 const changeUrl = (url) => {
     imageUrl.value = url
 }
 </script>

封装组件Upload.vue

这里只是简单使用axios,没有对其进行封装。

<template>
  <!- action="https://upload-z2.qiniup.com":每个地区访问域名不同,具体可通过 https://developer.qiniu.com/kodo/1671/region-endpoint-fq 查看 ->	
  <el-upload
    class="avatar-uploader"
    action="https://upload-z2.qiniup.com"
    :show-file-list="false"
    :http-request="up2qiniu"
    :before-upload="beforeUpload"
  >
    <img
      v-if="props.url"
      :src="props.url"
      class="avatar"
      :style="'width: ' + props.width + 'px;' + 'height: ' + props.height + 'px;'"
    />
    <el-icon
      v-else
      class="avatar-uploader-icon"
      :style="'width: ' + props.width + 'px;' + 'height: ' + props.height + 'px;'"
      ><Plus
    /></el-icon>
  </el-upload>
</template>

<script setup>
import { ref } from 'vue'
import { getQiniuToken } from '../api/token'
import axios from 'axios'
import { ElMessage } from 'element-plus'

const qiniuaddr = 'rlr92qkze.hn-bkt.clouddn.com' // 这里是七牛云存储对象中的CDN域名
const imageUrl = ref('')
// 父组件传值时,须有图片的url;其次可选择图片的宽高(默认都为180)
const props = defineProps({
  url: String,
  width: {
    type: Number,
    default: 180
  },
  height: {
    type: Number,
    default: 180
  }
})
const emit = defineEmits(['upload'])

const beforeUpload = (rawFile) => {
  if (rawFile.type !== 'image/jpg' && rawFile.type !== 'image/png') {
    ElMessage.error('图片格式应该是png或jpg')
    return false
  } else if (rawFile.size / 1024 / 1024 > 2) {
    ElMessage.error('图片大小应该小于2MB')
    return false
  }
  return true
}

/**
 * 上传图片至七牛云
 * @param {*} req
 */
const up2qiniu = (req) => {
  const config = {
    headers: { 'Content-Type': 'multipart/form-data' }
  }
  const fileType = req.file.type === 'image/png' ? 'png' : 'jpg'
  // 重命名要上传的文件
  const keyname = 'blog' + new Date().getTime() + '.' + fileType
  axios.get('/api/token/qiniu').then(res => {
      const formdata = new FormData()
      formdata.append('file', req.file)
      formdata.append('token', res.data)
      formdata.append('key', keyname)
      // 获取到凭证之后再将文件上传到七牛云空间
      axios.post('https://upload-z2.qiniup.com', formdata, config).then((res) => {
        imageUrl.value = 'http://' + qiniuaddr + '/' + res.data.key
        emit('upload', imageUrl.value) // 向父组件传递图片的url
      })
  })  
}
</script>

<style lang="scss" scoped>
.avatar-uploader .avatar {
  width: 360px;
  height: 180px;
  display: block;
}
.avatar-uploader :deep(.el-upload) {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

.avatar-uploader :deep(.el-upload:hover) {
  border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 360px;
  height: 180px;
  text-align: center;
}
</style>