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 相关文章推荐
Windows下Python2与Python3两个版本共存的方法详解
Feb 12 Python
python中实现精确的浮点数运算详解
Nov 02 Python
python实现Windows电脑定时关机
Jun 20 Python
python 3.6.5 安装配置方法图文教程
Sep 18 Python
python的pytest框架之命令行参数详解(下)
Jun 27 Python
python自动化实现登录获取图片验证码功能
Nov 20 Python
Numpy将二维数组添加到空数组的实现
Dec 05 Python
Pandas时间序列重采样(resample)方法中closed、label的作用详解
Dec 10 Python
Python调用shell cmd方法代码示例解析
Jun 18 Python
Python如何定义接口和抽象类
Jul 28 Python
python中super()函数的理解与基本使用
Aug 30 Python
Python函数对象与闭包函数
Apr 13 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
150kHz到30Mhz完全冲浪手册
2020/03/20 无线电
新版PHP将向Java靠拢
2006/10/09 PHP
PHPAnalysis中文分词类详解
2014/06/13 PHP
dojo 之基础篇
2007/03/24 Javascript
从零开始学习jQuery (六) jquery中的AJAX使用
2011/02/23 Javascript
javascript 主动派发事件总结
2011/08/09 Javascript
JavaScript基础语法让人疑惑的地方小结
2012/05/23 Javascript
jquery插件开发注意事项小结
2013/06/04 Javascript
onbeforeunload与onunload事件异同点总结
2013/06/24 Javascript
教你如何使用firebug调试功能了解javascript闭包和this
2015/03/04 Javascript
JavaScript实现图片轮播的方法
2015/07/31 Javascript
js实现按钮颜色渐变动画效果
2015/08/20 Javascript
轻松掌握JavaScript策略模式
2016/08/25 Javascript
javascript函数中的3个高级技巧
2016/09/22 Javascript
js html5 css俄罗斯方块游戏再现
2016/10/17 Javascript
利用iscroll4实现轮播图效果实例代码
2017/01/11 Javascript
实现一个完整的Node.js RESTful API的示例
2017/09/29 Javascript
JS使用canvas中的measureText方法测量字体宽度示例
2019/02/02 Javascript
npm的lock机制解析
2019/06/20 Javascript
js实现数字从零慢慢增加到指定数字示例
2019/11/07 Javascript
ES6函数实现排它两种写法解析
2020/05/13 Javascript
[43:58]DOTA2上海特级锦标赛C组败者赛 Newbee VS Archon第二局
2016/02/27 DOTA
python list转置和前后反转的例子
2019/08/26 Python
python+gdal+遥感图像拼接(mosaic)的实例
2020/03/10 Python
python进行二次方程式计算的实例讲解
2020/12/06 Python
美国领先的奢侈美容零售商:Bluemercury
2017/07/26 全球购物
俄罗斯在线购买飞机票、火车票、巴士票网站:Tutu.ru
2020/03/16 全球购物
初一英语教学反思
2014/01/11 职场文书
《得道多助,失道寡助》教学反思
2014/04/19 职场文书
取保候审保证书
2014/04/30 职场文书
机动车登记业务委托书
2014/10/08 职场文书
学生检讨书怎么写
2014/10/09 职场文书
《七月的天山》教学反思
2016/02/19 职场文书
Pandas自定义选项option设置
2021/07/25 Python
SQL Server查询某个字段在哪些表中存在
2022/03/03 SQL Server
Python加密与解密模块hashlib与hmac
2022/06/05 Python