溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Django工程的分層結(jié)構(gòu)詳解

發(fā)布時間:2020-09-07 21:30:54 來源:腳本之家 閱讀:256 作者:laozhang 欄目:開發(fā)技術(shù)

前言

傳統(tǒng)上我們都知道在Django中的MTV模式,具體內(nèi)容含義我們再來回顧一下:

M:是Model的簡稱,它的目標就是通過定義模型來處理和數(shù)據(jù)庫進行交互,有了這一層或者這種類型的對象,我們就可以通過對象來操作數(shù)據(jù)。

V:是View的簡稱,它的工作很少,就是接受用戶請求換句話說就是通過HTTP請求接受用戶的輸入;另外把輸入信息發(fā)送給處理程并獲取結(jié)果;最后把結(jié)果發(fā)送給用戶,當然最后這一步還可以使用模板來修飾數(shù)據(jù)。

T:是Template的簡稱,這里主要是通過標記語言來定義頁面,另外還可以嵌入模板語言讓引擎來渲染動態(tài)數(shù)據(jù)。

這時候我們看到網(wǎng)上大多數(shù)的列子包括有些視頻課程里面只講MVT以及語法和其他功能實現(xiàn)等,但大家有沒有想過一個問題,你的業(yè)務(wù)邏輯放在哪里?課程中的邏輯通常放在了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路由找到對應(yīng)的方法或者類,然后對請求進行處理,比如可以直接返回模型數(shù)據(jù)、驗證用戶輸入或者校驗用戶名和密碼等。在學習階段或者功能非常簡單的時候使用這種寫法沒問題,但是對于相對大一點的項目來說你很多具體的處理流程開始出現(xiàn),而這些東西都寫到View里顯然你自己都看不下去。

FBV全名Function-based views,基于函數(shù)的視圖;CBV全名Class-based views,基于類的視圖

所以View,它就是一個控制器,它不應(yīng)該包含業(yè)務(wù)邏輯,事實上它應(yīng)該是一個很薄的層。

業(yè)務(wù)邏輯到底放哪里

網(wǎng)上也有很多文章回答了這個問題,提到了Form層,這個其實是用于驗證用戶輸入數(shù)據(jù)的格式,比如郵件地址是否正確、是否填寫了用戶名和密碼,至于這個用戶名或者郵箱到底在數(shù)據(jù)庫中是否真實存在則不是它應(yīng)該關(guān)心的,它只是一個數(shù)據(jù)格式驗證器。所以業(yè)務(wù)邏輯到底放哪里呢?顯然要引入另外一層。

關(guān)于這一層的名稱有些人叫做UseCase,也有些人叫做Service,至于什么名字無所謂只要是大家一看就明白的名稱就好。如果我們使用UseCase這個名字,那么我們的Djaong工程架構(gòu)就變成了MUVT,如果是Service那么就MSVT。

這一層的目標是什么呢?它專注于具體業(yè)務(wù)邏輯,也就是不同用例的具體操作,比如用戶注冊、登陸和注銷都一個用例。所有模型都只是工作流程的一部分并且這一層也知道模型有哪些API。這么說有些空洞,我們用一個例子來說明:

場景是用戶注冊:

  • 信息填寫規(guī)范且用戶不存在則注冊成功并發(fā)送賬戶激活郵件
  • 如果用戶已存在則程序引發(fā)錯誤,然后傳遞到上層并進行告知用戶名已被占用

Django 2.2.1、Python 3.7

下圖是整個工程的結(jié)構(gòu)

Django工程的分層結(jié)構(gòu)詳解

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的表,也就是它自己的認證內(nèi)容,這個user表本身就是一個模型,它就是繼承了AbstractUser類,而這個類有繼承了AbstractBaseUser,而這個類繼承了models.Model,所以我們這里就是一個模型。再說回AbstractUser類,這個類里面定義了一些username、first_name、email、is_active等用戶屬性相關(guān)的字段,如果你覺得不夠用還可以自己擴展。

為了讓Django使用我們擴展的用戶模型,所以需要在settings.py中添加如下內(nèi)容:

AUTH_USER_MODEL = "users.UserAccount"

工具類

