侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Django工程的分层结构详解

框架(架构)  /  管理员 发布于 7年前   380

前言

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

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

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

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

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

# urls.pypath('hello/', Hello),path('helloworld/', HelloWorld.as_view())# Viewfrom django.views import View# FVBdef Hello(request): if request.method == "GET": return HttpResponse("Hello world")# CVBclass 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

下图是整个工程的结构

Models层

models.py

from django.db import modelsfrom django.utils.translation import gettext as _# Create your models here.from django.contrib.auth.models import AbstractUser, UserManager, Userclass 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_mailfrom django.contrib.sites.shortcuts import get_current_sitefrom django.contrib.auth.tokens import PasswordResetTokenGeneratorfrom django.utils import sixfrom django.template.loader import render_to_stringfrom django.utils.http import urlsafe_base64_encode, urlsafe_base64_decodefrom django.utils.encoding import force_bytes, force_textfrom mysite import settingsclass 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 = TrueEMAIL_HOST = 'smtp.163.com'EMAIL_PORT = 465EMAIL_HOST_USER = '' # 发件人邮箱地址EMAIL_HOST_PASSWORD = '' # 发件人邮箱密码DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

Services层

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

"""Service层,针对不同用例实现的业务逻辑代码"""from django.utils.translation import gettext as _from django.shortcuts import renderfrom .utils import ( WelcomeEmail, TokenGenerator,)from users.models import ( UserAccount)class UsernameAlreadyExistError(Exception): passclass UserIdIsNotExistError(Exception): """ 用户ID,主键不存在 """ passclass ActivatitionTokenError(Exception): passclass 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涫的憧梢钥吹皆valid_data函数中我的确返回了True,但是我希望你也应该注意,如果用户存在的话我并没有返回False,而是raise一个异常,这样这个异常就会被调用方获取而且还能获取错误信息,这种方式将是一个很好的处理方式,具体你可以通过views.py中看到。

Forms表单验证

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

"""表单验证功能"""from django import formsfrom 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, HttpResponseRedirectfrom rest_framework.views import APIViewfrom django.utils.http import urlsafe_base64_encode, urlsafe_base64_decodefrom django.utils.encoding import force_bytes, force_textfrom .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) passclass 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, includefrom .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 admin中查看,2个用户是激活状态的。


  • 上一条:
    解决Django一个表单对应多个按钮的问题
    下一条:
    解决django中ModelForm多表单组合的问题
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • Filament v3.1版本发布(0个评论)
    • docker + gitea搭建一个git服务器流程步骤(0个评论)
    • websocket的三种架构方式使用优缺点浅析(0个评论)
    • ubuntu20.4系统中宿主机安装nginx服务,docker容器中安装php8.2实现运行laravel10框架网站(0个评论)
    • phpstudy_pro(小皮面板)中安装最新php8.2.9版本流程步骤(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2018-05
    • 2020-02
    • 2020-03
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-08
    • 2020-11
    • 2021-03
    • 2021-09
    • 2021-10
    • 2021-11
    • 2022-01
    • 2022-02
    • 2022-03
    • 2022-08
    • 2023-08
    • 2023-10
    • 2023-12
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客