• 手把手教你做测开:开发Web平台之图书新增


    在前几篇文章中,我们主要讲了开发Web平台之环境准备、登录认证、用户信息管理、接口文档、图书信息、图书下架,图书修改。

    至此我们图书系统的CURD中的URD功能已初步实现,只剩下C也就是新增功能。因此,本篇文章将完成我们这个系列的最后一个功能,图书新增功能的实现。

    如上文所说,我们这个系列的教程不会教你搭建一个完完整整的系统,但会将最基本的方法教给你,掌握了这些方法,你可以在此基础上扩展更多的功能。我们先来看一下这篇文章要实现的内容:

    1.图片存储至阿里云OSS

    2.豆瓣图书新增功能的后端实现

    3.豆瓣图书新增功能的前端实现

    4.打包

    图片存储至阿里云OSS

    新增功能需要的填写字段

    如果我们要完成豆瓣图书的新增功能,那么我们要在新增页面填写哪些内容呢?至少前端页面上的这些信息是要具备的:

    1. title 书名

    2. score 评分

    3. img_src 图片链接

    4. publish_detail 出版信息

    5. slogan 简介

    图片

    图片信息存储位置

    其他的信息可以在前端通过输入框输入,图片信息是一个链接,当然你也可以输入一个网上现成的图片链接,但是还是做成上传图片的形式比较好,因为这样相对比较方便,直接上传图片就行,不用费力去网上找链接。

    那问题来了?上传的图片要存到哪里?其实如果只是你自己访问,那么这个网站的任何静态资源都可以存储在本地,如果要开放给其他人访问,那么最好这些图片信息能放在一个大家都能访问到的地方,阿里云存储对象OSS服务给我们提供了这样一个API服务,你可以上传图片到阿里云,然后它会给你返回图片的链接。

    开通阿里云OSS服务

    首先你要申请一个阿里云账号,然后实名认证后,开通阿里云OSS服务。这个服务一年费用相对比较便宜,网上也有各种操作教程,因此这里暂时不做操作演示。

    创建Bucket

    1.进入工作台,创建Bucket,Bucket名称可以随便取,只要不重复就像,读取权限使用公开读,其他采用默认就行。

    图片

    2.创建成功之后,你会看到阿里云给我们提供了一个外网访问的地域节点Endpoint和Bucket名字,一会我们会用到。

    图片

    3.点击左侧的文件管理,会发现文件管理里是空的,什么都没有,因为还没有存放图片。

    图片

    查看API

    通过网址:https://help.aliyun.com/document_detail/88426.html,打开对象存储OSS的API,它给我们提供的上传本地文件的方法,写得非常详细。

    图片

    查看AccessKey

    在工作台上方点头像,进入AccessKey管理。如果你已经创建过AccessKey,直接点击查看Secret就可以,如果没有创建,先创建一个AccessKey,创建好之后就可以查看AccessKey ID和AccessKey Secret。

    图片

    图片

    豆瓣图书新增功能的后端实现

    编写脚本进行上传

    安装oss2:

    pip install oss2
    
    • 1

    在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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    运行之后,发现阿里云有了图片,并且返回了一个图片链接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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到后面的图片名是一样的,只是前面的uuid不同。

    图片

    修改book视图

    在book/views.py中单独定义一个上传方法,这个方法是post请求,它的内部直接调用utils/aliyun_oss中的AliyunOss类,将图片上传到阿里云后,将图片地址返回。

    由于上传图片的方法,ModelViewSet中没有提供,因此需要自定义action,相当于除了ModelViewSet中提供的最基本的CURD接口外,我们可以自定义需要的接口。

    定义完成之后,可以在swagger中查看一下。一般来说,action定义的接口,如果没有指定url,那么它的方法名会作为url的一部分。

    图片

    豆瓣图书新增功能的前端实现

    前端思路分析

    在前端开始之前,我们先了解一下我们需要的样式。

    如果要新增一个功能,我们前端肯定要写成表单的形式,表单内可以输入内容,表单下方有提交功能,同时表格内还要嵌入上传图片的组件,当图片加载后,会通过调用后端上传图片的接口将返回的图片链接传给前端,当所有内容输入完成后,会通过提交按钮,将所有填写的内容传给封装好的新增方法,新增方法调用后端新增接口,完成图片的新增,最后,这个页面会跳转到图书展示页面。

    因此,前端需要完成以下的任务:

    1. 前端页面的创建

    2. 前端上传图片公共组件的创建

    3. 前端封装公共方法

    4. 修改路由

    新增views/book/create.vue

    先来看前端页面的创建,我们先看一个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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    修改components/Upload/SingleImage3.vue

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168

    修改api/douban-book.js

    封装上传图片方法和新增图片方法,这两个方法都是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
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    修改router/index.js

    在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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    测试

    我们在前端上传一部《新大学英语》,填写完字段,上传封面。

    图片

    创建成功后,可以看到已经跳转到图书列表页,并且在图书列表的最后一条信息就是我们刚刚创建的。

    图片

    打包

    前后端分离开发完成后,最终要把项目的代码合在一起。因此需要对前端项目进行打包,前端打包后需要在后端做相应的配置,这样最终才能使得外部通过后端服务来访问图书系统。

    前端打包

    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'),
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.在父路由urls.py中配置html的路由。

     urlpatterns = [
        path('', TemplateView.as_view(template_name='index.html')),
    ]
    
    • 1
    • 2
    • 3

    3.配置后端服务的Host为0.0.0.0,Port为8000,这样局域网的其他用户才能访问。

    图片

    访问后端服务

    在同一个局域网的其他电脑的浏览器中输入后端服务地址:192.168.1.13:8000,发现可以正常访问。

    图片

    总结

    不知不觉,这个系列已经写了八篇文章,在这些文章里我们从搭建环境做起,到解决登录问题,然后依次实现了图书信息的CURD(增删改查),最后完成了打包部署,就如前文说的,掌握了这些基本的方法,你可以扩展更多更丰富的功能出来。

    也许有人会说,测试学这些干什么。当然,这些东西在工作中可能用不到,但却可以成为面试找工作的“敲门砖”。

    在如今越来越卷的时代,多学一点知识,不要只局限于测试,也许某一天,你觉得开发有意思,走开发的路子未尝不是一条新路,也许你刚好碰上vue + django的项目架构,你的这些知识无疑对于了解项目大有裨益,也许你还可以从"点点点"升级为自动化测试,甚至测试开发工程师,从而实现升职加薪。

    但这一切的前提是,你要比别人更有竞争力。还等什么,积极行动起来吧!当写完几个功能之后,你会发现web开发的妙处,当某个“疑难杂症”的问题突然迎刃而解之时,你会得到超乎寻常的快乐。


    资源分享

    下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】

    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    RSA加密原理与RSA公钥加密系统、数字签名
    苹果电脑双系统正确打开方式,虚拟机已经Out了
    面试之并查集
    shell:bash【Bourne-Again SHell】
    使用国内代理该如何开展网页抓取项目?
    【Java】图书管理系统,完整版+源代码!!!
    C语言,洛谷题,赦免战俘
    独立站如何有效突破独立站转化率瓶颈
    HTTP协议简单理解
    两年Java的面试经验
  • 原文地址:https://blog.csdn.net/wx17343624830/article/details/127787139