最近开发新博客网站时,有几页需要使用照片上传功能。

整个项目前端基于vue3的element-plue和vue-cropper组件库封装一个图片上传组件,后端使Django REST framework开发api接口,存储使用七牛对象存储,以及腾讯CDN加速,总结了完整的前后端代码以及运维配置,以供大家参考。

一、流程与思路分析

1. 整体流程图

2. 流程分析

用户图片上传以及显示整个过程可分为以下几个阶段:

  • 页面加载阶段

当用户访问HTTPS://www.cuiliangblog.cn/applyLink时,前端nginx服务器接收请求(如果配置了CDN,DNS会智能解析到CDN节点处理请求),返回页面数据给用户,浏览器加载并显示页面

  • 用户上传图片阶段

用户选择好本地图片文件,点击开始上传操作后,浏览器向后端API接口发送请求,获取此次上传操作的token

API后端接收到请求后,使用七牛SDK请求七牛云存储服务,获取上传token后将token返回给客户端

客户端使用token上传文件至七牛对象存储服务,上传成功后,七牛存储返回客户端资源的URL地址给客户端

  • 浏览器请求图片资源阶段

客户端根据图片URL地址请求CDN服务,CDN节点发现没有找到资源后回源至七牛对象存储服务,获取文件资源成功后缓存至CDN并返回给客户端

  • 用户提交表单阶段

用户提交表单,表单内容中包含资源的URL地址请求后端API接口

后端API接口保存图片资源URL地址

3. 开发需求分析

本案例使用如今最流行的前后端分离开发模式。

  • 前端使用vue3开发,主要实现用户选择本地图片后裁剪成文件blob,以及将文件流和token直接请求对象存储服务,实现文件上传两个功能。
  • 后端使Python开发,借助django REST framework框架开发api接口。安装七牛对象存储的sdk,通过请求七牛服务,获取本次操作的token,并返回给前端。

4. 运维配置分析

本案例使用主流的企业网站项目配置,使用公有云的OSS对象存储服务、CDN内容分发网络以及DNS域名解析。此处选用七牛云对象存储和腾讯云CDN以及阿里云的域名解析,其他公有云厂商产品名称和配置项可能略有差异,但基本原理都是一样的,操作步骤也并无差别。

没有注册的小伙伴们可以使用以下链接进行注册

  • 七牛云:
  • 阿里云:
  • 腾讯云:

二、对象存储配置

此处使用七牛对象存储,有10G免费空间,对于一般小业务场景完全够用。

1. 创建云存储空间

登录七牛云——>进入控制台——>点击对象存储——>然后点击新建空间

填写表单完成存储空间的创建,完后后七牛云会自动为我分配一个测试域名,这样我们就可以使用这个域名进行上传/下载文件了。

需要注意的是:测试域名只能使用30天!!并且测试域名只能使用HTTP协议,不支持HTTPS协议

2. 对象存储服务绑定域名

因为我已经购买过域名cuiliangblog.cn。所以接下来绑定o给这个存储空间即可。需要注意的是,虽然七牛云的对象存储服务免费,但是CDN加速服务是收费的,我已经够买过腾讯CDN服务,所以此处配置了自定义源站域名,大家可以按照自己的实际情况选择最合适的配置。

  • 需要注意的是如果使用第三方CDN服务,记住这个默认分配的CNAME,CDN配置回源策略时,填写这个CNAME。

三、CDN配置

1. 添加CDN加速域名

登录腾讯云——>控制台——>CDN内容分发网络——>域名管理——>添加域名

2. 配置回源及缓存等策略

此处以我的o对象存储域名举例,其中回源策略填写七牛云对象存储的CNAME。

四、DNS配置

1. 添加域名解析记录

登录阿里云——>控制台——>域名——>解析

  • 新增一条o的CNAME域名解析记录,记录值填写腾讯云CDN的CNAME值

2. 访问验证

至此,存储服务和CDN以及DNS配置已全部完成,接下来做一个简单的测试

  • 七牛云——>控制台——>对象存储——>空间管理——>文件管理——>上传文件,随便选择一张图片资源上传

  • 随便上传一张图片后,点击返回,查看图片资源外链

  • 使用浏览器访问验证域名解析是否正常

经过访问测试,上传图片后生成的外链可以正常打开访问,且远程地址为腾讯云CDN加速节点。到这儿,运维的工作已经完成了,接下来角色转换,现在是一位专业的后端开发工程师。

五、后端-token接口开发

1. 后端功能模块概述

