近期在研发新版博客网站时,有几个页面需要运用照片上传功能。全部项日前端基于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 qiniuUpload7、前端-裁剪组件研发
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
|