Django工程的分层结构详解


Posted in Python onJuly 18, 2019

前言

传统上我们都知道在Django中的MTV模式,具体内容含义我们再来回顾一下:

M:是Model的简称,它的目标就是通过定义模型来处理和数据库进行交互,有了这一层或者这种类型的对象,我们就可以通过对象来操作数据。

V:是View的简称,它的工作很少,就是接受用户请求换句话说就是通过HTTP请求接受用户的输入;另外把输入信息发送给处理程并获取结果;最后把结果发送给用户,当然最后这一步还可以使用模板来修饰数据。

T:是Template的简称,这里主要是通过标记语言来定义页面,另外还可以嵌入模板语言让引擎来渲染动态数据。

这时候我们看到网上大多数的列子包括有些视频课程里面只讲MVT以及语法和其他功能实现等,但大家有没有想过一个问题,你的业务逻辑放在哪里?课程中的逻辑通常放在了View里面,就像下面:

# urls.py
path('hello/', Hello),
path('helloworld/', HelloWorld.as_view())

# View
from django.views import View

# FVB
def Hello(request):
 if request.method == "GET":
 return HttpResponse("Hello world")

# CVB
class HelloWorld(View):
 def get(self, request):
 pass
 def post(self, request):
 pass

无论是FBV还是CBV,当用户请求进来并通过URL路由找到对应的方法或者类,然后对请求进行处理,比如可以直接返回模型数据、验证用户输入或者校验用户名和密码等。在学习阶段或者功能非常简单的时候使用这种写法没问题,但是对于相对大一点的项目来说你很多具体的处理流程开始出现,而这些东西都写到View里显然你自己都看不下去。

FBV全名Function-based views,基于函数的视图;CBV全名Class-based views,基于类的视图

所以View,它就是一个控制器,它不应该包含业务逻辑,事实上它应该是一个很薄的层。

业务逻辑到底放哪里

网上也有很多文章回答了这个问题,提到了Form层,这个其实是用于验证用户输入数据的格式,比如邮件地址是否正确、是否填写了用户名和密码,至于这个用户名或者邮箱到底在数据库中是否真实存在则不是它应该关心的,它只是一个数据格式验证器。所以业务逻辑到底放哪里呢?显然要引入另外一层。

关于这一层的名称有些人叫做UseCase,也有些人叫做Service,至于什么名字无所谓只要是大家一看就明白的名称就好。如果我们使用UseCase这个名字,那么我们的Djaong工程架构就变成了MUVT,如果是Service那么就MSVT。

这一层的目标是什么呢?它专注于具体业务逻辑,也就是不同用例的具体操作,比如用户注册、登陆和注销都一个用例。所有模型都只是工作流程的一部分并且这一层也知道模型有哪些API。这么说有些空洞,我们用一个例子来说明:

场景是用户注册:

  • 信息填写规范且用户不存在则注册成功并发送账户激活邮件
  • 如果用户已存在则程序引发错误,然后传递到上层并进行告知用户名已被占用

Django 2.2.1、Python 3.7

下图是整个工程的结构

Django工程的分层结构详解

Models层

models.py

from django.db import models
from django.utils.translation import gettext as _

# Create your models here.

from django.contrib.auth.models import AbstractUser, UserManager, User

class UserAccountManager(UserManager):
 # 管理器
 def find_by_username(self, username):
 queryset = self.get_queryset()
 return queryset.filter(username=username)


class UserAccount(AbstractUser):
 # 扩展一个字段,家庭住址
 home_address = models.CharField(_('home address'), max_length=150, blank=True)
 # 账户是否被激活,与users表里默认的is_active不是一回事
 is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新账户注册后是否通过邮件验证激活。'),)

 # 指定该模型的manager类
 objects = UserAccountManager()

我们知道Django会为我们自动建立一个叫做auth_user的表,也就是它自己的认证内容,这个user表本身就是一个模型,它就是继承了AbstractUser类,而这个类有继承了AbstractBaseUser,而这个类继承了models.Model,所以我们这里就是一个模型。再说回AbstractUser类,这个类里面定义了一些username、first_name、email、is_active等用户属性相关的字段,如果你觉得不够用还可以自己扩展。

为了让Django使用我们扩展的用户模型,所以需要在settings.py中添加如下内容:

AUTH_USER_MODEL = "users.UserAccount"

工具类

这个文件主要是放一些通用工具,比如发送邮件这种公共会调用的功能,utils.py内容如下:

from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from mysite import settings


