详解 view.py
登录注册
完善视图函数
将之前写的注册函数改得完善亿点:
from django.shortcuts import render
import json
from user.models import User
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
# Create your views here.
@csrf_exempt
def register(request):
if request.method == 'POST':
# 检测 POST 类型的请求
data_json = json.loads(request.body)
# 此时 data_json 已经转为了字典类型
username = data_json['username']
password1 = data_json['password1']
password2 = data_json['password2']
email = data_json.get('email', '')
# 不确定是否有这个参数,使用get
if password1 != password2:
return JsonResponse({'result': 0, 'message': '两次密码不一致!'})
# 虽然密码一致性与合法性检测一般是前端判断
# 但是后端最好也加上,以防万一
user = User(username=username, password=password1, email=email)
# id 是自动赋值,不需要指明
# post 不赋值会使用默认值0
user.save()
return JsonResponse({'result': 1, 'message': '注册成功!'})
# 正常结束后需要给一个反馈信息
前后端在传递数据时,通常使用json
格式。不了解json
的同学看这里。前端把要传的数据打包成 json 发给后端,后端解析这个 json 得到字典后,进行相应的处理,最后返回给前端一个 json 信息来告知处理结果。
{
'result': 1,
'message': '注册成功!'
}
这是我个人经常用的一个返回信息模板,第一个参数0或1告诉前端是否正确处理了,后面一个参数则告诉详细的处理结果。比如由于两次密码不一致导致注册失败,前端就可以直接把 message 里的内容弹窗弹出来。
之所以选择 POST 类型的请求,是因为其更安全,数据内容不会携带在 URL中。同时它支持携带任意格式任意长度的请求参数,相对较为灵活。为了省事一点,可以要求前端只传 POST 类型的请求。POST 和 GET 的区别见这里。
完善了注册函数后,再次启动项目(其实也不用每次都启动, 在启动状态下保存文件,Django 会自动检测并更新),用 postman 测一下注册功能是否正确。这次的请求因为带上了参数,所以需要在 body 中添加相应内容:
随便填一个最简单的管理员账号:
不出所料,得到了两次密码不一致的结果:
改成一致后再次发送请求,就可以看到正确的反馈信息:
在 Datagrip 中刷新一下 user 表,就能看到刚刚写入到数据库中的内容:
增删改查中的“增“,到这里已经结束了。
继续完善登录函数:
@csrf_exempt
def login(request):
if request.method == 'POST':
data_json = json.loads(request.body)
username = data_json['username']
password = data_json['password']
user = User.objects.get(username=username)
# 查询语句,根据括号中的条件,返回唯一一条结果
# 如果有多个表项匹配这一条件,则报错
if user.password == password:
return JsonResponse({'result': 1, 'message': '登录成功!'})
else:
return JsonResponse({'result': 0, 'message': '密码错误!'})
同时在user/urls.py
中注册:
from django.urls import path
from user.views import *
urlpatterns = [
path('register', register),
path('login', login),
]
postman 中新建一个 login 请求,进行测试:
两种情况都可以正常识别。但是,如果是下面这个情况,就会报错:
返回了一个 500 错误(恭喜你解锁了一个以后最常看见的错误代码)。仔细看下 Django 的终端输出:
查询结果不存在,这代表着User.objects.get(username=username)
的获取结果为空。所以在不确定结果是否存在前,不能直接用get
去查询,应该先检查存在性。查询部分应该修改为如下格式:
if User.objects.filter(username=username).exists() == True:
# 使用 filter 先检查存在性
user = User.objects.get(username=username)
else:
return JsonResponse({'result': 0, 'message': '用户不存在!'})
此时就可以正常处理用户不存在的情况了:
之前那个报错,渲染成 HTML 格式,长下面这样:
在settings.py
中有一个参数,表示是否开启 DEBUG 模式,默认为 True。在这种情况下,报错会返回详细信息。在测试环境中不建议将其关闭,前端在浏览器中抓取到 500 错误后,可以查看返回信息,根据类似上面的报错信息提醒后端修 Bug。前后端对接的 debug 流程大概就是这样。
使用 session
在测试 login 接口时,发现存在一个问题,那就是后端无法检测登录状态,每次都认为是一个未登录用户在执行登录操作。这会带来很多问题,比如如果一个登录用户想查看自己所发表的博客,后端就无法进行正确的查询操作。
为解决这个问题,有两种办法:
- 使用 token,也就是身份令牌。在登录成功后,后端会将一些信息加密传递给前端,加密后的结果就称为 token。每次前端在向后端发送请求时,都要携带这个token,后端在解密之后,就会知道用户名等各项信息。这种办法不需要前后端进行过多配置,实现原理简单;但每次前端传参时都需要手动加上 token。有关 token 的更多介绍,可以看这里。
- 使用 session。这是 Django 官方推荐的方法。session 可以看做是一个存储信息的仓库,后端可以向 session 中写入、读取信息。在每次返回前端请求时,会自动捎带上 session id,前端将其存在cookie里,每次发请求就会自动发给后端。后端拿到id,先是自动去数据库中根据 id 找到session,然后就可以支持各种操作了。不过在跨域的情况下,前端如果想要保存 cookie,需要加很多额外的配置信息,但是后端用起来十分方便(
我只负责出后端教程,肯定后端怎么方便怎么来,就让前端去解决这个问题吧)。
下面介绍 session 的使用。对登录函数做一些修改:
@csrf_exempt
def login(request):
if request.method == 'POST':
id = request.session.get('id', 0)
# 从 session 中获取信息
if id != 0:
return JsonResponse({'result': 0, 'message': '用户已登录!'})
data_json = json.loads(request.body)
username = data_json['username']
password = data_json['password']
if User.objects.filter(username=username).exists() == True:
user = User.objects.get(username=username)
else:
return JsonResponse({'result': 0, 'message': '用户不存在!'})
if user.password == password:
request.session["id"] = user.id
# 登录成功后就把 id 存进去
return JsonResponse({'result': 1, 'message': '登录成功!'})
else:
return JsonResponse({'result': 0, 'message': '密码错误!'})
通过 session 来记忆当前登录的用户的 id 是什么。此时在进行多次登录测试:
就会发现已经可以正确识别登录状态了。
顺带加一个登出功能:
@csrf_exempt
def logout(request):
if request.session.get('id', 0) != 0:
request.session.flush()
# 清空所有的信息
return JsonResponse({'result': 1, 'message': r'已登出!'})
else:
return JsonResponse({'result': 0, 'message': r'请先登录!'})
注册路由:
from django.urls import path
from user.views import *
urlpatterns = [
path('register', register),
path('login', login),
path('logout', logout),
]
测试:
发表博客
完善了登录注册这些基本功能后,就要开始着手实现博客相关的接口了。首先是创建一个新的APP,命名为post,表示博客相关模块:
python manage.py startapp post
然后去settings.py
中注册,再在post/models.py
里初始化一下博客数据库:
from django.db import models
# Create your models here.
class Post(models.Model):
id = models.AutoField(primary_key=True)
# 主键
uid = models.IntegerField()
# 发表该博客的用户id
# 也可以选择用外键连接到User表中
title = models.CharField(max_length=50)
# 博客标题
article = models.TextField(null=True, blank=True)
# 博客正文
# 因为可能很长,所以使用 TextField 字段
time = models.DateTimeField()
# 发表时间
然后三板斧迁移一下,同步到本地的数据库文件里:
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
DataGrip 中刷新就可以看到新建的表了:
接下来是完善views.py
,实现发表博客的功能。参考代码如下:
from django.shortcuts import render
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from post.models import *
from user.models import *
from datetime import datetime
# Create your views here.
@csrf_exempt
def create(request):
if request.method != 'POST':
return JsonResponse({'result':0, 'message':'Error'})
id = request.session.get('id', 0)
if id == 0:
return JsonResponse({'result':0, 'message':r'请先登录'})
data_json = json.loads(request.body)
post = Post(
uid = id,
title = data_json['title'],
article = data_json['article'],
time = datetime.now()
)
post.save()
# 存储新博客文章信息
user = User.objects.get(id = id)
user.post += 1
user.save()
# 更新发表文章数量
关于 URL 的配置,同user
模块,这里只给出修改后的结果:
hello/urls.py
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('user/', include(('user.urls', 'user'), namespace="user")),
path('post/', include(('post.urls', 'post'), namespace="post")),
]
post.urls.py
from django.urls import path
from post.views import *
urlpatterns = [
path('create', create),
]
运行,在 Postman 中简单测试一下:
就可以在数据库中看到相应的结果了:
发表完博客后,下一步是查看。由于不知道用户会点开哪一篇博客,所以要先给前端当前用户所发表的博客列表,用户点击之后,再去精确获取详细的正文信息。涉及到的函数如下:
@csrf_exempt
def get_list(request):
if request.method != 'POST':
return JsonResponse({'result':0, 'message':'Error'})
data_json = json.loads(request.body)
id = int(data_json['id'])
# 这里是用户id,获取这个用户所发表的所有文章
posts = [{
'id': x.id,
'title': x.title,
'time': x.time
} for x in Post.objects.filter(uid = id)]
# filter 的结果是一个类似列表的可迭代元素
# 故可以使用 for .. in 去遍历
# 这其实是一种压行写法
return JsonResponse({'result':1, 'message':r'获取成功', 'posts':posts})
@csrf_exempt
def get_post(request):
if request.method != 'POST':
return JsonResponse({'result':0, 'message':'Error'})
data_json = json.loads(request.body)
id = int(data_json['id'])
# 这里是文章id
post = Post.objects.get(id = id)
return JsonResponse({'result':1, 'message':r'获取成功', 'post': post.to_dic()})
# 注意这里使用了to_dic()函数
Django 中的数据库定义是使用的类的形式,既然是类,就可以在里面声明函数。to_dic
的定义如下:
class Post(models.Model):
id = models.AutoField(primary_key=True)
# 主键
uid = models.IntegerField()
# 发表该博客的用户id
# 也可以选择用外键连接到User表中
title = models.CharField(max_length=50)
# 博客标题
article = models.TextField(null=True, blank=True)
# 博客正文
# 因为可能很长,所以使用 TextField 字段
time = models.DateTimeField()
# 发表时间
def to_dic(self):
return {
'id': self.id,
'title': self.title,
'time': self.time,
'article': self.article,
'uid': self.uid
}
之所以要加一步到字典的转换,这是因为 get 得到的结果是 Post 类型,如果直接放到 json 中传递给前端,会在打包转换的时候出现格式错误而报错。json 只能认字典,列表等基础类型,所以一定要加这么一个步骤。
测试结果如下:
最后顺手补一个删除博客的功能:
@csrf_exempt
def delete_post(request):
if request.method != 'POST':
return JsonResponse({'result':0, 'message':'Error'})
data_json = json.loads(request.body)
id = int(data_json['id'])
# 这里是文章id
post = Post.objects.get(id = id)
user = User.objects.get(id = post.uid)
user.post -= 1
user.save()
# 记得先更新发表文章数量
post.delete()
return JsonResponse({'result':1, 'message':r'删除成功'})
到这里,数据库最基本的增,删,改,查四种情况都已经覆盖到了。之后实现的视图函数,基本上就是以上的各种变种,只不过业务逻辑上可能复杂一些, 比如一次涉及到多个表的查询,表之间信息同步更加复杂,数据库中的原始数据有时候不能直接给后端要先本地处理(例如要根据某个统计数据让前端画图等)。
同一个功能的实现是多样的。比如上面获取发表文章列表,其实可以直接把这个文章的所有详细信息全部返回给前端,这样前端在点击某个博客的页面的时候,就可以根据已有的数据直接渲染,不再向后端请求,可以大大提升相应速度。但是在发表博客数很多,博客正文很长的情况下,这么实现会导致在获取列表时相应时间变长,同时一次返回过多数据也会给带宽带来很大的压力。这就需要前后端对业务做一个合理的切割,来得到一个多方面平衡的接口实现方案。
到这里视图函数讲解过程就已经结束了。相关代码已经提交到了 Github 的示例项目上,最后一次 commit 的注释为“Finish view”。