在前几篇文章中,我们主要讲了开发Web平台之环境准备、登录认证、用户信息管理、接口文档、图书信息、图书下架,图书修改。
至此我们图书系统的CURD中的URD功能已初步实现,只剩下C也就是新增功能。因此,本篇文章将完成我们这个系列的最后一个功能,图书新增功能的实现。
如上文所说,我们这个系列的教程不会教你搭建一个完完整整的系统,但会将最基本的方法教给你,掌握了这些方法,你可以在此基础上扩展更多的功能。我们先来看一下这篇文章要实现的内容:
1.图片存储至阿里云OSS
2.豆瓣图书新增功能的后端实现
3.豆瓣图书新增功能的前端实现
4.打包
新增功能需要的填写字段
如果我们要完成豆瓣图书的新增功能,那么我们要在新增页面填写哪些内容呢?至少前端页面上的这些信息是要具备的:
title 书名
score 评分
img_src 图片链接
publish_detail 出版信息
slogan 简介
其他的信息可以在前端通过输入框输入,图片信息是一个链接,当然你也可以输入一个网上现成的图片链接,但是还是做成上传图片的形式比较好,因为这样相对比较方便,直接上传图片就行,不用费力去网上找链接。
那问题来了?上传的图片要存到哪里?其实如果只是你自己访问,那么这个网站的任何静态资源都可以存储在本地,如果要开放给其他人访问,那么最好这些图片信息能放在一个大家都能访问到的地方,阿里云存储对象OSS服务给我们提供了这样一个API服务,你可以上传图片到阿里云,然后它会给你返回图片的链接。
首先你要申请一个阿里云账号,然后实名认证后,开通阿里云OSS服务。这个服务一年费用相对比较便宜,网上也有各种操作教程,因此这里暂时不做操作演示。
1.进入工作台,创建Bucket,Bucket名称可以随便取,只要不重复就像,读取权限使用公开读,其他采用默认就行。
2.创建成功之后,你会看到阿里云给我们提供了一个外网访问的地域节点Endpoint和Bucket名字,一会我们会用到。
3.点击左侧的文件管理,会发现文件管理里是空的,什么都没有,因为还没有存放图片。
通过网址:https://help.aliyun.com/document_detail/88426.html,打开对象存储OSS的API,它给我们提供的上传本地文件的方法,写得非常详细。
在工作台上方点头像,进入AccessKey管理。如果你已经创建过AccessKey,直接点击查看Secret就可以,如果没有创建,先创建一个AccessKey,创建好之后就可以查看AccessKey ID和AccessKey Secret。
安装oss2:
pip install oss2
在backend目录下的utils目录下,创建一个aliyun_oss的py文件,写入以下内容:
import oss2
import os
class AliyunOss(object):
def __init__(self):
#access_key
self.access_key_id = "xxxxxx"
self.access_key_secret = "xxxxxx"
self.auth = oss2.Auth(self.access_key_id, self.access_key_secret)
#bucket name
self.bucket_name = "book-image-store"
#endpoint
self.endpoint = "oss-cn-hangzhou.aliyuncs.com"
self.bucket = oss2.Bucket(self.auth, self.endpoint, self.bucket_name)
#定义本地上传图片的方法:name表示上传到阿里云OSS后显示的图片名字,file表示图片的路径
def put_object_from_file(self, name, file):
self.bucket.put_object_from_file(name, file)
img_src = "https://{}.{}/{}".format(self.bucket_name, self.endpoint, name)
return img_src
if __name__ == '__main__':
aliyunoss = AliyunOss()
img_src = aliyunoss.put_object_from_file("katong.jpeg", r"C:\Users\beck\Desktop\a51034ceb9be6492bf75fbb18bdfa5a8.jpeg")
print(img_src)
运行之后,发现阿里云有了图片,并且返回了一个图片链接https://book-image-store.oss-cn-hangzhou.aliyuncs.com/katong.jpeg。
新的问题:如果每次在脚本里把图片名固定为katong.jpeg,那么传多张相同图片的时候会无法区分,因此,我们需要将name设置为可变的,即使是同一张图片传多次,也能保证图片名不会重复。
这里我们可以使用os.path.basename方法拿到图片名,然后给它拼接一个uuid,可以思考一下,这里为什么要用方法self.bucket.put_object,而不是self.bucket.put_object_from_file?
def put_object_from_file(self, file):
name = str(uuid.uuid1()) + os.path.basename(str(file))
self.bucket.put_object(name, file)
img_src = "https://{}.{}/{}".format(self.bucket_name, self.endpoint, name)
return img_src
可以看到后面的图片名是一样的,只是前面的uuid不同。
在book/views.py中单独定义一个上传方法,这个方法是post请求,它的内部直接调用utils/aliyun_oss中的AliyunOss类,将图片上传到阿里云后,将图片地址返回。
由于上传图片的方法,ModelViewSet中没有提供,因此需要自定义action,相当于除了ModelViewSet中提供的最基本的CURD接口外,我们可以自定义需要的接口。
定义完成之后,可以在swagger中查看一下。一般来说,action定义的接口,如果没有指定url,那么它的方法名会作为url的一部分。
在前端开始之前,我们先了解一下我们需要的样式。
如果要新增一个功能,我们前端肯定要写成表单的形式,表单内可以输入内容,表单下方有提交功能,同时表格内还要嵌入上传图片的组件,当图片加载后,会通过调用后端上传图片的接口将返回的图片链接传给前端,当所有内容输入完成后,会通过提交按钮,将所有填写的内容传给封装好的新增方法,新增方法调用后端新增接口,完成图片的新增,最后,这个页面会跳转到图书展示页面。
因此,前端需要完成以下的任务:
前端页面的创建
前端上传图片公共组件的创建
前端封装公共方法
修改路由
先来看前端页面的创建,我们先看一个elemementui提供的form表单组件,地址:https://element.eleme.cn/#/zh-CN/component/form。这个是比较符合我们的情况。
再来看一个vue-element-admin提供的组件,这个也是比较符合我们对上传组件的要求。
接下来,新增views/book/create.vue,然后写入内容,需要说明一下的是:
1.SingleImage3 是上传图片的公共组件,你可以从vue-element-admin的src/components下将整个Upload目录全部复制到自己项目对应的目录下。
2.createDouBanBookAPI是我们预定义的封装好的新增方法,这个方法里面的this.$notify表示提交成功后出现一个成功的提示。
3.this.$router.push(‘/douban/book/’),表示提交成功后跳转到图书展示页面。
4.这一句中使用了@get_tempUrl来触发get_imgSrc来获得子组件SingleImage3 传给父组件的值,将该值最终传递给bookForm.img_src。
<template>
<div>
<el-form ref="form" :model="bookForm" label-width="80px" size="large" style="margin-top: 50px">
<el-form-item label="书 名">
<el-input v-model="bookForm.title" style="width: 300px"></el-input>
</el-form-item>
<el-form-item label="评 分">
<el-input v-model="bookForm.score" style="width: 300px"></el-input>
</el-form-item>
<el-form-item label="简 介">
<el-input v-model="bookForm.slogan" style="width: 300px"></el-input>
</el-form-item>
<el-form-item label="出版信息">
<el-input v-model="bookForm.publish_detail" style="width: 300px"></el-input>
</el-form-item>
<el-form-item label="上传封面" prop="img_src" style="margin-bottom: 30px;">
<SingleImage3 v-model="bookForm.img_src" @get_tempUrl="get_imgSrc" />
</el-form-item>
<el-form-item size="large">
<el-button type="primary" @click="onSubmit">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import SingleImage3 from '@/components/Upload/SingleImage3'
import * as DouBanBook from '@/api/douban-book'
export default {
name: 'create',
components: { SingleImage3 },
data() {
return {
bookForm: {
title: '',
score: '',
slogan: '',
publish_detail: '',
img_src: '',
}
};
},
methods: {
onSubmit() {
DouBanBook.createDouBanBookAPI(this.bookForm)
.then(res => {
this.$notify({
title: 'Success',
message: '提交成功!',
type: 'success',
duration: 2000
})
this.$router.push('/douban/book/')
})
},
get_imgSrc(data) {
this.bookForm.img_src = data
}
}
};
</script>
components下的Upload目录是从中复制下来的,但需要做一些修改:
修改点-1:将beforeUpload中原来的获得key, token等方法去掉,换成限制图片大大小、类型。
修改点-2:通过action直接跟着后端的接口地址,容易引起跨域的问题,因此需要通过http-request来请求后端上传图片的接口。
修改点-3:封装上传图片的方法,方法中通过调用后端接口,来实现图片的上传,将后端接口返回的图片链接通过this.$emit传给父组件。
<template>
<div class="upload-container">
<el-upload
:multiple="false"
:show-file-list="false"
:on-success="handleImageSuccess"
class="image-uploader"
drag
action= ''
//修改点-2
:http-request="(param) => uploadDouBanBookImage(param)"
>
<i class="el-icon-upload" />
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
</el-upload>
<div class="image-preview image-app-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl">
<div class="image-preview-action">
<i class="el-icon-delete" @click="rmImage" />
</div>
</div>
</div>
<div class="image-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl">
<div class="image-preview-action">
<i class="el-icon-delete" @click="rmImage" />
</div>
</div>
</div>
</div>
</template>
<script>
import * as DouBanBook from '@/api/douban-book'
export default {
name: 'SingleImageUpload3',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
tempUrl: '',
}
},
computed: {
imageUrl() {
return this.value
}
},
methods: {
rmImage() {
this.emitInput('')
},
emitInput(val) {
this.$emit('input', val)
},
handleImageSuccess(file) {
this.emitInput(file.files.file)
},
//修改点-1
beforeUpload(file) {
const isPic = file.type.indexOf('image') >= 0;
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isPic) {
this.$message.error('上传的文件只能为图片格式!')
}
if (!isLt2M) {
this.$message.error('上传图片大小不能超过 2MB!')
}
},
//修改点-3
uploadDouBanBookImage(param) {
const formData = new FormData()
formData.append('file', param.file)
console.log(param.file)
DouBanBook.uploadDouBanBookImageAPI(formData)
.then(response => {
console.log('上传图片成功')
this.value = response.data.img_src
this.tempUrl = this.value
this.$emit('get_tempUrl', this.tempUrl)
})
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
.upload-container {
width: 100%;
position: relative;
@include clearfix;
.image-uploader {
width: 35%;
float: left;
}
.image-preview {
width: 200px;
height: 200px;
position: relative;
border: 1px dashed #d9d9d9;
float: left;
margin-left: 50px;
.image-preview-wrapper {
position: relative;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
.image-preview-action {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, .5);
transition: opacity .3s;
cursor: pointer;
text-align: center;
line-height: 200px;
.el-icon-delete {
font-size: 36px;
}
}
&:hover {
.image-preview-action {
opacity: 1;
}
}
}
.image-app-preview {
width: 320px;
height: 180px;
position: relative;
border: 1px dashed #d9d9d9;
float: left;
margin-left: 50px;
.app-fake-conver {
height: 44px;
position: absolute;
width: 100%; // background: rgba(0, 0, 0, .1);
text-align: center;
line-height: 64px;
color: #fff;
}
}
}
</style>
封装上传图片方法和新增图片方法,这两个方法都是post请求。
export function uploadDouBanBookImageAPI(data) {
return request({
url: '/douban/book/upload/',
method: 'post',
data
})
}
export function createDouBanBookAPI(data) {
return request({
url: '/douban/book/',
method: 'post',
data
})
}
在router/index.js中,创建二级路由,二级路由下有两个页面,/douban/book对应的是图书展示页面,/douban/create对应的是图书新增页面。
{
path: '/douban',
component: Layout,
name: 'douban',
meta: { title: '豆瓣图书', icon: 'dashboard' },
children: [
{
path: 'book',
component: () => import('@/views/book/index'),
name: 'book',
meta: { title: '图书列表', icon: 'form', affix: true }
},
{
path: 'create',
component: () => import('@/views/book/create'),
name: 'create',
meta: { title: '图书创建', icon: 'form', affix: true }
}
]
},
我们在前端上传一部《新大学英语》,填写完字段,上传封面。
创建成功后,可以看到已经跳转到图书列表页,并且在图书列表的最后一条信息就是我们刚刚创建的。
前后端分离开发完成后,最终要把项目的代码合在一起。因此需要对前端项目进行打包,前端打包后需要在后端做相应的配置,这样最终才能使得外部通过后端服务来访问图书系统。
1.打包前将.env.production中的VUE_APP_BASE_API改为http://192.168.1.13:8000(192.168.1.13是我的ip,你的可能不一样),注意不要使用127.0.0.1:8000,否则同一个局域网的其他用户会无法访问后端服务。
2.使用命令npm run build:prod或者在前端服务里做相应的配置,在Scripts选项下选择build:prod,最后点击Apply。
3.打包完成后,前端项目下会生成一个dist目录,将这个目录全部拷贝到后端项目下。
1.在settings.py中配置模板文件和静态文件的地址,并且将ALLOWED_HOSTS修改为*,允许所有ip访问。
ALLOWED_HOSTS = ['*']
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ os.path.join(BASE_DIR, 'dist') ],
}
]
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'dist/static'),
]
2.在父路由urls.py中配置html的路由。
urlpatterns = [
path('', TemplateView.as_view(template_name='index.html')),
]
3.配置后端服务的Host为0.0.0.0,Port为8000,这样局域网的其他用户才能访问。
在同一个局域网的其他电脑的浏览器中输入后端服务地址:192.168.1.13:8000,发现可以正常访问。
不知不觉,这个系列已经写了八篇文章,在这些文章里我们从搭建环境做起,到解决登录问题,然后依次实现了图书信息的CURD(增删改查),最后完成了打包部署,就如前文说的,掌握了这些基本的方法,你可以扩展更多更丰富的功能出来。
也许有人会说,测试学这些干什么。当然,这些东西在工作中可能用不到,但却可以成为面试找工作的“敲门砖”。
在如今越来越卷的时代,多学一点知识,不要只局限于测试,也许某一天,你觉得开发有意思,走开发的路子未尝不是一条新路,也许你刚好碰上vue + django的项目架构,你的这些知识无疑对于了解项目大有裨益,也许你还可以从"点点点"升级为自动化测试,甚至测试开发工程师,从而实现升职加薪。
但这一切的前提是,你要比别人更有竞争力。还等什么,积极行动起来吧!当写完几个功能之后,你会发现web开发的妙处,当某个“疑难杂症”的问题突然迎刃而解之时,你会得到超乎寻常的快乐。
下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】