class TokenGenerator(PasswordResetTokenGenerator):
 def __init__(self):
 super(TokenGenerator, self).__init__()

 # def _make_hash_value(self, user, timestamp):
 # return (
 #  six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)
 # )


class WelcomeEmail:
 subject = 'Activate Your Account'

 @classmethod
 def send_to(cls, request, user_account):
 try:
  current_site = get_current_site(request)
  account_activation_token = TokenGenerator()
  message = render_to_string('activate_account.html', {
  'username': user_account.username,
  'domain': current_site.domain,
  'uid': urlsafe_base64_encode(force_bytes(user_account.id)),
  'token': account_activation_token.make_token(user_account),
  })

  send_mail(
  subject=cls.subject,
  message=message,
  from_email=settings.EMAIL_HOST_USER,
  recipient_list=[user_account.email]
  )
 except Exception as err:
  print(err)

TokenGenerator这个东西使用还是它父类本身的功能,之所以这样做是为了在必要的时候可以重写一些功能。父类PasswordResetTokenGenerator的功能主要是根据用户主键来生成token,之后还会根据传递的token和用户主键去检查传递的token是否一致。

针对邮件发送我这里使用Django提供的封装,你需要在settings.py中添加如下内容:

# 邮件设置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = '' # 发件人邮箱地址
EMAIL_HOST_PASSWORD = '' # 发件人邮箱密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

Services层

这层主要是根据用例来实现业务逻辑,比如注册用户账号和激活用户账号。

"""
Service层,针对不同用例实现的业务逻辑代码
"""
from django.utils.translation import gettext as _
from django.shortcuts import render
from .utils import (
 WelcomeEmail,
 TokenGenerator,
)
from users.models import (
 UserAccount
)


class UsernameAlreadyExistError(Exception):
 pass


class UserIdIsNotExistError(Exception):
 """
 用户ID,主键不存在
 """
 pass


class ActivatitionTokenError(Exception):
 pass


class RegisterUserAccount:
 def __init__(self, request, username, password, confirm_password, email):
 self._username = username
 self._password = password
 self._email = email
 self._request = request

 def valid_data(self):
 """
 检查用户名是否已经被注册
 :return:
 """
 user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
 if user_query_set:
  error_msg = ('用户名 {} 已被注册,请更换。'.format(self._username))
  raise UsernameAlreadyExistError(_(error_msg))
 return True

 def _send_welcome_email_to(self, user_account):
 """
 注册成功后发送电子邮件
 :param user_account:
 :return:
 """
 WelcomeEmail.send_to(self._request, user_account)

 def execute(self):
 self.valid_data()
 user_account = self._factory_user_account()
 self._send_welcome_email_to(user_account)
 return user_account

 def _factory_user_account(self):
 """
 这里是创建用户
 :return:
 """

 # 这样创建需要调用save()
 # ua = UserAccount(username=self._username, password=self._password, email=self._email)
 # ua.save()
 # return ua

 # 直接通过create_user则不需要调用save()
 return UserAccount.objects.create_user(
  self._username,
  self._email,
  self._password,
 )


class ActivateUserAccount:
 def __init__(self, uid, token):
 self._uid = uid
 self._token = token

 def _account_valid(self):
 """
 验证用户是否存在
 :return: 模型对象或者None
 """
 return UserAccount.objects.all().get(id=self._uid)

 def execute(self):
 # 查询是否有用户
 user_account = self._account_valid()
 account_activation_token = TokenGenerator()
 if user_account is None:
  error_msg = ('激活用户失败,提供的用户标识 {} 不正确,无此用户。'.format(self._uid))
  raise UserIdIsNotExistError(_(error_msg))

 if not account_activation_token.check_token(user_account, self._token):
  error_msg = ('激活用户失败,提供的Token {} 不正确。'.format(self._token))
  raise ActivatitionTokenError(_(error_msg))

 user_account.is_activated = True
 user_account.save()
 return True

这里定义的异常类比如UsernameAlreadyExistError等里面的内容就是空的,目的是raise异常到自定义的异常中,这样调用方通过try就可以捕获,有些时候代码执行的结果影响调用方后续的处理,通常大家可能认为需要通过返回值来判断,比如True或者False,但通常这不是一个好办法或者说在有些时候不是,因为那样会造成代码冗长,比如下面的代码:

这是上面代码中的一部分,

