最近在开发新版博客网站时,有几个页面需要使用图片上传功能。整个项目前端基于vue3的element-plue和vue-cropper组件库封装一个图片上传组件,后端使Django REST framework开发api接口,存储使用七牛对象存储,以及腾讯CDN加速,总结了完整的前后端代码以及运维配置,以供大家参考。
1. 整体流程图
2. 流程分析
用户图片上传以及显示整个过程可分为以下几个阶段:
当用户访问http://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. 开发需求分析
本案例使用如今最流行的前后端分离开发模式。
4. 运维配置分析
本案例使用主流的企业网站项目配置,使用公有云的OSS对象存储服务、CDN内容分发网络以及DNS域名解析。此处选用七牛云对象存储和腾讯云CDN以及阿里云的域名解析,其他公有云厂商产品名称和配置项可能略有差异,但基本原理都是一样的,操作步骤也并无差别。
没有注册的小伙伴们可以使用以下链接进行注册
此处使用七牛对象存储,有10G免费空间,对于一般小业务场景完全够用。
1. 创建云存储空间
登录七牛云——>进入控制台——>点击对象存储——>然后点击新建空间
填写表单完成存储空间的创建,完后后七牛云会自动为我分配一个测试域名,这样我们就可以使用这个域名进行上传/下载文件了。
需要注意的是:测试域名只能使用30天!!并且测试域名只能使用HTTP协议,不支持HTTPS协议
2. 对象存储服务绑定域名
因为我已经购买过域名cuiliangblog.cn。所以接下来绑定oss.cuiliangblog.cn给这个存储空间即可。需要注意的是,虽然七牛云的对象存储服务免费,但是CDN加速服务是收费的,我已经够买过腾讯CDN服务,所以此处配置了自定义源站域名,大家可以按照自己的实际情况选择最合适的配置。
1. 添加CDN加速域名
登录腾讯云——>控制台——>CDN内容分发网络——>域名管理——>添加域名
2. 配置回源及缓存等策略
此处以我的oss.cuiliangblog.cn对象存储域名举例,其中回源策略填写七牛云对象存储的CNAME。
1. 添加域名解析记录
登录阿里云——>控制台——>域名——>解析
2. 访问验证
至此,存储服务和CDN以及DNS配置已全部完成,接下来做一个简单的测试
经过访问测试,上传图片后生成的外链可以正常打开访问,且远程地址为腾讯云CDN加速节点。到这儿,运维的工作已经完成了,接下来角色转换,现在是一位专业的后端开发工程师。
1. 后端功能模块概述
要想使用七牛的对象存储服务上传文件,就需要在后端通过七牛SDK生成的一个安全凭证,只有客户端拿着这个上传凭证上传文件才是有效的,否则七牛服务器是不接受的。七牛云的开发者中心提供了很多版本的SDK,例如Go,Javascript,PHP,Python,Node.js,Ruby,C,C/C++等等,这里是我使用的是python的SDK,详见开发者文档:http://developer.qiniu.com/kodo/1242/python
2. 获取accessKey和secretKey
通过查看开发者文档可知,调用SDK需要传入bucket、accessKey、secretKey三个参数
bucket的值就是存储空间的名称,accessKey和secretKey可以将鼠标悬浮在右上角的头像上然后点击密钥管理,然后创新密钥
3. DRF项目相关功能代码实现
pip install qiniu
七牛OSS存储配置
QINIU_AK = &39;XXXXXXXXXXXXX&39;
QINIU_SK = &39;XXXXXXXXXXXXX&39;
QINIU_BUCKET = &39;cuiliangoss&39;
QINIU_DOMAIN = &39;http://oss.cuiliangblog.cn/&39;
from django.urls import path
from rest_framework import routers
from public import views
app_name = &34;public&34;
urlpatterns = [
path(&39;qiniuToken/&39;, views.QiniuTokenAPIView.as_view()),
获取七牛上传token
…………
]
router = routers.DefaultRouter()
urlpatterns += router.urls
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from qiniu import Auth
from django.conf import settings
class QiniuTokenAPIView(APIView):
&34;&34;&34;
获取七牛上传文件token
&34;&34;&34;
def get(request):
q = Auth(settings.QINIU_AK, settings.QINIU_SK)
token = q.upload_token(settings.QINIU_BUCKET)
return Response({&39;token&39;: token, &39;domain&39;: settings.QINIU_DOMAIN}, status=status.HTTP_200_OK)
至此,后端API接口开发完成,短短几行代码,轻松而愉快的完成了后端的开发。接来下才是整个项目最核心的部分,苦逼的前端工程师上岗了。
1. 上传组件分析
七牛对象存储支持多种多样的类型文件上传,虽然官方提供了详细的demo示例,但是在实际开发使用过程中,为了便于多个不同项目的移植和以及vue组件调用,因此将其封装为js的模块,当需要调用使用七牛的对象存储服务上传文件时,只需要传入上传文件的路径和文件对象即可。函数在执行时,先请求后端API接口,获取本次上传文件的token和domain,并提取文件名加入时间戳,避免同一时间传入多张图片导致文件名冲突,最后调用七牛Javascript-SDK实现文件上传,并返回成功上传的文件URL地址。详细说明请参考官方文档:http://developer.qiniu.com/kodo/1283/javascript
2. 上传组件代码实现
import index from &39;./index&39;
// 获取七牛图片上传token
export function getQiNiuToken() {
return index.get(&39;public/qiniuToken/&39;)
}
import * as qiniu from &34;qiniu-js&34;;
import {getQiNiuToken} from &34;@/api/public&34;;
function qiniuUpload() { //file是选择的文件对象
const upload = (dir, file) => {
return new Promise((resolve, reject) => {
getQiNiuToken().then((response) => {
let domain = response.domain
let token = response.token
let key = dir + &39;/&39; + file.name.substring(0, file.name.lastIndexOf(&39;.&39;)) + &39;-&39; + new Date().getTime()
+ file.name.substring(file.name.lastIndexOf(&39;.&39;))
let config = {
useCdnDomain: true, //表示是否使用 cdn 加速域名,为布尔值,true 表示使用,默认为 false。
region: qiniu.region.z1 // 根据具体提示修改上传地区,当为 null 或 undefined 时,自动分析上传域名区域
}
let putExtra = {
fname: &34;&34;, //文件原文件名
params: {}, //用来放置自定义变量
mimeType: null //用来限制上传文件类型,为 null 时表示不对文件类型限制;限制类型放到数组里: [&34;image/png&34;, &34;image/jpeg&34;, &34;image/gif&34;]
};
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) => {
//上传成功后触发。包含文件地址。
let url = domain + result.key
// console.log(url)
resolve(url)
},
});
}).catch(response => {
//发生错误时执行的代码
console.log(response)
});
})
}
return {
upload
}
}
export default qiniuUpload
1. 裁剪模块分析
用户上传图片并镜像预览裁剪操作在多个页面中都会使用到,因此非常有必要将它封装成一个公共的子组件。其他页面使用这个组件时,图片上传地址、图片宽度、图片高度都不尽相同。因此将这个三个值设为子组件的参数变量,当用户完成图片裁剪后,点击上传时,调用上传组件,并给父组件传递success事件,并包含最终图片的URL地址参数。
2. 裁剪组件代码实现
裁剪组件基于element-plus(参考地址:http://github.com/element-plus/element-plus)和vue-cropper(参考地址:http://github.com/xyxiao001/vue-cropper)二次封装实现
选择图片
图片预览
取 消
firm/iFn&34; size=&34;medium&34;>确 定
cript setup>
import {reactive, ref} from &39;vue&39;
import icon from &34;@/utils/icon&34;;
import timeFormat from &34;@/utils/timeFormat&34;;
import &39;vue-cropper/dist/index.css&39;
import {VueCropper} from &34;vue-cropper&34;;
import qiniuUpload from &34;@/utils/qiniuUpload&34;;
import {ElMessage} from &39;element-plus&39;
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: &39;upload&39;
}
})
// 定义事件(子组件向父组件传参)
const emit = defineEmits([&39;saveImg&39;]);
// 图像裁剪组件对象
const cropper = ref(null);
// 裁剪后的图片文件
const cropImg = ref(&39;&39;);
// 图片裁剪对话框是否显示
const showCopper = ref(false);
// 文件上传组件选取图片事件
const uploadChange = (file) => {
let fileObj
if (&39;raw&39; in file) {
console.log(&34;element对象&34;)
fileObj = file.raw
} else {
console.log(&34;原生对象&34;)
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()) + &39;.jpg&39;, {type: blobData.type});
console.log(file)
upload(props.dir, file).then((response) => {
console.log(response)
ElMessage({
message: &39;图片上传成功!&39;,
type: &39;success&39;,
})
emit(&39;saveImg&39;, response)
showCopper.value = false
loading.value = false
}).catch(response => {
//发生错误时执行的代码
console.log(response)
ElMessage.error(&39;图片上传失败!&39;)
loading.value = false
});
})
}
cript>
申请表单
inkFormRef&34; :model=&34;linkForm&34; label-width=&34;120px&34; :rules=&34;rules&34;>
inkForm.name&34;>
inkForm.url&34; placeholder=&34;请输入完整地址,http://开头&34;>
inkForm.describe&34;>
inkForm.logo===&39;&39;&34;>
提交
重置
cript setup>
import UploadImg from &34;@/components/common/UploadImg.vue&34;
import {onMounted, reactive, ref} from &34;vue&34;;
import {getSiteConfig, postlink} from &34;@/api/management&34;;
import icon from &34;@/utils/icon&34;;
import {ElMessage} from &34;element-plus&34;;
let {MyIcon} = icon()
// 图片上传成功事件
const saveImg = (url) => {
console.log(url)
linkForm.logo = url
}
// 提交友链表单对象
const linkFormRef = ref(null)
// 提交友链表单
const linkForm = reactive({
url: &39;&39;,
name: &39;&39;,
describe: &39;&39;,
logo: &39;&39;,
})
// 表单验证规则
const rules = {
url: [{required: true, message: &39;请输入网站地址&39;, trigger: &39;blur&39;,}],
name: [{required: true, message: &39;请输入网站名称&39;, trigger: &39;blur&39;,}],
describe: [{required: true, message: &39;请输入网站描述&39;, trigger: &39;blur&39;,}],
logo: [{required: true, message: &39;请上传网站logo&39;, trigger: &39;blur&39;,}],
}
// 提交表单事件
const onSubmit = () => {
console.log(&39;submit!&39;)
linkFormRef.value.validate((valid) => {
if (valid) {
postlink(linkForm).then((response) => {
console.log(response)
ElMessage({
message: &39;友链申请提交成功,请耐心等待审核!&39;,
type: &39;success&39;,
})
linkForm.url = &39;&39;
linkForm.name = &39;&39;
linkForm.describe = &39;&39;
linkForm.logo = &39;&39;
}).catch(response => {
//发生错误时执行的代码
console.log(response)
for (let i in response) {
ElMessage.error(response[i][0])
}
});
}
})
}
// 重置表单
const reset = () => {
linkFormRef.value.resetFields()
}
onMounted(() => {
siteConfigData()
})
cript>
一切准备就绪,接下来演示图片上传效果,也可查看在线地址查看效果http://www.cuiliangblog.cn/applylink
1. 图片上传前
2. 图片上传中
3. 图片上传后
至此,整个用户图片上传流程开发完成!
更多运维开发相关文章,欢迎访问崔亮的博客 http://www.cuiliangblog.cn