要想使用七牛的对象存储服务上传文件,就需要在后端通过七牛SDK生成的一个安全凭证,只有客户端拿着这个上传凭证上传文件才是有效的,否则七牛服务器是不接受的。七牛云的开发者中心提供了很多版本的SDK,例如Go,JavaScript,PHP,Python,Node.js,Ruby,C#,C/C++等等,这里是我使用的是python的SDK,详见开发者文档:

2. 获取accessKey和secretKey

通过查看开发者文档可知,调用SDK需要传入bucket、accessKey、secretKey三个参数

bucket的值就是存储空间的名称,accessKey和secretKey可以将鼠标悬浮在右上角的头像上然后点击密钥管理,然后创新密钥

3. DRF项目相关功能代码实现

  • 安装SDK
pip install qiniu
  • 中存放密钥信息
# 七牛OSS存储配置 QINIU_AK = 'XXXXXXXXXXXXX' QINIU_SK = 'XXXXXXXXXXXXX' QINIU_BUCKET = 'cuiliangoss' QINIU_DOMAIN = 'https://o/'
  • 配置请求token的API接口路由(urls.py)
from django.urls import path from rest_framework import Routers from public import views app_name = "public" urlpatterns = [ path('qiniuToken/', views.QiniuTokenAPIView.as_view()), # 获取七牛上传token ………… ] router = rou() urlpatterns += rou
  • 编写视图函数(views.py),因为此处仅处理简单的响应,不涉及到模型操作,直接使用一级视图即可
from rest_framework import status from re import Response from re import APIView from qiniu import Auth from django.conf import settings class QiniuTokenAPIView(APIView): """ 获取七牛上传文件token """ def get(request): q = Au, ) token = q.upload_token) return Response({'token': token, 'domain': }, status=)
  • 使用API接口工具访问测试

至此,后端API接口开发完成,短短几行代码,轻松而愉快的完成了后端的开发。接来下才是整个项目最核心的部分,苦逼的前端工程师上岗了。

六、前端-上传组件开发

1. 上传组件分析

七牛对象存储支持多种多样的类型文件上传,虽然官方提供了详细的demo示例,但是在实际开发使用过程中,为了便于多个不同项目的移植和以及vue组件调用,因此将其封装为js的模块,当需要调用使用七牛的对象存储服务上传文件时,只需要传入上传文件的路径和文件对象即可。函数在执行时,先请求后端API接口,获取本次上传文件的token和domain,并提取文件名加入时间戳,避免同一时间传入多张图片导致文件名冲突,最后调用七牛Javascript-SDK实现文件上传,并返回成功上传的文件URL地址。详细说明请参考官方文档:

2. 上传组件代码实现

  • API请求封装,具体请参考以前发布的文章
import index from './index' // 获取七牛图片上传token export function getQiNiuToken() { return index.get('public/qiniuToken/') }
  • 七牛文件上传模块
import * as qiniu from "qiniu-js"; import {getQiNiuToken} from "@/api/public"; function qiniuUpload() { //file是选择的文件对象 const upload = (dir, file) => { return new Promise((resolve, reject) => { getQiNiuToken().then((response) => { let domain = re let token = re let key = dir + '/' + (0, ('.')) + '-' + new Date().getTime() + (('.')) let config = { useCdnDomain: true, //表示是否使用 cdn 加速域名,为布尔值,true 表示使用,默认为 false。 region: qiniu.region.z1 // 根据具体提示修改上传地区,当为 null 或 undefined 时,自动分析上传域名区域 } let putExtra = { fname: "", //文件原文件名 params: {}, //用来放置自定义变量 mimeType: null //用来限制上传文件类型,为 null 时表示不对文件类型限制;限制类型放到数组里: ["image/png", "image/jpeg", "image/gif"] }; const observable = qiniu.upload(file, key, token, putExtra, config) ob({ next: (result) => { //主要用来展示进度 con(result) }, error: (error) => { //上传错误后触发 con(error); reject(error) }, complete: (result) => { //上传成功后触发。包含文件地址。 let url = domain + re // con(url) resolve(url) }, }); }).catch(response => { //发生错误时执行的代码 con(response) }); }) } return { upload } } export default qiniuUpload

七、前端-裁剪组件开发

1. 裁剪模块分析

用户上传图片并镜像预览裁剪操作在多个页面中都会使用到,因此非常有必要将它封装成一个公共的子组件。其他页面使用这个组件时,图片上传地址、图片宽度、图片高度都不尽相同。因此将这个三个值设为子组件的参数变量,当用户完成图片裁剪后,点击上传时,调用上传组件,并给父组件传递success事件,并包含最终图片的URL地址参数。

2. 裁剪组件代码实现

裁剪组件基于element-plus(参考地址:)和vue-cropper(参考地址:)二次封装实现

  • 用户图片裁剪组件(U)
<template> <div> <el-upload accept=".jpg,.jpeg,.png" action="./" :auto-upload="false" :on-change="uploadChange" :show-file-list="false" > <el-button class="upload-btn"> <MyIcon class="upload-icon" type="icon-upload-img"/> <p>选择图片</p> </el-button> </el-upload> <el-dialog title="图片裁剪" v-model="showCopper" append-to-body center> <div class="cropper" v-loading="loading" element-loading-text="图片上传中..."> <span class="cropper-area"> <vueCropper ref="cropper" :img="cropImg" :autoCrop="true" :autoCropWidth="; :autoCropHeight="; :fixedNumber="[]" :fixed="true" @realTime="realTime" ></vueCropper> </span> <span class="preview-area"> <p>图片预览</p> <div class="show-preview"> <div :style="; class="preview"> <img :src="; :style=";> </div> </div> </span> </div> <template #footer> <el-button size="medium" type="success"> <label class="pointer" for="uploads">更换图片</label> </el-button> <input type="file" id="uploads" style="position:absolute; clip:rect(0 0 0 0);" accept="image/png, image/jpeg, image/jpg" @change="uploadChange($event)"> <el-button-group class="cropper-btn-group"> <el-button size="medium" type="primary" plain @click="changeScale(1)"> <MyIcon type="icon-amplification"/> </el-button> <el-button size="medium" type="primary" plain @click="changeScale(-1)"> <MyIcon type="icon-narrow"/> </el-button> <el-button size="medium" type="primary" plain @click="changeReset()"> <MyIcon type="icon-reset"/> </el-button> <el-button size="medium" type="primary" plain @click="changeRotate(1)"> <MyIcon type="icon-clockwise-sense"/> </el-button> <el-button size="medium" type="primary" plain @click="changeRotate(-1)"> <MyIcon type="icon-clockwise-dirction"/> </el-button> </el-button-group> <el-button size="medium" @click="showCopper=false">取 消</el-button> <el-button type="primary" @click="confirmFn" size="medium">确 定</el-button> </template> </el-dialog> </div> </template> <script setup> import {reactive, ref} from 'vue' import icon from "@/utils/icon"; import timeFormat from "@/utils/timeFormat"; import 'vue-cropper/di; import {VueCropper} from "vue-cropper"; import qiniuUpload from "@/utils/qiniuUpload"; import {ElMessage} from 'element-plus' let {MyIcon} = icon() // 格式化处理时间 let {timeFile} = timeFormat() // 七牛图片上传 let {upload} = qiniuUpload() const props = defineProps({ // 图片宽度 width: { type: Number, required: false, default: 200 }, // 图片高度 height: { type: Number, required: false, default: 200 }, // 图片保存目录 dir: { type: String, required: true, default: 'upload' } }) // 定义事件(子组件向父组件传参) const emit = defineEmits(['saveImg']); // 图像裁剪组件对象 const cropper = ref(null); // 裁剪后的图片文件 const cropImg = ref(''); // 图片裁剪对话框是否显示 const showCopper = ref(false); // 文件上传组件选取图片事件 const uploadChange = (file) => { let fileObj if ('raw' in file) { con("element对象") fileObj = } else { con("原生对象") fileObj = [0] } const reader = new FileReader(); reader.onload = (event) => { cro = event.; }; reader.readAsDataURL(fileObj) = true; } // 图片裁剪预览数据 const previews = reactive({}) // 图片裁剪预览事件 const realTime = (data) => { Object.assign(previews, data) } // 图片裁剪缩放事件 const changeScale = (num) => { num = num || 1 cro(num) } // 图片裁剪旋转事件 const changeRotate = (num) => { if (num === 1) { cro() } else { cro() } } // 图片裁剪重置事件 const changeReset = () => { cro() } // 文件上传动画状态 const loading = ref(false) // 图片裁剪完成上传事件 const confirmFn = () => { // 获取blob对象 cro(blobData => { con(blobData) loading.value = true //blob转file const file = new File([blobData], timeFile()) + '.jpg', {type: blobDa}); con(file) upload, file).then((response) => { con(response) ElMessage({ message: '图片上传成功!', type: 'success', }) emit('saveImg', response) = false loading.value = false }).catch(response => { //发生错误时执行的代码 con(response) ElMe('图片上传失败!') loading.value = false }); }) } </script> <style scoped lang="scss"> .upload-btn { .upload-icon { font-size: 24px; color: $color-text-secondary; vertical-align: -7 px !important; margin-right: 5px; } p { display: inline-block; vertical-align: 4px; } } .cropper { display: flex; height: 50vh; .cropper-area { flex: 2; } .preview-area { flex: 1; margin-left: 20px; p { text-align: center; margin-bottom: 20px; } .show-preview { flex: 1; -webkit-flex: 1; display: flex; display: -webkit-flex; justify-content: center; -webkit-justify-content: center; .preview { overflow: hidden; border-radius: 50%; border: 1px solid #cccccc; background: #cccccc; } } } } .cropper-btn-group { margin: 0 40px; .anticon { font-size: 18px; } } </style>
  • 其他vue页面调用图片上传组件时,传入裁剪完成后图片的宽度,高度,以及文件上传目录。当完成上传操作后,图片裁剪组件会返回一个上传完成事件,并携带图片URL地址。
<template> <div class="page"> <div class="animate__animated animate__zoomIn"> <el-card> <template #header> <span class="card-title no-choose"><MyIcon type="icon-form-color"/> 申请表单</span> </template> <div> <el-form ref="linkFormRef" :model="linkForm" label-width="120px" :rules="rules"> <el-form-item label="网站名称" prop="name"> <el-input v-model="linkForm.name"></el-input> </el-form-item> <el-form-item label="网站地址" prop="url"> <el-input v-model="linkForm.url" placeholder="请输入完整地址,https://开头"></el-input> </el-form-item> <el-form-item label="网站简介" prop="describe"> <el-input v-model="linkForm.describe"></el-input> </el-form-item> <el-form-item label="网站logo" prop="logo"> <span v-if="linkForm.logo===''"> <UploadImg :width="150" :height="150" :dir="'logo'" @saveImg="saveImg"></UploadImg> </span> <span v-else><el-avatar :size="100" :src="linkForm.logo"></el-avatar></span> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">提交</el-button> <el-button @click="reset">重置</el-button> </el-form-item> </el-form> </div> </el-card> </div> </div> </template> <script setup> import UploadImg from "@/components/common/U" import {onMounted, reactive, ref} from "vue"; import {getSiteConfig, postLink} from "@/api/management"; import icon from "@/utils/icon"; import {ElMessage} from "element-plus"; let {MyIcon} = icon() // 图片上传成功事件 const saveImg = (url) => { con(url) linkForm.logo = url } // 提交友链表单对象 const linkFormRef = ref(null) // 提交友链表单 const linkForm = reactive({ url: '', name: '', describe: '', logo: '', }) // 表单验证规则 const rules = { url: [{required: true, message: '请输入网站地址', trigger: 'blur',}], name: [{required: true, message: '请输入网站名称', trigger: 'blur',}], describe: [{required: true, message: '请输入网站描述', trigger: 'blur',}], logo: [{required: true, message: '请上传网站logo', trigger: 'blur',}], } // 提交表单事件 const onSubmit = () => { con('submit!') linkFormRef.value.validate((valid) => { if (valid) { postLink(linkForm).then((response) => { con(response) ElMessage({ message: '友链申请提交成功,请耐心等待审核!', type: 'success', }) linkForm.url = '' linkForm.name = '' linkForm.describe = '' linkForm.logo = '' }).catch(response => { //发生错误时执行的代码 con(response) for (let i in response) { ElMe(response[i][0]) } }); } }) } // 重置表单 const reset = () => { linkFormRef.value.resetFields() } onMounted(() => { siteConfigData() }) </script> <style scoped lang="scss"> .demo { margin: 15px 0 } .point-text { line-height: 30px; color: $color-text-primary; } </style>

八、功能验证与演示

一切准备就绪,接下来演示图片上传效果,也可查看在线地址查看效果

1. 图片上传前

  • 表单显示上传组件按钮

2. 图片上传中

  • 选择添加本地图片并调整尺寸

3. 图片上传后

  • 调用七牛上传组件SDK,完成图片上传,并返回图片URL地址

至此,整个用户图片上传流程开发完成!

更多运维开发相关文章,欢迎访问崔亮的博客

1.《详解用户图片上传流程》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《详解用户图片上传流程》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/gl/3117040.html