def valid_data(self):
 """
 检查用户名是否已经被注册
 :return:
 """
 user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
 if user_query_set:
  error_msg = ('用户名 {} 已被注册,请更换。'.format(self._username))
  raise UsernameAlreadyExistError(_(error_msg))
 return True

 def execute(self):
 self.valid_data()
 user_account = self._factory_user_account()
 self._send_welcome_email_to(user_account)
 return user_account

execute函数会执行valid_data()函数,如果执行成功我才会向下执行,可是你看我在execute函数中并没有这样的语句,比如:

def execute(self):
 if self.valid_data():
 user_account = self._factory_user_account()
 self._send_welcome_email_to(user_account)
 return user_account
 else:
 pass

换句话说你的每个函数都可能有返回值,如果每一个你都这样写代码就太??铝恕F涫的憧梢钥吹皆?alid_data函数中我的确返回了True,但是我希望你也应该注意,如果用户存在的话我并没有返回False,而是raise一个异常,这样这个异常就会被调用方获取而且还能获取错误信息,这种方式将是一个很好的处理方式,具体你可以通过views.py中看到。

Forms表单验证

这里是对于用户输入做检查

"""
表单验证功能
"""
from django import forms
from django.utils.translation import gettext as _


class RegisterAccountForm(forms.Form):
 username = forms.CharField(max_length=50, required=True, error_messages={
 'max_length': '用户名不能超过50个字符',
 'required': '用户名不能为空',
 })
 
 email = forms.EmailField(required=True)
 password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
 confirm_password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())

 def clean_confirm_password(self) -> str: # -> str 表示的含义是函数返回值类型是str,在打印函数annotation的时候回显示。
 """
 clean_XXXX XXXX是字段名
 比如这个方法是判断两次密码是否一致,密码框输入的密码就算符合规则但是也不代表两个密码一致,所以需要自己来进行检测
 :return:
 """
 password = self.cleaned_data.get('password')
 confirm_password = self.cleaned_data.get('confirm_password')

 if confirm_password != password:
  raise forms.ValidationError(message='Password and confirmation do not match each other')

 return confirm_password

前端可以实现输入验证,但是也很容易被跳过,所以后端肯定也需要进行操作,当然我这里并没有做预防XSS攻击的措施,因为这个不是我们今天要讨论的主要内容。

Views

from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from rest_framework.views import APIView
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from .forms import (
 RegisterAccountForm,
)
from .services import (
 RegisterUserAccount,
 UsernameAlreadyExistError,
 ActivateUserAccount,
 ActivatitionTokenError,
 UserIdIsNotExistError,
)
# Create your views here.


class Register(APIView):
 def get(self, request):
 return render(request, 'register.html')

 def post(self, request):
 # print("request.data 的内容: ", request.data)
 # print("request.POST 的内容: ", request.POST)

 # 针对数据输入做检查,是否符合规则
 ra_form = RegisterAccountForm(request.POST)
 if ra_form.is_valid():
  # print("验证过的数据:", ra_form.cleaned_data)
  rua = RegisterUserAccount(request=request, **ra_form.cleaned_data)
  try:
  rua.execute()
  except UsernameAlreadyExistError as err:
  # 这里就是捕获自定义异常,并给form对象添加一个错误信息,并通过模板渲染然后返回前端页面
  ra_form.add_error('username', str(err))
  return render(request, 'register.html', {'info': ra_form.errors})

  return HttpResponse('We have sent you an email, please confirm your email address to complete registration')
  # return HttpResponseRedirect("/account/login/")
 else:
  return render(request, 'register.html', {'info': ra_form.errors})


class Login(APIView):
 def get(self, request):
 return render(request, 'login.html')

 def post(self, request):
 print("request.data 的内容: ", request.data)
 print("request.POST 的内容: ", request.POST)
 pass


class ActivateAccount(APIView):
 # 用户激活账户
 def get(self, request, uidb64, token):
 try:
  # 获取URL中的用户ID
  uid = force_bytes(urlsafe_base64_decode(uidb64))
  # 激活用户
  aua = ActivateUserAccount(uid, token)
  aua.execute()
  return render(request, 'login.html')
 except(ActivatitionTokenError, UserIdIsNotExistError) as err:
  return HttpResponse('Activation is failed.')

这里就是视图层不同URL由不同的类来处理,这里只做基本的接收输入和返回输出功能,至于接收到的输入该如何处理则有其他组件来完成,针对输入格式规范则由forms中的类来处理,针对数据验证过后的具体业务逻辑则由services中的类来处理。

Urls

from django.urls import path, re_path, include
from .views import (
 Register,
 Login,
 ActivateAccount,
)


