一些思考
# Q1: 多业务? 路由分发?
view多业务+路由分发
- flask项目中视图采用的是函数的形式,flask不需要路由文件,所以里面一个py就是业务的一个功能模块.
而Django是需要路由文件的,所以 功能模块对应的视图函数些放一个py,功能模块对应的路由些放一个py.
这两py放到一个文件夹里!! - flask项目可根据返回值是json还是html模版进一步进行了拆分. 在该Django项目中,我也进行了拆分,因为这是半分离嘛,尽可能的区分下html和api.往全分离的方向靠拢!! ps: 我在视图函数上进行了表明,若视图函数是以"_html"结尾,那就代表返回的是html模版!!
# Q2: 使用FBV还是CBV?
下面的分析,我是在思考这个Django项目用FBV还是用CBV,结合参考的Flask项目以及以前写过的stark组件,我决定用FBV来实现.
以下是对django不分离的路由的回顾.
- FBV 4个请求 -- 4个路由 -- 4个函数
urlpatterns = [
path('list/', views.role_list, name='role_list'),
path('add/', views.role_add, name='role_add'),
path('edit/<int:pk>/', views.role_edit, name='role_edit'),
path('del/<int:pk>/', views.role_del, name='role_del'),
]
2
3
4
5
6
- CBV 5个请求(多了个获取单条数据) -- 2个路由 -- 2个类
urlpatterns = [
path('role/', views.RoleView.as_view()), # get多条、 post
path('role/<int:pk>/', views.RoleDetailView.as_view()), # get单条、 put、 delete
]
2
3
4
- 还可改进,参照drf的ViewSetMixin对Django的源码进行改写 5个请求 -- 2个路由 -- 1个类
urlpatterns = [
path('role/', views.RoleView.as_view({"get":"list","post":"create"})),
path('role/<int:pk>/', views.RoleView.as_view({"get":"retrive","delete":"destroy","put":"update"})),
]
2
3
4
# Q3: 表单值的处理
关于后端接收到前端传过来的值的处理,要经过两方面的考虑:
1.用户在前端表单中是怎么填的, 前端ajax中传给后端的值是咋样的? 后端接收到的表单传过来的值是怎么样的?
2.后端接收到的值经过验证后,值是怎么样的? 后端在进行存储和修改时,值又是怎么样的?
前端可以不传, 有两含义 - 表单对该字段不填 or 表单中压根没该字段
1> ORM表中有xx字段,该字段设置了可为空 + modelform里有xx字段 + 前端表单不填
> 则,经过验证后,form.clean_data['xx']的值为xx字段类型的默认值!!eg:CharField字段类型的默认值是空字符串.
2> ORM表中有xx字段,该字段设置了default但并未设置可为空 + modelform里有xx字段 + 前端表单不填
> 则,验证时,xx字段自身验证就出错,报错信息:必填!
3> ORM表中有xx字段,该字段设置了可为空 + modelform里有xx字段 + 前端表单中没有该字段
> 则,form.clean_data['xx']的值为None.
2
3
4
5
6
★ 记住!
- drf的ORM字段设置了 default、时间类型、可为空,代表required=False,即前端可不传
- django的form表单,ORM字段设置了default没用!是否必填的判断只认有无null=True、blank=True (时间类型除外
2
★ 记住!Django的form表单验证
- 先看null=True,blank=True,有,则前端可不传,不传可表示没有该表单项,亦可表示有该表单项但该表单项可不填
- save时,若有default,则存为默认值,否则为空 (CharFelid字段的空有两含义 空字符串和None
2
★ 关于ORM表中的外键
一对多外键,设置了null=True,blank=True,那前端表单中不填是可以的.
若前端表单中填了,后端form表单验证时,会自动跨表过去看该值对应的记录是否存在!!
- 前端表单传过来的值是'9'
modelform的Meta fields中写的是pid > 验证成功后,form.clean_data['pid']的值是pid_id=9对应的对象
modelform的Meta fields中写的是pid_id > 验证成功后,form.clean_data['pid_id']的值是9
2
3
4
5
# Q4: save存储和修改
1. 在表单类中显式指明要处理的字段(field中是类变量“优先级更高”+ORM表中的字段)
- 尽管在类中重写了唯一索引字段,若重写了clean方法或者重写了_post_clean方法, 就不会在检验阶段 报该字段索引的错误!
但在利用modelform进行save时, 会报字段约束的错误!! (详见Q13)
2. 循环这些字段进行表单验证,(每个循环体都要经历 自身字段的验证和字段勾子的验证)
- 自身字段的验证 成功 > self.cleaned_data['字段名'] = 表单里对应的值
- 字段勾子的验证 成功 > self.cleaned_data['字段名'] = 钩子方法的返回值
3. 循环执行完后,执行clean方法
2
3
4
5
6
7
form表单只是对meta fields中的字段进行验证.
前端表单传不传(该表单项 有但不填 ; 该表单项直接没有) 取决于ORM表中该字段是否设置可为空!
(时间类型除外、当然你可以在表单类中重写这个字段对象, 字段自身验证时,是去request.data中拿值
default属性的设置是在save存储时起作用的!
验证通过后, save时用的是 form.instance对象, 该对象是去form.cleaned_data 中拿的值..
(但要注意,在视图函数的save之前, 改了form.cleaned_data某个字段的值, 不会作用于form.instance, 在表单类代码中改可以!!)
★★★ 明确两点
1. 查看了下源码,Form组件没有save方法! ModelForm组件有save方法!!
2. - 试验了下,ModelForm更新时,使用save,无需所有字段,前端表单中填的字段有一个是ORM表中的就行;
(多的字段是ORM表中没有的字段也无伤大雅
- 试验了下,ModelForm新增时,按照下面的步骤来,准没错!! (以用户新增为例)
1. 我们自己在fields属性中设置要检验哪些字段.(强烈不建议用__all__) > 对应前端表单中有哪些表单项!!
2. 在clean勾子方法中对form.cleaned_data的值进行处理.
<该项目中,前端表单中是密码重复密码,我们在表单类中自定义了这两个字段对象,
然后重写了password_hash字段并设置该字段不是必填!
在clean勾子中,验证两次密码是否一致,一致就给password_hash赋hash值>
3. 在视图的save之前,打印request.POST、form.cleaned_data、form.instance.__dict__
可以看到:
1> ORM表中外键字段名叫department、前端传过来的名也叫department,request.POST['"department"']值是 ["8"]
form.cleaned_data["department"] 会自己拿到对应的对象!
form.instance自动处理了 > form.instance.__dict__["department_id"] 其值是int类型的8
2> 因为ORM表中没有password和repassword字段,所有form.instance.__dict__中是 password_hash 字段!
3> form.instance在自动处理时,会添加在form.cleaned_data中没有,但在ORM表中有默认值的字段!
4> 我实验了下,在clean勾子中改form.cleaned_data中某个字段的值,会作用于form.instance
但在视图函数的save语句之前,改form.cleaned_data中某个字段的值,是不会作用于form.instance的!!
★ 无论是新增还是更新,其内部的本质是 将form.instance进行save!!不是将form.cleaned_data来进行save哦!
经过内部处理后,form.instance里面有的字段只会保留ORM表中的字段!!
PS:若你头铁,使用 self.model_class.objects.create(**form.cleaned_data) 来进行新增的话,必须确保一个不多一个不少!!
╮( ̄▽ ̄"")╭ 再多说一点,前后端分离的drf组件,使用save新增时,应该是一个不多一个不少!!多了save前pop,少了就给save传参!!
(¯﹃¯)因为其内部是用 校验成功后的数据进行的save!!
modelform __all__ 是会包含外键字段的(1对1、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
简单回顾下form表单验证流程
- step1: 循环modelform的Meta fields写的每个字段,每个字段正常情况下都会经历两个步骤
1> 字段自身的校验,校验通过放到cleaned_data属性中,校验不通过,字段的勾子方法不会执行,错误放到errors属性中
2> 执行字段的勾子方法,勾子方法中是不能通过cleaned_data属性拿到前面自身验证失败的字段的值哦!
- step2: 执行clean方法!! 错误放到__all__属性中!
if self.errors:
return self.cleaned_data # 源码中默认返回的就是它
# 没走if逻辑,那么self.cleaned_data中就必定能取到modelform的Meta fields写的每个字段的值!!
2
3
4
5
6
7
# Q5: 关于可为空 (了解)
关于为空的一点了解, 不必深究, 看看就好..
ORM表中 mobile = models.CharField(verbose_name="手机", max_length=11)
前端表单中 不管是 <没有name为mobile的表单项> or 还是 <有name为mobile的表单项,并且填了>
关键 ==> 只要 后端form表单类的field中,没有写mobile字段 > 意味着不验证该字段
★★★ 表单校验通过后,save后,mobile字段在数据库中的值为""空字符串!!
2
3
4
★ 注意, Django中默认null=False,blank=False.
null 针对数据库而言,该字段在数据库中能否存为NULL值,Django通常选用空字符串; blank针对表单验证时判断前端是否需要传这个字段的值
◎ null=Flase 表明该字段不能为空,CharField类型字段的空值分两种情况
username = models.CharField(verbose_name="登录名", max_length=128)
1. 传None值 User(username=None).save() 报错:(1048, "Column 'username' cannot be null")
2. 不传name字段的值 User().save() 不会报错
因为新增数据时,不给字段传值,对于字符串类型的字段,即使我们没有设置默认值,
Django也会自动将该数据类型下的默认值存储到数据库,在数据库中进行查看(这里字符串类型默认值是空字符串)
相当于 User(username='').save()
PS.username = models.CharField(verbose_name="登录名", max_length=128)
test = models.IntegerField(verbose_name="测试字段")
若你写 User().save() 会报错:(1048, "Column 'test' cannot be null")
> username是CharField,会默认传空字符串,test必须传值!!
◎ null=True
username = models.CharField(verbose_name="登录名", max_length=128, null=True)
1. User(username=None).save() 数据库中该字段值显示为NULL
2. User(username='').save() 数据库中该字段值空白的
◎ null=True default="H"
username = models.CharField(verbose_name="登录名", max_length=128, null=True,default="H")
1. 不给字段传值,会将默认值存入到数据库中 User().save()
2. 字段传入值设置为None,数据库中会存NULL User(username=None).save()
★ default="H", null=True, blank=True
-- 没填/表单中没有这个字段,新增成功后数据库中该字段显示默认值;
-- 表单中有这个字段,可以让它为空 为空,后端接收到的值是啥???是空字符串
print(request.POST) # <QueryDict: {'name': ['小明'], 'age': ['']}>
age = request.POST.get('age')
print(type(age)) # <class 'str'>
参考:https://blog.csdn.net/ProQianXiao/article/details/113748114
PS: 在Flask中默认可为空
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
# Q6: 会话保持用jwt还是session?
一开始,我是使用jwt来实现会话保持的, 但在实践的过程中, 发现在前后的部分分离的项目中, jwt不是很好实现对页面加载的控制,
其根源在于 前后的部分分离的项目 页面还是由后端来返回的.. (前后的完全分离的项目,页面是由前端服务器提供的)
这就导致了 能否加载这个页面 前端需要传递token给后端, 后端通过token来判断, 恰恰这个token不好传..
(eg: 登陆成功后 location.href = '/' 跳转首页, token写在url参数中吗? 一点都不安全.. 有些跳转还是在配置文件中的,更不好加token
So, 权衡利弊后, 我改为了用session来实现..
- 会话保持
- 在视图函数中判断登陆成功后,
request.session[settings.LOGIN_SESSION_KEY] = u_obj.id
- 在视图函数中判断登陆成功后,
- 登出
- 清除session,客户端和服务端的都清空
request.session.flush()
- 清除session,客户端和服务端的都清空
- 中间件
- 自定义中间件, 每个请求都必须登陆后才能访问/通过session来进行登陆验证,可设置白名单
session默认是在数据库中的,但以后我们可以将session放到memcache、redis等地方!!
★ 为什么去session中校验,而不是每次都去数据库中校验?联表查询性能会降低,减少数据库压力..
2
再深入一点, 在前后端完全分离的vue+drf项目中, 是怎样应用jwt的呢?
[vue+drf]
□ vue+drf 页面(前端控制)与数据api(前端请求,后端返回)是完全分开的!
登陆成功,后端返回前端token,前端放到localStorage中
前端再发送请求时,请求携带这个token到后端(token可在url?params、请求头、请求体中)到后端,后端拿到token,对该token进行验证!!
vue+drf前后端分离的项目中 vue全局导航守卫和axios拦截器可以实现!!
- > 前端页面的路由
vue全局路由守卫,根据localStorage有无token来判断是否能访问前端路由,可设置白名单
- > 前端页面发送axios请求对应的后端路由
axios请求拦截器在向后端发送axios请求时,会携带token; axios响应拦截器,根据后端对jwt token验证返回的code进行处理!
2
3
4
5
6
7
8
[layui+django]
□ django + layui 页面与数据api都是后端返回的!
但在前后的部分分离的场景中,比如登陆成功后跳转首页,是直接通过location.href = '/' 跳转的,
能否成功访问到这个页面(能否访问页面不像前后端分离一样是由前端的路由守卫决定的),这里是由后端代码决定的!!
后端如何决定呢,通过token,需要携带token给后端!
- 若使用jwt,请求页面是get请求,可将token放在url的?params参数中,这是很不安全的!!
而且,有些url是写在配置文件中的,不好处理
- 那使用session呢?
因而在前后端部分分离的场景中,我们可以使用session来实现 - 这样无论是啥请求,都可通过 request.session来判断是否已经登陆!!
request.session['xxx'] = 'dafadfka' > 会给前端一个cookies随机字符串 ; 数据库中 随机字符串:{'xxx':'dafadfka'}
前端携带着随机字符串过来时,后端根据随机字符串去数据库中匹配 拿到{'xxx':'dafadfka'}赋值给request.session
-> 使用session还有一个好处,可以通过request.session来进行权限的判断!!
PS:那些ajax请求,token可以放到请求体中
2
3
4
5
6
7
8
9
10
11
12
# Q7: 状态禁用、软删除、登陆权限认证?
- 动态菜单里进行的权限筛选,只是让用户没地方点击某些功能,防君子不防小人.
- 中间件里才是真正的防护!!防止用户通过api自己构造url发送请求进行操作.
- session进行登陆会话状态的保持/认证( 登陆才能访问某url
- session保存当前用户的所有权限/权限校验( 有权限才能访问某url
- 角色禁用 - 相当于让所有人都不具备这个角色 (分配了这个角色也没用).
权限禁用 - 相当于让所有人都不具备这个权限 (分配了这个权限也没用)
用户、角色、权限 > 软删除 [判断 - 中间件的权限验证;动态菜单;视图函数]
用户禁用 - [登陆逻辑里登陆失败;中间件里登陆认证失败]
[需判断 - 中间件的权限验证;动态菜单]
[需判断 - 中间件的权限验证;动态菜单]
逻辑上若中间件只进行了登陆认证, 没进行权限认证的拦截, 那么我只要登陆后, 后端写了的视图我都可以构建对应url进行访问!!
注:
- CURD的视图函数里但凡涉及查询, 即filter的地方,都得加上条件不是软删除..
- 在中间件里进行的登陆认证、权限认证和动态菜单里, 软删除和是否启用两个方面都得考虑!!
2
3
4
5
6
7
8
9
# Q8: 登陆-首页-动态菜单 发送了什么?
输入网址127.0.0.1:8000
, 跳转登陆页, 输入用户名和密码, 点击登陆按钮, 跳转首页.. 这是用户看到的, 背后发生了什么?
(该过程暂且不讨论中间件里的权限认证)
Ps: 思考这一过程, 其实让我显著认识到, 访问某页面/页面对应的路由, 有两个方向可实现, 后端可以重定向、render, 前端可以跳转..
1.用户输入网址127.0.0.1:8000,前端向后端发送请求,该请求走到后端的中间件时
因为请求里没有用户相关的session信息,所以登陆验证不通过,会被后端重定向页面到登陆页.
★ 每一个请求都会携带cookie到后端,都会先走中间件的流程!! 登陆页和登陆和登出在白名单里,直接就通过中间件啦.
2.用户在登陆页输入用户名和密码,点击登陆
○ 前端携带表单数据向后端发送ajax post的登陆请求!
○ 后端拿到请求的请求体里的表单数据,会进行如下的操作:
-1- 进行form组件的表单验证 (我只进行了字段自身的校验) 若校验通过,继续
-2- 用户名和密码是否匹配? 该账户是否可用? 该账号是否被注销? 依次进行验证 若验证通过,继续
-3- 利用session组件来保持登陆会话,简单来说
> 后端session表中 - '随机字符串':{'id':1} 前端cookies中 - 'sessionid':'随机字符串'
> 往后 前端/浏览器 发送的每个请求都会携带该cookies,
后端运行request.session代码时,会从当前请求中拿到随机字符串去session表中匹配
匹配成功后,将 {'id':1} 赋值给request.session
-4- 登陆成功,返回code=0,status=200的json信息!!
○ 前端接收到登陆成功的json信息,layer弹窗提示用户登陆成功,并携带session信息向后端发起跳转首页的请求.
○ 后端接收到跳转首页的请求. 首页不在白名单里,不能直接通过中间件.
so,在后端中间件里,从session中取到用户信息,判断session信息所对应的用户是否未启用?以及是否已被软删除?
(你想一下,若不这样做,当超级管理员让当前用户禁止使用后,当前用户只要不退出登陆,所发送的请求都能通过中间件.
也就是说,若不在中间件里来这么一手,只要该用户的session不过期,不主动退出登陆清除session信息,禁用对它来说不起作用!!
若不是,通过登陆认证,跳转首页; 否则,登陆认证不通过,跳转登陆页!!
○ 成功跳转首页后,前端会发生get请求,请求后端返回左侧动态菜单的信息.
○ 后端通过seesion信息里的用户id,可查询到当前登陆用户所拥有的所有角色(生效且未被软删除)的所有权限(生效且未被软删除).
因为权限类型分为菜单、节点、权限,只有菜单和节点能出现在左侧菜单上,所以得到的所有权限应该排除权限类型为权限的权限.
根据排除后的权限构建符合动态菜单的数据结构,并返回给前端.
○ 前端渲染出动态菜单!!
○ 页面中间部分的内容也是前端请求后端,通过iframe渲染到页面上的!!
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
# Q9: 简单的登陆, 前后端发送了什么?
此过程就是最为简单的登陆逻辑..
登陆逻辑
[前端] 自定义校验规则, 提交表单时自动触发校验规则
[前端] 提交表单, 发送ajax-post请求
[后端] 后端接收ajax请求, 取出请求体里的数据, 进行表单验证.
- 验证失败, 返回信息状态码 401, code=-1
- 验证成功, 看用户名和密码是否匹配
- 不匹配 401, code=-2
- 匹配 200, code=0;
★ 注意,Django request.POST拿不到json数据! 因为前端ajax请求应设置 contentType: "application/x-www-form-urlencoded",
1
2[前端] 接收后端返回的数据, 根据返回的信息状态码进行处理
返回信息状态码 200 - 将token持久化到本地 (若是jwt的话); 并跳转首页
返回信息状态码 不是200, 根据返回信息的code字段分别进行处理
- code = -2, 查询数据库信息不匹配 eg: 用户名或密码错误!
- code = -1, [后端] form表单字段校验不通过
★ 避免有人通过apifox进行访问跳过了前端验证.所以前端校验后,后端也要校验!! 这里我通过if-else if来判断的,当然也可以通过try捕获异常来解决.
1
2
注销逻辑
前端向后端发送 get、post请求都可, 若得到后端返回信息的code=0, 那么就清除浏览器token
# 若是jwt的话, To do(可有可无):jwt的撤销与黑名单. 后端来控制,撤销后就不允许它用之前的token了,用redis来实现.
1
# Q10: 用户权限的更新?
查看代码, 可以清楚的看到, 获取动态菜单的逻辑是orm语句查询得到的,中间件里的权限更新是依托于权限session..
用户权限更新后, 手动刷新页面后, 动态菜单要同步更新, 中间件中依赖的权限session的内容也要同步更新!!
大体逻辑, 登陆成功后, 查询一次数据库, 设置了权限session;
往后对用户权限进行了更改, 只要手动刷新页面.. 都会自动执行动态菜单的代码..
执行动态菜单相应ORM语句时, 非auth的作菜单, 非menu的用作更新权限session..
(这样的话,就不用在中间件里查询数据库获取当前用户的权限了 - 一切的一切都是为了每个请求来时, 对照的用户权限是最新的!)
不足之处: 这还是得依托于用户手动刷新, 想想也是, 用户不可能不刷新呀..
若不在获取动态菜单时更新权限session, 还有一个想法, 超级管理员让用户的session直接到期, 强行让其重新登陆获取自己最新的权限..
# Q11: 新增时, 传递is_super值?
我通过pastman工具, 传递了is_super字段值.
没用呀,因为我在field属性中我没有写is_super,压根不会检验它, form.clean_data中就不会有,
form.instance也从ORM表中拿的is_super的默认值!
# Q12: 关于日志?!
只希望记录对数据库的增、删、改的操作, 其余的请求不进行记录.
1.信号? 相当于操作数据库就可以给你提示,但错误这些提示信息怎么拿?
2.中间件,在request_response中进行判断
- 通过name值? url的name值是否包含 list、add、edit、del
- 通过请求的类型? get、post、put、delete
但原生Django解析不了 PUT、DELETE请求体的里内容呀!咋整? 在中间件里拦截处理下:
if request.method == "PUT": # 前端的ajax务必是 "application/x-www-form-urlencoded"
request.PUT = QueryDict(request.body)
if request.method == "DELETE": # 前端的ajax务必是 "application/x-www-form-urlencoded"
request.DELETE = QueryDict(request.body)
>> 综上,我最后采用 get、post两种请求方式,然后这个请求干啥的 > 通过状态码200返回的json数据里的msg字段,+状态码不是200来获知.
2
3
4
5
6
7
8
9
10
11
12
# Q13: 重写表单字段会覆盖数据表中对应字段的索引吗?
不能!!!源码大体流程如下: (指的是自动检测外键索引是否正确的操作
class BaseForm:
def is_valid(self):
return self.is_bound and not self.errors
@property
def errors(self):
if self._errors is None:
self.full_clean()
return self._errors
def full_clean(self):
self._clean_fields()
self._clean_form()
self._post_clean()
def _clean_fields(self):
pass
def _clean_form(self):
cleaned_data = self.clean()
class BaseModelForm(BaseForm):
def clean(self):
self._validate_unique = True ### !!
return self.cleaned_data
def _post_clean(self):
if self._validate_unique:
self.validate_unique()
def validate_unique(self):
self.instance.validate_unique(exclude=exclude)
class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
pass
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
所以说你在表单类里重写了clean方法, 或者 重写了_post_clean方法, 都不会在检验阶段 报该字段索引的错误!
但在利用modelform进行save时, 会报字段约束的错误!!