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实现的多线程端口扫描工具分享
Jan 21 Python
Python使用metaclass实现Singleton模式的方法
May 05 Python
Python实现识别图片内容的方法分析
Jul 11 Python
Django框架的中的setting.py文件说明详解
Oct 15 Python
Python selenium根据class定位页面元素的方法
Feb 26 Python
复化梯形求积分实例——用Python进行数值计算
Nov 20 Python
Python内置类型性能分析过程实例
Jan 29 Python
python GUI库图形界面开发之PyQt5选项卡控件QTabWidget详细使用方法与实例
Mar 01 Python
Django获取model中的字段名和字段的verbose_name方式
May 19 Python
Django之富文本(获取内容,设置内容方式)
May 21 Python
Pycharm学生免费专业版安装教程的方法步骤
Sep 24 Python
Flask使用SQLAlchemy实现持久化数据
Jul 16 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
PHP strtok()函数的优点分析
2010/03/02 PHP
PHP二维数组的去重问题解析
2011/07/17 PHP
鸡肋的PHP单例模式应用详解
2013/06/03 PHP
php中unserialize返回false的解决方法
2014/09/22 PHP
推荐几款用 Sublime Text 开发 Laravel 所用到的插件
2014/10/30 PHP
实例分析PHP中PHPMailer发邮件
2017/12/13 PHP
thinkphp5.1框架模板布局与模板继承用法分析
2019/07/19 PHP
jQuery的链式调用浅析
2010/12/03 Javascript
jquery随意添加移除html的实现代码
2011/06/21 Javascript
jQuery控制TR显示隐藏的三种常用方法
2014/08/21 Javascript
js和jq使用submit方法无法提交表单的快速解决方法
2016/05/17 Javascript
AngularJS 模块详解及简单实例
2016/07/28 Javascript
JavaScript生成验证码并实现验证功能
2016/09/24 Javascript
利用 spin.js 生成等待效果(js 等待效果)
2017/06/25 Javascript
在vue中使用vue-echarts-v3的实例代码
2018/09/13 Javascript
vue中实现点击按钮滚动到页面对应位置的方法(使用c3平滑属性实现)
2019/12/29 Javascript
[01:02:53]DOTA2上海特级锦标赛主赛事日 - 5 总决赛Liquid VS Secret第二局
2016/03/06 DOTA
[01:56]生活中的妖精之七夕特别档
2016/08/09 DOTA
[02:44]重置世界,颠覆未来——DOTA2 7.23版本震撼上线
2019/12/01 DOTA
Python数据结构之Array用法实例
2014/10/09 Python
pandas计数 value_counts()的使用
2019/06/24 Python
python实现输入三角形边长自动作图求面积案例
2020/04/12 Python
印度最大的旅游网站:MakeMyTrip
2016/10/05 全球购物
全球性的在线购物网站:Zapals
2017/03/22 全球购物
RetroStage德国:复古服装
2019/02/03 全球购物
adidas泰国官网:adidas TH
2020/07/11 全球购物
新加坡鲜花速递/新加坡网上花店:Ferns N Petals
2020/08/29 全球购物
公司委托书范本
2014/04/04 职场文书
环保倡议书100字
2014/05/15 职场文书
办护照工作证明
2014/10/01 职场文书
授权委托书
2015/01/28 职场文书
房屋认购协议书
2015/01/29 职场文书
民事辩护词范文
2015/05/21 职场文书
golang 实现菜单树的生成方式
2021/04/28 Golang
win10如何开启ahci模式?win10开启ahci模式详细操作教程
2022/07/23 数码科技
win10截图快捷键win+shift+s没有反应无法截图怎么解决?
2022/08/14 数码科技