1. Django的設計模式
Django是基於pyton語言的一個比較全面的框架,採用了MVC設計模式,但是Django更關注於模型(Model)、模板(Template)和視圖(Views),稱為 MTV模式。各自職責如下:
| 層次 | 職責 |
|---|---|
| 模型(Model) | 即數據存取層。處理與數據相關的所有事務: 如何存取、如何驗證有效性、包含哪些行為以及數據之間的關係等。 |
| 視圖(View) | 即表現層。處理與表現相關的決定: 如何在頁面或其他類型文檔中進行顯示。模型與模板的橋樑。 |
| 模板(Template) | 即業務邏輯層。存取模型及調取恰當模板的相關邏輯。 |
從以上表述可以看出Django 視圖不處理用户輸入,而僅僅決定要展現哪些數據給用户,而Django 模板 僅僅決定如何展現Django視圖指定的數據。或者説, Django將MVC中的視圖進一步分解為 Django視圖 和 Django模板兩個部分,分別決定 “展現哪些數據” 和 “如何展現”,使得Django的模板可以根據需要隨時替換,而不僅僅限制於內置的模板。至於MVC控制器部分,由Django框架的URLconf來實現。URLconf機制是使用正則表達式匹配URL,然後調用合適的Python函數。URLconf對於URL的規則沒有任何限制,你完全可以設計成任意的URL風格,不管是傳統的,RESTful的,或者是另類的。框架把控制層給封裝了,無非與數據交互這層都是數據庫表的讀,寫,刪除,更新的操作.在寫程序的時候,只要調用相應的方法就行了,感覺很方便。程序員把控制層東西交給Django自動完成了。 只需要編寫非常少的代碼完成很多的事情。所以,它比MVC框架考慮的問題要深一步,因為我們程序員大都在寫控制層的程序。現在這個工作交給了框架,僅需寫很少的調用代碼,大大提高了工作效率。
2. 前後端分離
在Django中實現前後端分離,通常意味着後端(使用Django)只提供API接口,而前端(可能是Web頁面、移動應用或其他客户端)通過HTTP請求來獲取和提交數據。前後端分離的思想,就是前端負責界面交互和美觀,後端負責數據管理和數據輸出。前端和後端的通信,完全基於 API 來處理。
首先,説明下格式問題。前端找後端要數據,後端給數據,前端拿數據,都是有特定格式的,這種格式是前後端兩個規則好的。所以在這裏,以 json 格式為例,json 也是前端編程語言 javascript 的對象結構。
3. 使用Django和Vue構建前後端分離的WEB應用
3.1 後端定義API
在app1.urls.py中定義4個API,分別實現提交註冊表單、提交登錄表單、訪問主頁,驗證是否登錄和刷新圖片驗證碼功能。
#文件名:app1.urls.py
from django.urls import path
from app1 import views
urlpatterns = [
path("register", views.RegisterResponse.as_view()), # 提交註冊表單
path("login", views.LoginResponse.as_view(), name="app1-login"), # 提交登錄表單
path("home", views.home, name="home"), # 訪問主頁,驗證是否登錄
path("get_image", views.VerificationCode.as_view()), # 刷新圖片驗證碼
]
3.2 後端實現視圖函數
4個視圖函數與3.1中的4個API一一對應。
#文件名:app1.views.py
import json
from django.http import HttpResponse, JsonResponse
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.views import View
from .form import RegisterForm, LoginForm
from verification_code import load_image
class RegisterResponse(View):
def post(self, request):
data = json.loads(request.body)
form = RegisterForm(data)
if form.is_valid:
User.objects.create_user(username=form.username, password=form.password)
form.info = form.register_success
form.status = 1
else:
form.info = "\n".join(form.error)
form.status = -1
return JsonResponse(form.response)
class LoginResponse(View):
def post(self, request):
data = json.loads(request.body)
form = LoginForm(data)
if form.code != request.session["code"]:
form.info = form.authentication_failure
form.status = -1
return JsonResponse(form.response)
if form.is_valid:
user = authenticate(request, username=form.username, password=form.password)
if user:
login(request, user) if user.is_active else None
form.info = form.login_success
form.status = 1
else:
form.info = form.login_failure
form.status = -1
else:
error = "\n".join(form.error)
form.info = error
form.status = -1
return JsonResponse(form.response)
def home(request):
if request.user.is_authenticated:
return JsonResponse({"view": "home"})
else:
return JsonResponse({"view": "login"})
class VerificationCode(View):
def get(self, request):
code, data = load_image()
if code and data:
request.session["code"] = code
return HttpResponse(data)
else:
return HttpResponse("")
視圖函數中用到的用户名密碼格式校驗、隨機驗證碼圖片生成見app1.form.py和django_study.verification_code.__init__.py。
#文件名:app1.form.py
from django.contrib.auth.models import User
from string import ascii_lowercase, ascii_uppercase
class AbstractForm:
def __init__(self, data):
self.error = list()
self.info = ""
self.status = 0
self.username = data.get("username") if "username" in data.keys() else ""
self.password = data.get("password") if "password" in data.keys() else ""
self.code = data.get("code") if "username" in data.keys() else "" # 驗證碼
self.username_valid = False
self.password_valid = False
# self.is_valid = self.username_valid and self.password_valid
@property
def is_valid(self):
return self.username_valid and self.password_valid
@property
def response(self):
return {"info": self.info, "status": self.status}
class RegisterForm(AbstractForm):
def __init__(self, data: dict):
super(RegisterForm, self).__init__(data)
self.username_valid = self.username_strategy()
self.password_valid = self.password_strategy()
self.register_success = "註冊成功"
self.register_failure = "\n".join(self.error)
def username_strategy(self):
res = True
query = User.objects.filter(username=self.username)
if query:
self.error.append("用户{}已存在".format(self.username))
res = False
if len(self.username) > 30:
self.error.append("用户名長度超過30")
res = False
if len(self.username) < 6:
self.error.append("用户名長度小於6")
res = False
if len(self.username) < 1:
self.error.append("用户名不能為空")
res = False
return res
def password_strategy(self):
res = True
upper_exists = False
lower_exists = False
for letter in self.password:
if letter in ascii_uppercase:
upper_exists = True
break
for letter in self.password:
if letter in ascii_lowercase:
lower_exists = True
break
if len(self.password) < 8:
self.error.append("密碼長度小於8")
res = False
if not upper_exists:
self.error.append("密碼中不包含大寫字母")
res = False
if not lower_exists:
self.error.append("密碼中不包含小寫字母")
res = False
return res
class LoginForm(AbstractForm):
def __init__(self, data: dict):
super(LoginForm, self).__init__(data)
self.username_valid = self.username or None
self.password_valid = self.password or None
if not self.username:
self.error.append("用户名不能為空")
if not self.password:
self.error.append("密碼不能為空")
self.login_success = "登錄成功"
self.login_failure = "\n".join(self.error)
self.authentication_failure = "驗證碼錯誤"
#文件名:django_study.verification_code.__init__.py
import numpy as np
import cv2
import os
import base64
def randcolor(lower=0, upper=255):
return np.random.randint(lower, upper), np.random.randint(lower, upper), np.random.randint(lower, upper)
def randchar():
lower = chr(np.random.randint(97, 122))
upper = chr(np.random.randint(65, 90))
number = str(np.random.randint(0, 9))
return np.random.choice([lower, upper, number])
def randpos(x0, x1, y0, y1):
return np.random.randint(x0, x1), np.random.randint(y0, y1)
def load_image(image_width = 120, image_height = 30, num = 4, path = "./"):
# 創建背景圖片
img = np.random.randint(181, 255, (image_height, image_width, 3), np.uint8)
image_name = ""
x_pos = 0
y_pos = int(image_height / 2)
for i in range(num): # num:驗證碼字符個數
ch = randchar()
image_name += ch
# 添加文字
cv2.putText(
img,
ch,
(np.random.randint(x_pos, x_pos + int(image_width / (num + 1))),
np.random.randint(y_pos, y_pos + int(image_height / 2))), # 座標
cv2.FONT_HERSHEY_SIMPLEX,
0.8, # 字號
randcolor(0, 180),
2,
cv2.LINE_AA
)
# 添加直線作為干擾信息
x_pos += int(image_width / (num + 1))
img = cv2.line(
img,
randpos(0, image_width, 0, image_height),
randpos(0, image_width, 0, image_height),
randcolor(),
np.random.randint(1, 2)
)
file = os.path.join(path, image_name + ".jpg")
try:
cv2.imwrite(file, img)
if os.path.isfile(file):
with open(file, "rb") as f:
data = base64.b64encode(f.read()).decode('utf-8')
os.unlink(file)
return image_name, data # 返回驗證碼字符串和base64編碼的圖像數據
else:
return None, None
except:
return None, None
3.3 前端路由定義
//文件名:./src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter);
const routes = [
{
path: '/',
component: () => import('../views/home'),
redirect: '/home',
meta: {
title: 'Django Study'
}
},
{
path: '/home',
component: () => import('../views/home'),
meta: {
title: 'Django Study'
}
},
{
path: '/login',
component: () => import('../views/login'),
meta: {
title: 'Login'
}
},
{
path: '/register',
component: () => import('../views/register'),
meta: {
title: 'Register'
}
},
];
const router = new VueRouter({
mode: 'history',
routes
});
export default router
3.4 註冊頁面
<!--文件名:./src/view/register.vue-->
<template>
<div>
<div>
<span>新用户</span>
<label>
<input v-model="username" type="text"/>
</label>
</div>
<div>
<span>請輸入新密碼</span>
<label>
<input v-model="password" type="text"/>
</label>
</div>
<div>
<span>請確認新密碼</span>
<label>
<input v-model="password2" type="text"/>
</label>
</div>
<div>
<button @click="registerSubmit">提交</button>
<span id="tologin" @click="toLogin">已有賬户?</span>
</div>
</div>
</template>
<script>
export default {
name: "register",
data() {
return {
username: "",
password: "",
password2: "",
info: "",
status: 0,
}
},
methods: {
registerSubmit () {
if (this.password !== this.password2) {
console.log("密碼不一致")
}
else {
this.$axios({
method: "POST",
url: "/api/app1/register",
data: {
username: this.username,
password: this.password,
}
}).then((res) => {
const data = res.data;
this.info = data.info;
this.status = data.status;
if (this.status === 1) {
alert("註冊成功:" + this.username);
} else if (this.status === -1) {
alert("註冊失敗:" + this.info);
}
else {}
})
}
},
toLogin () {
this.$router.push("/login")
}
}
}
</script>
<style scoped>
#tologin {
color: #f00;
}
#tologin:hover {
cursor: pointer;
}
</style>
3.5 登錄頁面
<!--文件名:./src/view/login.vue-->
<template>
<div>
<div>
<span>用户名</span>
<label>
<input v-model="username" type="text"/>
</label>
</div>
<div>
<span>密碼</span>
<label>
<input v-model="password" type="password"/>
</label>
</div>
<div>
<span>驗證碼</span>
<label>
<input v-model="verificationCode" type="text"/>
</label>
<img
style="width: 120px; height: 30px"
:src="'data:image/png;base64,' + imageCode"
@click="getImage"
alt=""
>
</div>
<div>
<button @click="loginSubmit">確認</button>
<button @click="toRegister">註冊</button>
</div>
</div>
</template>
<script>
export default {
name: "login",
mounted() {
this.getImage();
},
data() {
return {
username: "",
password: "",
verificationCode: "", //驗證碼
imageCode: "", // 圖片
}
},
methods: {
loginSubmit () {
this.$axios({
method: "POST",
url: "/api/app1/login",
data: {
username: this.username,
password: this.password,
code: this.verificationCode,
}
}).then((res) => {
const data = res.data;
this.info = data.info;
this.status = data.status;
if (this.status === 1) {
alert("登錄成功:" + this.username);
this.$router.back();
} else if (this.status === -1) {
alert("登錄失敗:" + this.info);
this.getImage();
}
else {}
})
},
getImage () {
this.$axios({
method: "get",
url: "/api/app1/get_image",
}).then(res => {
this.imageCode = res.data;
console.log(res.data);
})
},
toRegister () {
this.$router.push("/register")
}
}
}
</script>
<style scoped>
</style>
3.6 主頁
<!--文件名:./src/view/home.vue-->
<template>
<div style="overflow-y: auto; overflow-x: auto; height: 100%">
home
</div>
</template>
<script>
export default {
name: "home",
mounted() {
this.$axios({
method: "get",
url: "/api/app1/home"
}).then(res => {
const view = res.data.view;
console.log("/" + view)
this.$router.push("/" + view);
})
},
components: {
},
data() {
return {
}
},
}
</script>
<style scoped>
</style>
4. 測試
略