這個文件主要是放一些通用工具,比如發(fā)送郵件這種公共會調(diào)用的功能,utils.py內(nèi)容如下:

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的功能主要是根據(jù)用戶主鍵來生成token,之后還會根據(jù)傳遞的token和用戶主鍵去檢查傳遞的token是否一致。

針對郵件發(fā)送我這里使用Django提供的封裝,你需要在settings.py中添加如下內(nèi)容:

# 郵件設(shè)置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = '' # 發(fā)件人郵箱地址
EMAIL_HOST_PASSWORD = '' # 發(fā)件人郵箱密碼
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

Services層

這層主要是根據(jù)用例來實現(xiàn)業(yè)務(wù)邏輯,比如注冊用戶賬號和激活用戶賬號。

"""
Service層,針對不同用例實現(xiàn)的業(yè)務(wù)邏輯代碼
"""
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):
 """
 檢查用戶名是否已經(jīng)被注冊
 :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):
 """
 注冊成功后發(fā)送電子郵件
 :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):
 """
 這里是創(chuàng)建用戶
 :return:
 """

 # 這樣創(chuàng)建需要調(diào)用save()
 # ua = UserAccount(username=self._username, password=self._password, email=self._email)
 # ua.save()
 # return ua

 # 直接通過create_user則不需要調(diào)用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等里面的內(nèi)容就是空的,目的是raise異常到自定義的異常中,這樣調(diào)用方通過try就可以捕獲,有些時候代碼執(zhí)行的結(jié)果影響調(diào)用方后續(xù)的處理,通常大家可能認為需要通過返回值來判斷,比如True或者False,但通常這不是一個好辦法或者說在有些時候不是,因為那樣會造成代碼冗長,比如下面的代碼:

這是上面代碼中的一部分,

def valid_data(self):
 """
 檢查用戶名是否已經(jīng)被注冊
 :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函數(shù)會執(zhí)行valid_data()函數(shù),如果執(zhí)行成功我才會向下執(zhí)行,可是你看我在execute函數(shù)中并沒有這樣的語句,比如:

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

換句話說你的每個函數(shù)都可能有返回值,如果每一個你都這樣寫代碼就太啰嗦了。其實你可以看到在valid_data函數(shù)中我的確返回了True,但是我希望你也應(yīng)該注意,如果用戶存在的話我并沒有返回False,而是raise一個異常,這樣這個異常就會被調(diào)用方獲取而且還能獲取錯誤信息,這種方式將是一個很好的處理方式,具體你可以通過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 表示的含義是函數(shù)返回值類型是str,在打印函數(shù)annotation的時候回顯示。
 """
 clean_XXXX XXXX是字段名
 比如這個方法是判斷兩次密碼是否一致,密碼框輸入的密碼就算符合規(guī)則但是也不代表兩個密碼一致,所以需要自己來進行檢測
 :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

前端可以實現(xiàn)輸入驗證,但是也很容易被跳過,所以后端肯定也需要進行操作,當然我這里并沒有做預(yù)防XSS攻擊的措施,因為這個不是我們今天要討論的主要內(nèi)容。

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 的內(nèi)容: ", request.data)
 # print("request.POST 的內(nèi)容: ", request.POST)

 # 針對數(shù)據(jù)輸入做檢查,是否符合規(guī)則
 ra_form = RegisterAccountForm(request.POST)
 if ra_form.is_valid():
  # print("驗證過的數(shù)據(jù):", 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 的內(nèi)容: ", request.data)
 print("request.POST 的內(nèi)容: ", 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由不同的類來處理,這里只做基本的接收輸入和返回輸出功能,至于接收到的輸入該如何處理則有其他組件來完成,針對輸入格式規(guī)范則由forms中的類來處理,針對數(shù)據(jù)驗證過后的具體業(yè)務(wù)邏輯則由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工程的分層結(jié)構(gòu)詳解

激活郵件內(nèi)容

Django工程的分層結(jié)構(gòu)詳解

Django工程的分層結(jié)構(gòu)詳解

點擊后就會跳轉(zhuǎn)到登陸頁。下面我們從Django admin中查看,2個用戶是激活狀態(tài)的。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI