外链论坛

 找回密码
 立即注册
搜索
查看: 47|回复: 3

详解用户照片上传流程

[复制链接]

3046

主题

2万

回帖

9909万

积分

论坛元老

Rank: 8Rank: 8

积分
99098928
发表于 2024-8-14 20:00:22 | 显示全部楼层 |阅读模式

近期研发新版博客网站时,有几个页面必须运用照片上传功能。全部日前端基于vue3的element-plue和vue-cropper组件库封装一个照片上传组件,后端使Django REST framework研发api接口,存储运用七牛对象存储,以及腾讯CDN加速,总结了完整的前后端代码以及运维配置,以供大众参考。

1、流程与思路分析

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以及阿里云的域名解析,其他公有云厂商制品叫作和配置项可能略有差异,但基本原理都是同样的,操作过程并无差别。

注册的小伙伴们能够运用以下链接进行注册

七牛云:https://s.qiniu.com/VVfMnq阿里云:https://www.aliyun.com/1111/new?userCode=gs1gd34d腾讯云:https://curl.qcloud.com/yXdJdtu9

2、对象存储配置

此处运用七牛对象存储,有10G免费空间,针对通常小业务场景完全够用。

1. 创建云存储空间

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

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

必须重视的是:测试域名只能运用30天!!并且测试域名只能运用HTTP协议,不支持HTTPS协议

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

由于已然购买过域名cuiliangblog.cn。因此接下来绑定oss.cuiliangblog.cn给这个存储空间就可必须重视的是,虽然七牛云的对象存储服务免费,然则CDN加速服务是收费的,我已然够买过腾讯CDN服务,因此此处配置了自定义源站域名,大众能够根据自己的实质状况选取最合适的配置。

必须重视的是倘若运用第三方CDN服务,记住这个默认分配的CNAME,CDN配置回源策略时,填写这个CNAME。

3、CDN配置

1. 添加CDN加速域名

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

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

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

4、DNS配置

1. 添加域名解析记录

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

新增一条oss.cuiliangblog.cn的CNAME域名解析记录,记录值填写腾讯云CDN的CNAME值

2. 拜访验证

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

七牛云——>掌控台——>对象存储——>空间管理——>文件管理——>上传文件,随便选取一张照片资源上传
随便上传一张照片后,点击返回,查看照片资源外链
运用浏览器拜访验证域名解析是不是正常

经过拜访测试,上传照片后生成的外链能够正常打开拜访,且远程位置为腾讯云CDN加速节点。到这儿,运维的工作已然成为了,接下来角色转换,此刻是一位专业的后端研发工程师。

5、后端-token接口研发

1. 后端功能模块概述

想要运用七牛的对象存储服务上传文件,就必须在后端经过七牛SDK生成的一个安全凭证,仅有客户端拿着这个上传凭证上传文件才是有效的,否则七牛服务器是不接受的。七牛云的研发者中心供给非常多版本的SDK,例如Go,JavaScript,PHP,Python,Node.js,Ruby,C#,C/C++等等,这儿是我运用的是python的SDK,详见研发者文档:https://developer.qiniu.com/kodo/1242/python

2. 获取accessKey和secretKey

经过查看研发者文档可知,调用SDK必须传入bucket、accessKey、secretKey三个参数

bucket的值便是存储空间的名叫作,accessKey和secretKey能够将鼠标悬浮在右上角的头像上而后点击密钥管理,而后创新密钥

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

安装SDKpip install qiniusetting.py中存放密钥信息# 七牛OSS存储配置 QINIU_AK = XXXXXXXXXXXXX QINIU_SK = XXXXXXXXXXXXX QINIU_BUCKET = cuiliangoss QINIU_DOMAIN = https://oss.cuiliangblog.cn/配置请求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 = routers.DefaultRouter() urlpatterns += router.urls编写视图函数(views.py),由于此处仅处理简单的响应,不触及到模型操作,直接运用一级视图就可from rest_framework import status fromrest_framework.responseimport Response from rest_framework.views import APIView from qiniu import Auth from django.conf import settings class QiniuTokenAPIView(APIView): """ 获取七牛上传文件token """ def get(request):q = Auth(settings.QINIU_AK, settings.QINIU_SK) token = q.upload_token(settings.QINIU_BUCKET)return Response({token: token, domain: settings.QINIU_DOMAIN}, status=status.HTTP_200_OK)运用API接口工具拜访测试

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

6、前端-上传组件研发

1. 上传组件分析

七牛对象存储支持多种多样的类型文件上传,虽然官方供给仔细的demo示例,然则实质研发运用过程中,为了便于多个区别项目的移植和以及vue组件调用,因此呢将其封装为js的模块,当必须调用运用七牛的对象存储服务上传文件时,只必须传入上传文件的路径和文件对象就可。函数在执行时,先请求后端API接口,获取这次上传文件的token和domain,并提取文件名加入时间戳,避免同一时间传入多张照片引起文件名冲突,最后调用七牛JavaScript-SDK实现文件上传,并返回成功上传的文件URL位置仔细说明请参考官方文档:https://developer.qiniu.com/kodo/1283/javascript

2. 上传组件代码实现

API请求封装,详细请参考以前发布的文案https://www.cuiliangblog.cn/detail/article/12import 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) => { letdomain = response.domainlet token = response.token let key = dir + / + file.name.substring(0, file.name.lastIndexOf(.)) + - + new Date().getTime() + file.name.substring(file.name.lastIndexOf(.)) 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) observable.subscribe({ next: (result) => { //重点用来展示进度 console.log(result) }, error: (error) => { //上传错误后触发 console.log(error); reject(error) }, complete: (result) => { //上传成功后触发。包括文件位置 leturl = domain + result.key// console.log(url) resolve(url) }, }); }).catch(response => { //出现错误时执行的代码 console.log(response) }); }) } return{ upload } }export default qiniuUpload

7、前端-裁剪组件研发

1. 裁剪模块分析

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

2. 裁剪组件代码实现

裁剪组件基于element-plus(参考位置https://github.com/element-plus/element-plus)和vue-cropper(参考位置https://github.com/xyxiao001/vue-cropper)二次封装实现

用户照片裁剪组件(UploadImg.vue)<template> <div> <el-upload accept=".jpg,.jpeg,.png" action="./" :auto-upload="false" n-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="props.width" :autoCropHeight="props.height" :fixedNumber="[props.width/props.height,1]" :fixed="true" @realTime="realTime" ></vueCropper> </span> <span class="preview-area"> <p>照片预览</p> <div class="show-preview"> <div :style="previews.div" class="preview"> <img :src="previews.url" :style="previews.img"> </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/dist/index.css 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]); // 图像裁剪组件对象 constcropper = ref(null); // 裁剪后的照片文件 const cropImg = ref(); // 照片裁剪对话框是不是表示 const showCopper = ref(false); // 文件上传组件选择照片事件 const uploadChange = (file) => { let fileObj if (raw in file) { console.log("element对象") fileObj = file.raw } else { console.log("原生对象") fileObj = file.target.files[0] } const reader = new FileReader(); reader.onload = (event) =>{ cropImg.value = event.target.result; }; reader.readAsDataURL(fileObj) showCopper.value =true; } // 照片裁剪预览数据 const previews = reactive({}) // 照片裁剪预览事件 const realTime = (data) => { Object.assign(previews, data) }// 照片裁剪缩放事件 const changeScale = (num) => { num = num || 1 cropper.value.changeScale(num) } // 照片裁剪旋转事件 const changeRotate = (num) =>{if (num === 1) { cropper.value.rotateLeft() } else { cropper.value.rotateRight() } } // 照片裁剪重置事件 const changeReset = () => { cropper.value.refresh() } // 文件上传动画状态 const loading = ref(false) // 照片裁剪完成上传事件 const confirmFn = () => { // 获取blob对象cropper.value.getCropBlob(blobData => { console.log(blobData) loading.value = true //blob转file const file = new File([blobData], timeFile(Date.now()) + .jpg, {type: blobData.type});console.log(file) upload(props.dir, file).then((response) => { console.log(response) ElMessage({ message: 照片上传成功!, type: success, }) emit(saveImg, response) showCopper.value = false loading.value = false }).catch(response => { //出现错误时执行的代码 console.log(response) ElMessage.error(照片上传失败!) 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; bac公斤round: #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/UploadImg.vue" 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) => { console.log(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 = () => { console.log(submit!) linkFormRef.value.validate((valid) => { if (valid) { postLink(linkForm).then((response) => { console.log(response) ElMessage({ message: 友链申请提交成功,请耐心等待审核!, type: success, }) linkForm.url = linkForm.name = linkForm.describe = linkForm.logo = }).catch(response => { //出现错误时执行的代码 console.log(response)for (let i in response) { ElMessage.error(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>

8、功能验证与演示

一切准备就绪,接下来演示照片上传效果,可查看在线位置查看效果https://www.cuiliangblog.cn/applyLink

1. 照片上传前

表单表示上传组件按钮

2. 照片上传中

选取添加本地照片调节尺寸

3. 照片上传后

调用七牛上传组件SDK,完成照片上传,并返回照片URL位置

至此,全部用户照片上传流程研发完成!

更加多运维研发关联文案,欢迎拜访崔亮的博客 https://www.cuiliangblog.cn

回复

使用道具 举报

0

主题

844

回帖

1

积分

新手上路

Rank: 1

积分
1
发表于 2024-8-25 01:39:51 | 显示全部楼层
我完全同意你的观点,说得太对了。
回复

使用道具 举报

0

主题

1万

回帖

1

积分

新手上路

Rank: 1

积分
1
发表于 2024-9-2 11:27:43 | 显示全部楼层
你的见解独到,让我受益匪浅,期待更多交流。
回复

使用道具 举报

0

主题

1010

回帖

1

积分

新手上路

Rank: 1

积分
1
发表于 2024-9-9 22:44:19 | 显示全部楼层
“板凳”(第三个回帖的人)‌
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

站点统计|Archiver|手机版|小黑屋|外链论坛 ( 非经营性网站 )|网站地图

GMT+8, 2024-11-9 04:04 , Processed in 0.091299 second(s), 19 queries .

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.