app_name = 'users'
urlpatterns = [
 re_path(r'^register/$', Register.as_view(), name='register'),
 re_path(r'^login/$', Login.as_view(), name='login'),
 re_path(r'^activate/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
  ActivateAccount.as_view(), name='activate'),
]

Templates

是我用到的html模板,我就不放在这里了

下载全部的代码

页面效果

Django工程的分层结构详解

激活邮件内容

Django工程的分层结构详解

Django工程的分层结构详解

点击后就会跳转到登陆页。下面我们从Django admin中查看,2个用户是激活状态的。

Python 相关文章推荐
python处理中文编码和判断编码示例
Feb 26 Python
用Python实现一个简单的能够发送带附件的邮件程序的教程
Apr 08 Python
剖析Python的Twisted框架的核心特性
May 25 Python
深入理解NumPy简明教程---数组2
Dec 17 Python
Python使用win32com实现的模拟浏览器功能示例
Jul 13 Python
python 通过字符串调用对象属性或方法的实例讲解
Apr 21 Python
Windows下安装Scrapy
Oct 17 Python
Python列表常见操作详解(获取,增加,删除,修改,排序等)
Feb 18 Python
如何使用Python标准库进行性能测试
Jun 25 Python
解决Python中报错TypeError: must be str, not bytes问题
Apr 07 Python
python字典key不能是可以是啥类型
Aug 04 Python
python爬取音频下载的示例代码
Oct 19 Python
django mysql数据库及图片上传接口详解
Jul 18 #Python
解决django中ModelForm多表单组合的问题
Jul 18 #Python
浅谈Django中view对数据库的调用方法
Jul 18 #Python
django-rest-framework解析请求参数过程详解
Jul 18 #Python
python Django中models进行模糊查询的示例
Jul 18 #Python
django-rest-framework 自定义swagger过程详解
Jul 18 #Python
django框架使用方法详解
Jul 18 #Python
You might like
Linux下将excel数据导入到mssql数据库中的方法
2010/02/08 PHP
全新的PDO数据库操作类php版(仅适用Mysql)
2012/07/22 PHP
php批量删除超链接的实现方法
2015/10/19 PHP
PHP实现的折半查找算法示例
2017/12/19 PHP
解决Laravel blade模板转义html标签的问题
2019/09/03 PHP
基于laravel Request的所有方法详解
2019/09/29 PHP
javascript 写类方式之八
2009/07/05 Javascript
方便实用的jQuery checkbox复选框全选功能简单实例
2013/10/09 Javascript
基于jquery的simpleValidate简易验证插件
2014/01/31 Javascript
Bootstrap每天必学之栅格系统(布局)
2015/11/25 Javascript
jQuery实现的tab标签切换效果示例
2016/09/05 Javascript
利用jquery实现瀑布流3种案例
2016/09/18 Javascript
jQuery实现立体式数字滚动条增加效果
2016/12/21 Javascript
VUEJS 2.0 子组件访问/调用父组件的实例
2018/02/10 Javascript
Vue使用mixins实现压缩图片代码
2018/03/14 Javascript
vue input 输入校验字母数字组合且长度小于30的实现代码
2018/05/16 Javascript
从零开始搭建webpack+react开发环境的详细步骤
2018/05/18 Javascript
10种JavaScript最常见的错误(小结)
2019/06/21 Javascript
vue transition 在子组件中失效的解决
2019/11/12 Javascript
[01:56]《DOTA2》中文配音CG
2013/04/22 DOTA
Python实现的科学计算器功能示例
2017/08/04 Python
利用python开发app实战的方法
2019/07/09 Python
PyQt 图解Qt Designer工具的使用方法
2019/08/06 Python
Pytorch学习之torch用法----比较操作(Comparison Ops)
2020/06/28 Python
Python tempfile模块生成临时文件和临时目录
2020/09/30 Python
Python判断变量是否是None写法代码实例
2020/10/09 Python
详解HTML5中的标签
2015/06/19 HTML / CSS
印尼穆斯林时尚购物网站:Hijabenka
2016/12/10 全球购物
美国最大婚纱连锁店运营商:David’s Bridal
2019/03/12 全球购物
会计人员岗位职责
2014/03/19 职场文书
关于中国梦的演讲稿
2014/04/23 职场文书
学校周年庆活动方案
2014/08/22 职场文书
2014年语文教学工作总结
2014/12/17 职场文书
2016年春季趣味运动会开幕词
2016/03/04 职场文书
python process模块的使用简介
2021/05/14 Python
聊聊redis-dump工具安装问题
2022/01/18 Redis