第3章 用Django设计大型电商的类别表
在本章中,我们将要做一个符合数据量庞大的电商平台的类别表。看似使用SQL语句就可以完成的功能需求,其实并没有想象中那么简单。对于如天猫和京东这样数据量庞大的电商类平台来说,类别表内的数据记录是存在层层嵌套的,如果采用传统的构建数据表的方式来完成功能需求,是非常难以实现的。通过本章的学习,我们就可以了解到,如何使用Django来设计一张数据表,以实现数据量庞大的商品分类需求。
3.1 电商类别表的项目功能需求
本节将分析电商“大鳄”的类别表中实现了哪些功能。让我们从宏观角度来观察一下大型的企业电商对于类别表的设计思路,从而提升和夯实我们对于数据库设计的水平。相信从这一节中,读者会对数据类别表有一个重新的认识,从而对以后实战项目中的数据库设计,能够做到胸有成竹。
3.1.1 类别表需求分析
如图3-1所示,天猫、京东,以及目前国内的一线电商平台,大多都是以这种结构作为首页上方的分类导航界面,将树状分类导航与轮播图镶嵌在一起,这样从网页设计角度上来讲,可以用最少的空间呈现最多的数据信息。当然,使用Django主要是分析后端的逻辑,前端的网页设计并不是本书的重点。
大家可以将图3-1与天猫或者京东的首页进行对比查看。如图3-1所示,在轮播图上方是一级类目,包括商品分类及一些业务分类。当我们将光标悬浮在任何一个二级类目上时,就会出现在此二级类目下的三级类目、二级类目所包含领域的一些推荐品牌的Logo,以及在三级类目中比较热门的一些四级类目的搜索内容。
这样听起来似乎有一点绕,我们可以拿天猫来举一个例子。比如图3-1中的一级类目,在天猫中,这些一级类目是“商品分类”“天猫超市”“天猫国际”“喵生鲜”等,二级类目是“女装/内衣”“男装/户外运动”“女鞋/男鞋/箱包”等,当我们将光标移动悬浮到某一个二级类目上方时,比如将光标悬浮到“女鞋/男鞋/箱包”这个类目上方,在原本轮播图的上面,就会出现一个新的导航页将其覆盖,而在导航页中分为左、中、右三个结构,左边这一列就是三级类目,比如“推荐女鞋”“潮流男鞋”“女单鞋”“特色鞋”等,中间这一列就是四级类目,比如“潮流过膝靴”“气质百搭踝靴”“永远的帆布鞋”,右边这一列则是一些品牌Logo,比如“李宁”“耐克”“乔丹”等。总结一下,类别表的设计需求如下:
(1)类别表必须包含多级类目,至少分为四级类目。
(2)类别表每一级类目数据都要有各自类目级别的标注,以便前端进行网页设计。
(3)类别表内的类别数据,必须可以灵活地进行增、减,并且不会因此而改变其上下层级的数据关系。
3.1.2 使用Vue.js在前端开发一个电商导航栏项目demo1
在3.1.1节中,我们了解到了一个大型的电商网站的导航栏类别表的项目需求。本节我们将使用Vue.js开发一个电商导航栏的前端demo。
什么是Vue.js?在回答这个问题之前,我们需要先了解一个概念:前端框架。
在第1章中,我们详细地讲述了什么是前后端分离,以及在当今技术领域,使用前后端分离面相切面编程的技术模式,来处理多端应用开发的需求。既然是前后端分离,有后端框架Django,当然也要有前端框架。前端框架与后端框架不同,后端框架Django是为Python处理网站后端逻辑而创造出来的,Django之于Python,就如同ThinkPHP之于PHP,.NET之于C#,几乎每一种后端框架,都对应着一种编程语言。但是对于前端框架而言,所有的前端框架,都只对应HTML+CSS+JavaScript语言,绝大多数的前端框架,都是为了将HTML+CSS+JavaScript工程化而产生的。
Vue.js就是一个前端框架。Vue.js的单页面应用模式对于前端技术的影响非常深远,包括微信小程序在内的很多前端原生语言,都是借鉴了此模式。
Vue.js是一个构建数据驱动的Web界面渐进式框架,与Angular和React并称为前端三大框架,而Vue.js框架是由华人尤雨溪所创造,开发文档更适合中国人阅读,而且尤雨溪也已经加盟阿里巴巴,所以Vue.js在国内也得到了阿里巴巴的推广,已经成为了国内最热门的前端框架之一。Vue.js的知识非常简单,如果大家已经掌握了HTML+CSS+JavaScript语言,通过Vue.js的官方文档,只需要几个小时的学习,就可以轻松上手Vue.js框架了。Vue.js是实现多端并行中非常重要,也是非常基础的一个技术。
下面我们来搭建Vue.js的开发环境:
(1)下载Node.js。
Node.js官网地址为https://nodejs.org/en/ ,如图3-2所示,选择适合大多数用户使用的稳定版本10.15.0LTS安装包。
注意:如图3-2所示,这里下载的是适合Windows 64位操作系统的Node.js。由于Node.js的下载和安装非常简单,所以这里只介绍Windows 64位操作系统下Node.js的下载与安装方法。Mac OS系统和Linux系统下的下载和安装方法,大家可以自行在网上查看相关教程。
(2)安装Node.js。
将Node.js安装包msi文件下载到计算机中,双击安装包,打开Node.js安装对话框,如图3-3所示,然后单击Next按钮。
在进入的对话框中,勾选同意协议,然后单击Next按钮,进入下一步,如图3-4所示。
在进入的对话框中,单击Change…按钮可以自定义将Node.js安装到哪个路径,也可以不做修改,安装在默认路径下。然后单击Next按钮,进入下一步,如图3-5所示。
在进入的对话框中,单击Next按钮,进入下一步,如图3-6所示。
在进入的对话框中,单击Install按钮进行安装,如图3-7所示。
经过几分钟的等待,出现如图3-8所示的对话框后,单击Finish按钮,完成安装。
(3)查看Node.js是否安装成功。
通过单击桌面左下角的“开始”按钮,然后输入cmd,打开cmd.exe程序,如图3-9所示。
在cmd操作界面输入以下命令,运行结果如图3-10所示。
然后按Enter键,如果像图3-10中一样显示Node.js的当前版本,则证明安装成功。
(4)安装淘宝镜像cnpm,可以让资源包下载得更快。在cmd操作界面输入以下命令,运行结果如图3-11所示。
然后按Enter键进行安装。
注意:由于国内网络原因,使用淘宝镜像cnpm进行资源包下载虽然比直接使用npm进行资源包下载速度快得多,但是使用npm进行资源包下载很多时候会因为网速过于缓慢导致资源包下载失败,因此多数情况下都会选择使用cnpm。但是有一点需要注意,cnpm在镜像的过程中,存在一个时间差,有可能因为这个时间差造成资源包的版本差异,从而导致项目报错无法运行的情况,当大家发现有因为资源包而报错的情况时,可以尝试用npm重新下载一次资源包。
(5)安装Vue.js的脚手架工具。输入以下命令,运行结果如图3-12所示。
然后按Enter键进行安装。
(6)创建项目。创建Vue.js 项目,命名为demo1,如图3-13所示。
然后连续按5次Enter键默认选项。
注意:5次Enter键,分别默认5个选项,其意义是:第1项Project name,代表项目名称,默认是demo1;第2项Project description,代表对此项目进行一个简短的说明,默认是A Vue.js project;第3项Author,代表输入作者姓名,默认是暂不设置作者姓名;第4项License,代表软件授权许可协议类型,默认是MIT(代表作者只想保留版权,而无其他限制);第5项Use sass,代表是否使用sass,默认是No。
项目新建完成后,切换到项目目录下:
cd demo1
安装依赖:
cnpm install
(7)运行初始项目。在项目目录demo1下,执行以下命令,结果如图3-14所示。
在运行了项目以后,浏览器会自动打开如图3-15所示的页面,并访问以下网址:
当大家见到如图3-15所示的页面,代表我们新建的Vue.js项目成功了。
(8)项目目录结构。如图3-16所示,使用VS Code编辑器将项目demo1打开,可以看到项目的目录结构,由于我们仅仅需要演示导航栏这一个功能,所以可以将代码都写在App.vue中。
注意:VS Code是一款专门做前端代码编辑的编辑器,是完全免费的,而且不需要安装,下载以后就可以直接打开。在这里可以使用VS Code打开项目,大家也可以选择自己喜欢使用的前端代码编辑器,并没有特殊限制。
(9)编辑电商导航栏所需代码。将App.vue中的代码替换如下:
HTML部分:
<template>
<div id="app">
<div class="all">
<div class="one">
<div class="oneType" v-for="(item,index) in one" :key="index">
<b>{{one[index]}}</b>
</div>
</div>
<div class="twothreefour">
<div class="two">
<div class="twoType"
v-for="(item,index) in two" :key="index"
@mouseenter="open(index)">
<b>{{two[index]}}</b>
</div>
</div>
<div class="threefour" v-if="flag"
@mouseleave="close()">
<div class="threefourType" v-for="(item,index) in three" :key=
"index">
<span class="three">{{three[index]}}</span>
<span class="four" v-for="(item4,index4) in four" :key="index4">
{{four[index4]}}</span>
</div>
</div>
</div>
</div>
</div>
</template>
JavaScript部分:
<script>
export default {
name: 'app',
data () {
return {
one:['一级类目','一级类目','一级类目','一级类目','一级类目'],
two:['二级类目1','二级类目2','二级类目3','二级类目4','二级类目5'],
three:[],
four:['四级类目','四级类目','四级类目','四级类目','四级类目'],
flag:false
}
},
methods: {
open(index){
var index=index+1;
var i=index+"";
this.three=['三级目录'+i,'三级目录'+i,'三级目录'+i,'三级目录'+i,'三级目录
'+i]
this.flag=true
},
close(){
this.flag=false
}
},
}
</script>
CSS样式部分如下:
<style>
*{
/* 样式初始化 */
box-sizing: border-box;
margin: 0;
padding: 0;
}
.all{
/* 将整个导航栏组件做整体设置 */
/*宽度占浏览器80%,高度400px;背景为灰色;上外边距50px; 左右居中*/
/* 设置为flex弹性盒子,并且定义为高度不够自动折行模式,用于横向排列子元素 */
width: 80%;
height: 400px;
background:#eee;
margin: 50px auto;
display: -webkit-flex; /* Safari */
display: flex;
flex-wrap: wrap;
}
.one{
/* 设置一级类目所占地区的样式,宽度占满all盒子的100% */
width: 100%;
height: 50px;
background: #FF8888;
display: flex;
display: -webkit-flex; /* Safari */
flex-wrap: wrap;
/* 弹性盒子内部的子元素都均匀排列成一横排,并且左右两边都留有一定空隙 */
justify-content: space-around;
}
.oneType{
width: 20%;
height: 50px;
line-height: 50px;
text-align: center;
}
.oneType:hover{
background-color:chocolate;
color: #eee;
}
.twothreefour{
/* 盛放二、三、四级目录的盒子 */
width: 100%;
height: 350px;
background: #66FF66;
display: -webkit-flex; /* Safari */
display: flex;
flex-wrap: wrap;
/* 弹性盒子内部的子元素都均匀排列成一横排,并且左右两边都不留空隙 */
justify-content: space-between;
}
.two{
/* 设置盛放二级类目的弹性盒子 */
width: 15%;
height: 100%;
background: #77FFCC;
display: -webkit-flex; /* Safari */
display: flex;
/* 弹性盒子内部的子元素从上到下排成一列 */
flex-direction: column;
}
.twoType{
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
background: #EEFFBB;
}
.twoType:hover{
background-color:black;
color: #eee;
}
.threefour{
width: 40%;
margin-right: 45%;
height: 100%;
background: #33FFDD;
display: -webkit-flex; /* Safari */
display: flex;
flex-direction: column;
}
.threefourType{
margin: 10px auto;
}
.three{
font-family: 微软雅黑, 黑体;
font-size: 16px;
font-weight: 800;
}
.four{
font-family: 宋体;
font-size: 12px;
font-weight: 400;
}
</style>
重新运行项目demo1,然后在浏览器中访问http://localhost:8080/ ,我们使用Vue.js开发的最简易的电商导航栏效果图,如图3-17所示。
3.2 为什么不用传统建表方式建类别表
在3.1.2节中,我们建立了一个前端电商平台导航栏项目demo1。通过demo1项目中App.vue的代码不难看出,在完整的前后端分离项目中,前端的Vue.js项目,是将网络请求通过API发送给后端项目,从而获取数据并且赋值给前端项目的data,替换原本的data内容。这也是实现前后端分离的基本原理。
在本节中,我们将新建一个Django后端项目demo2,与前端的Vue.js项目demo1组成一个前后端分离项目,通过实际项目中前后端数据之间的调试来解述为什么传统的建表方式无法满足大型电商网站的类别表需求。
注意:传统的建表方式虽然可解决大部分功能需求,但是当遇到诸如大型电商网站类别表这种级别的开发需求时,传统建表方式就显得捉襟见肘了。所以不建议大家跳过此节内容直接看下一节。
3.2.1 使用PyCharm新建后端演示项目
PyCharm是一种Python的IDE,是广大Python程序员非常喜欢的一款IDE。本书的读者定位是具有一定Python基础的人,相信许多读者的计算机上即使没有Office,也有一款PyCharm。此处不再介绍关于PyCharm的下载与安装步骤,请读者自行查看相关介绍。
(1)如图3-18所示,新建Django项目并命名为demo2,同时新建App,命名为app01。
(2)如图3-19所示,在PyCharm中打开项目终端,安装相关依赖包:
注意:这里也可以使用cmd窗口,通过命令行切换到项目目录下来执行依赖包的下载和安装命令,只不过这样显然要麻烦很多。
(3)在demo2/demo2/settings.py中注册rest_framework:
INSTALLED_APPS = [
'Django.contrib.admin',
'Django.contrib.auth',
'Django.contrib.contentTypes',
'Django.contrib.sessions',
'Django.contrib.messages',
'Django.contrib.staticfiles',
'app01.apps.App01Config',
'rest_framework'
]
(4)在demo2/app01/models.py中新建类别表:
from Django.db import models
from datetime import datetime
# Create your models here.
class Type1(models.Model):
"""
一级类目
"""
name=models.CharField(max_length=10,default="",verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='
添加时间')
class Meta:
verbose_name = '商品类别'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Type2(models.Model):
"""
二级类目
"""
parent=models.ForeignKey(Type1,verbose_name="父级类别",
null=True,blank=True,on_delete=models.CASCADE)
name=models.CharField(max_length=10,default="",verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='
添加时间')
class Meta:
verbose_name = '商品类别2'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Type3(models.Model):
"""
三级类目
"""
parent=models.ForeignKey(Type2,verbose_name="父级类别",
null=True,blank=True,on_delete=models.CASCADE)
name=models.CharField(max_length=10,default="",verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='
添加时间')
class Meta:
verbose_name = '商品类别3'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Type4(models.Model):
"""
四级类目
"""
parent=models.ForeignKey(Type3,verbose_name="父级类别",
null=True,blank=True,on_delete=models.CASCADE)
name=models.CharField(max_length=10,default="",verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='
添加时间')
class Meta:
verbose_name = '商品类别4'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
然后同第(2)步一样,打开项目终端,执行数据更新命令:
Python manage.py makemigrations
Python manage.py migrate
如图3-20所示,当数据表构建成功后,在项目目录中会生成一个db.sqlite3数据库文件。
(5)手动添加一些数据。
我们不需要其他的数据库操作软件,只要将图3-20中所示的db.sqlite3文件直接拖曳到PyCharm的Database窗口内,如图3-21所示,就可以直接对项目demo2的所有数据表进行增、删、改操作了。
注意:因为本章所介绍的是一个数据量很小的项目,所以我们并没有使用MySQL数据库,而是直接使用了Django自带的sqlite3数据库,的确方便了许多。但是sqlite3的局限性是只适合数据量级比较小的数据库服务,一旦涉及数据量比较庞大的项目,就要选择使用MySQL数据库或者其他数据库。
如图3-22所示,我们通过PyCharm的Database界面,手动向一级类目表中增加了5条数据记录。
如图3-23所示,我们给二级类目表内手动添加了5条数据记录。
如图3-24所示,同样给三级类目表内手动添加了5条数据记录。
如图3-25所示,在四级类目表内,我们手动添加了11条数据记录。
3.2.2 完善demo2的后台逻辑代码
本节中会将demo2的业务逻辑补充完整,为下一节前后端项目联合调试做好准备。
(1)在app01目录下新建序列化模块serializers.py,新建4个类别表的序列化类:
from rest_framework import serializers #引入序列化模块
from .models import Type1,Type2,Type3,Type4 #引入所有数据表类
class Type1ModelSerializer(serializers.ModelSerializer):
class Meta:
model=Type1
fields="__all__"
class Type2ModelSerializer(serializers.ModelSerializer):
class Meta:
model=Type2
fields="__all__"
class Type3ModelSerializer(serializers.ModelSerializer):
class Meta:
model=Type3
fields="__all__"
class Type4ModelSerializer(serializers.ModelSerializer):
class Meta:
model=Type4
fields="__all__"
(2)在app01/views.py中,编写访问4个类别表的视图逻辑代码:
#引入序列化类
from .serializers import Type1ModelSerializer,Type2ModelSerializer
from .serializers import Type3ModelSerializer,Type4ModelSerializer
#引入数据表
from .models import Type1,Type2,Type3,Type4
#引入rest_framework相关模块
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer
# Create your views here.
class Type1View(APIView):
"""
all Type1
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
Types=Type1.objects.all()
Types_serializer = Type1ModelSerializer(Types, many=True)
return Response(Types_serializer.data)
class Type2View(APIView):
"""
all Type2
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
Types=Type2.objects.all()
Types_serializer = Type2ModelSerializer(Types, many=True)
return Response(Types_serializer.data)
class Type3View(APIView):
"""
all Type3
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
Types=Type3.objects.all()
Types_serializer = Type3ModelSerializer(Types, many=True)
return Response(Types_serializer.data)
class Type4View(APIView):
"""
all Type4
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
Types=Type4.objects.all()
Types_serializer = Type4ModelSerializer(Types, many=True)
return Response(Types_serializer.data)
注意:这里用到了Django REST framework的选择器,大家可以根据自己的喜好,选择使用JSONRenderer模式还是BrowsableAPIRenderer。
(3)在demo2/urls.py中添加路由代码:
from Django.contrib import admin
from Django.urls import path
#引入视图类
from app01.views import Type1View,Type2View,Type3View,Type4View
urlpatterns = [
path('admin/', admin.site.urls),
path('api/Type1/',Type1View.as_view()),
path('api/Type2/',Type2View.as_view()),
path('api/Type3/',Type3View.as_view()),
path('api/Type4/',Type4View.as_view())
]
3.2.3 前后端项目联合调试
现在,前端项目demo1和后端项目demo2都完成了,可以正式开始前后端联合调试的工作了。
(1)运行demo2项目。如图3-26所示,使用PyCharm可以通过右上方的运行项目按钮,一键便捷启动运行demo2项目。
(2)给demo1项目安装网络请求模块axios。在cmd窗口执行安装axios模块命令,结果如图3-27所示。
注意:axios是前端项目中非常主流的一款提供网络请求功能的第三方模块,我们在使用cnpm进行安装的时候,不要忘记在命令的后面加上--save,将模块的注册信息写入前端项目的配置信息中。
(3)改写前端项目,在demo1/src/App.vue中,style样式标签内的代码不做改变,其他代码修改如下:
HTML部分:
<template>
<div id="app">
<div class="all">
<div class="one">
<div class="oneType" v-for="(item,index) in one" :key="index">
<b>{{one[index].name}}</b>
</div>
</div>
<div class="twothreefour">
<div class="two">
<div class="twoType"
v-for="(item,index) in two" :key="index"
@mouseenter="open(index)">
<b>{{two[index].name}}</b>
</div>
</div>
<div class="threefour" v-if="flag"
@mouseleave="close()">
<div class="threefourType" v-for="(item,index) in three1" : key=
"index">
<span class="three">{{three1[index]}}</span>
<span class="four" v-for="(item4,index4) in four1" :key="index4">
{{four1[index4]}} </span>
</div>
</div>
</div>
</div>
</div>
</template>
JavaScript部分:
<script>
import Axios from 'axios';
export default {
name: 'app',
data () {
return {
one:[],
two:[],
three:[],
four:[],
flag:false,
three1:[],
four1:[]
}
},
methods: {
getData(){
const api='http://127.0.0.1:8000/';
var api1=api+'api/Type1/';
var api2=api+'api/Type2/';
var api3=api+'api/Type3/';
var api4=api+'api/Type4/';
var Type1=[];
var Type2=[];
var Type3=[];
var Type4=[];
Axios.get(api1)
.then(function (response) {
// console.log(response);
for(var i=0;i<response.data.length;i++){
// console.log(response.data[i])
Type1.push(response.data[i])
}
// console.log(Type1)
})
.catch(function (error) {
console.log(error);
});
this.one=Type1;
Axios.get(api2)
.then(function (response) {
// console.log(response);
for(var i=0;i<response.data.length;i++){
// console.log(response.data[i])
Type2.push(response.data[i])
}
// console.log(Type2)
})
.catch(function (error) {
console.log(error);
});
this.two=Type2;
Axios.get(api3)
.then(function (response) {
// console.log(response);
for(var i=0;i<response.data.length;i++){
// console.log(response.data[i])
Type3.push(response.data[i])
}
// console.log(Type3)
})
.catch(function (error) {
console.log(error);
});
this.three=Type3;
Axios.get(api4)
.then(function (response) {
// console.log(response);
for(var i=0;i<response.data.length;i++){
// console.log(response.data[i])
Type4.push(response.data[i])
}
// console.log(Type4)
})
.catch(function (error) {
console.log(error);
});
this.four=Type4;
// console.log(this.one)
// console.log(this.two)
// console.log(this.three)
// console.log(this.four)
},
open(index){
// console.log(this.two[index].id)
var temp=[]
for(var i=0;i<this.three.length;i++){
if(this.three[i].parent===index){
temp.push(this.three[i].name)
}
}
console.log(temp)
this.three1=temp;
var temp4=[]
for(var j=0;j<this.four.length;j++){
temp4.push(this.four[j].name)
}
this.four1=temp4
this.flag=true
},
close(){
this.flag=false
}
},
mounted() {
this.getData()
},
}
</script>
注意:考虑到篇幅问题,一级、二级、三级类目遵循了从属关系,而第四级类目为了体现效果并没有通过筛选赋值,筛选的逻辑原理跟三级类目相同,大家如果有兴趣,可以做进一步的优化和完善。
(4)解决跨域问题。在后端Django项目demo2中安装相关模块:
pip install Django-cors-headers
然后在settings.py中的注册里配置如下:
INSTALLED_APPS = [
'Django.contrib.admin',
'Django.contrib.auth',
'Django.contrib.contentTypes',
'Django.contrib.sessions',
'Django.contrib.messages',
'Django.contrib.staticfiles',
'app01.apps.App01Config',
'rest_framework',
'corsheaders'
]
在settings.py中的MIDDLEWARE里设置如下:
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', #放到中间件顶部
'Django.middleware.security.SecurityMiddleware',
'Django.contrib.sessions.middleware.SessionMiddleware',
'Django.middleware.common.CommonMiddleware',
'Django.middleware.csrf.CsrfViewMiddleware',
'Django.contrib.auth.middleware.AuthenticationMiddleware',
'Django.contrib.messages.middleware.MessageMiddleware',
'Django.middleware.clickjacking.XFrameOptionsMiddleware',
]
在settings.py中新增配置项,即可解决本项目中的跨域问题。
CORS_ORIGIN_ALLOW_ALL = True
注意:在Python全栈开发的知识体系里,跨域问题和深浅拷贝,几乎是逢面试必考的两个笔试题。不同的是,深浅拷贝在实际项目中很少用到,而跨域问题却几乎在每个项目中都有涉及,只是并非都能被察觉罢了。跨域问题是非常重要的一个知识点,关系到网络安全,甚至说跨域问题是Web安全中最重要的一环也不为过。我们在这里只是先预热一下,在第9章中将针对跨域问题进行详细分析。
(5)重启前后端,即可看到效果,如图3-28所示。
总结一下,从本节不难看出,为了获取类别表数据,前端通过不同的4个API发送了4次网络请求,这是因为我们假设一个电商平台只有四级类目。但是现实中这显然是不可能的,往往一个用户量越庞大的电商平台,商品的种类越齐全、越细分,就意味着发送网络请求的倍数也就越多,需要的带宽也就更多,这个成本是非常庞大的。
抛去带宽成本不谈,假如恰巧你的老板不在乎钱,不懂技术,又很好骗,但是前端工程师也会在开发获取类别数据时,因不得不开发冗长的代码而投诉后端工程师。所以,不要抱有侥幸心理,传统的建表方式构建大型电商平台的类别表是行不通的。
3.3 使用Django的model实现类别表建立
利用Django框架,Python已经可以通过新建一个类直接构建一个高质量的数据表了。在本节中,我们将构建一个能满足大型电商网站业务需求的类别表。
3.3.1 四表合一
类别表本来就应该是一张表,我们来构建一张类别表,把上一节用传统建表方式建立的四张类别表合为一张。
(1)在demo2/app01/models.py中新建类Type。
class Type(models.Model):
"""
商品类别
"""
CATEGORY_TYPE=(
(1,'一级类目'),
(2,'二级类目'),
(3,'三级类目'),
(4,'四级类目')
)
name=models.CharField(default='',max_length=30,verbose_name='类别名',
help_text='类别名')
code=models.CharField(default='',max_length=30,verbose_name='类别code',
help_text='类别code')
desc=models.CharField(default='',max_length=30,verbose_name='类别描述',
help_text='类别描述')
category_Type=models.IntegerField(choices=CATEGORY_TYPE,verbose_name='类别描述',help_text='类别描述')
parent_category=models.ForeignKey('self',null=True,blank=True,verbose_name='父类目录',
help_text='父类别',related_name='sub_cat',on_delete=models.CASCADE)
is_tab=models.BooleanField(default=False,verbose_name='是否导航',help_text=
'是否导航')
class Meta:
verbose_name='商品类别'
verbose_name_plural=verbose_name
def __str__(self):
return self.name
(2)建表,执行数据更新命令如下:
Python manage.py makemigrations
Python manage.py migrate
3.3.2 数据导入
因为在上一节中建立的4个类别表已经手动加入了一些数据,我们没有必要再用手动加入的方式将这些记录加入新建的Type表里,而是选择使用一种更加方便的方式,即通过Postman将数据以post的方式,加入Type表内。
(1)在demo2/app01/ serializers.py内增加Type表的序列化类:
from .models import Type
class TypeModelSerializer(serializers.ModelSerializer):
class Meta:
model=Type
fields="__all__"
(2)在demo2/app01/views.py内创建TypeView视图类:
from .models import Type
#导入序列化类
from .serializers import TypeModelSerializer
class TypeView(APIView):
"""
操作类别表
"""
renderer_classes = [JSONRenderer]
def get(self,request,format=None):
types=Type.objects.all()
types_serializer = TypeModelSerializer(types, many=True)
return Response(types_serializer.data)
def post(self,request):
name=request.data.get('name')
category_type=request.data.get('lei')
parent_category_id=request.data.get('parent')
type=Type()
type.name=name
type.category_type=category_type
if parent_category_id:
parent_category=Type.objects.filter(id=parent_category_id).first()
type.parent_category=parent_category
type.save()
type_serializer=TypeModelSerializer(type)
return Response(type_serializer.data)
(3)在demo2/demo2/urls.py内增加路由代码如下:
from app01.views import TypeView
urlpatterns = [
#......
path('api/type/',TypeView.as_view())
]
(4)运行demo2项目,使用Postman通过post加入数据记录,如图3-29所示。
注意:如图3-29所示,我们之前建立的4个表,非一级类目的父级id(parent)跟四合一以后的表中非一级类目的父级id是不一样的。
如图3-30和图3-31所示,大家在加入数据记录以后,可以对照一下查看是否正确。
3.3.3 前后端项目联合调试
崭新的数据类别表构建完成了,下面将前端项目改造一下,对接调试我们的类别表,看看可以节省多少前端的工作量。
(1)在demo1/src/App.vue原来的基础上,只修改
<script>
import Axios from 'axios';
export default {
name: 'app',
data () {
return {
type:[],
one:[],
two:[],
flag:false,
three1:[],
four1:[]
}
},
methods: {
getData(){
const api='http://127.0.0.1:8000/api/type/';
var _this=this
Axios.get(api)
.then(function (response) {
_this.type=response.data;
for(var i=0;i<_this.type.length;i++){
if(_this.type[i].category_type===1){
_this.one.push(_this.type[i])
}
}
for(var j=0;j<_this.type.length;j++){
if(_this.type[j].category_type===2){
_this.two.push(_this.type[j])
}
}
})
.catch(function (error) {
console.log(error);
});
},
open(index){
this.three1=[]
this.four1=[]
var parent=this.two[index].id
for(var i=0;i<this.type.length;i++){
if(this.type[i].parent_category===parent){
this.three1.push(this.type[i].name)
}
if(this.type[i].category_type===4){
this.four1.push(this.type[i].name)
}
}
this.flag=true
},
close(){
this.flag=false
}
},
mounted() {
this.getData()
}
}
</script>
注意:关于同步网络请求和异步网络请求的问题,因为考虑到本书主要面对的读者群体是Django开发者,可能对于同步网络请求和异步网络请求的“挖坑”经验不足,如果大家将上面methods中的getData方法与给this.one和this.two赋值分开,就有可能掉到异步网络请求的“坑”里去,导致出现加载出来的数值为空的情况。
为什么JavaScript异步网络请求的“坑”这么多?因为早些年网速比较慢,浏览器端通过网址向后端服务器请求获取资源(代码、文字、图片、音频、视频等),如果等所有的资源都加载完成,用户才可以与网页做交互,那么网站将会毫无体验可言,于是有了AJAX异步网络请求。异步网络请求可以理解为,先把最重要的资源(大多时候是代码,但某些时候是图片或视频)接收到网页端,用户就可以操作网页了,与此同时,其他的资源也在慢慢地加载着。往往网页中所有的资源还没加载完,用户已经通过网页加载的部分资源,将想要做的事给办完啦,然后就可以关闭网页了。
这无疑是解决网页加载资源过慢问题的一个绝佳方案。但异步网络请求所带来的一个令人烦恼的问题就是,当想要通过网络请求获取一些数据,然后用这些数据对网页的某些变量做初始化时,网络请求还没来得及响应,这些变量就已经完成了初始化,当然,所有变量都初始化为null。所以为了避免异步网络请求所带来的问题,除非开发者对JavaScript运用得比较熟练,不然最好不要将this.one和this.two的初始化与getData方法分离。
(2)运行前端demo1和后端demo2,查看效果图,如图3-32所示。我们用四分之一的代码量,完成了一个更加强大的类别表。细心的读者可以发现,我们的类别表还有一些字段没有用上,不然功能还会更加强大,这些就交给读者去发现和体验吧。