开发模式 前后端不分离
后端框架 Django + Jinja2模板引擎
前端框架 Vue.js
1、码云(https://gitee.com/)
2、创建源码远程仓库:website
3、克隆项目代码仓库 新建文件夹 下载git https://git-scm.com/download/win
按默认方式安装。克隆项目:git clone https://gitee.com/lgc1286191951/website.git
1.进入本地项目仓库
$ cd website
2.创建美多商城虚拟环境,安装Django框架
$ mkvirtualenv -p python3 website
$ pip install django
3.创建Django工程
$ django-admin startproject website
创建工程完成后:运行程序,测试结果。
1.新建MySQL数据库:test
$ create database meiduo charset=utf8;
2.新建MySQL用户
$ create user test identified by ‘123456’;
DATABASES = {
‘default’: {
‘ENGINE’: ‘django.db.backends.mysql’, # 数据库引擎
‘HOST’: ‘127.0.0.1’, # 数据库主机
‘PORT’: 3306, # 数据库端口
‘USER’: ‘root’, # 数据库用户名root
‘PASSWORD’: ‘123456’, # 数据库用户密码
‘NAME’: ‘test’ # 数据库名字
},
}
可能出现的错误
Error loading MySQLdb module: No module named ‘MySQLdb’.
出现错误的原因:
Django中操作MySQL数据库需要驱动程序MySQLdb
目前项目虚拟环境中没有驱动程序MySQLdb
解决办法:
安装PyMySQL扩展包
因为MySQLdb只适用于Python2.x的版本,Python3.x的版本中使用PyMySQL替代MySQLdb
1.安装驱动程序
在windows下安装Mysql
$ pip install PyMySQL
2.在工程同名子目录的__init__.py文件中,添加如下代码:
from pymysql import install_as_MySQLdb
install_as_MySQLdb()
配置完成后:运行程序,测试结果。
1.安装django-redis扩展包
在windows下安装redis
$ pip install django-redis
2.django-redis使用说明文档
点击进入文档
CACHES = {
“default”: { # 默认
“BACKEND”: “django_redis.cache.RedisCache”,
“LOCATION”: “redis://127.0.0.1:6379/0”,
“OPTIONS”: {
“CLIENT_CLASS”: “django_redis.client.DefaultClient”,
}
},
“session”: { # session
“BACKEND”: “django_redis.cache.RedisCache”,
“LOCATION”: “redis://127.0.0.1:6379/1”,
“OPTIONS”: {
“CLIENT_CLASS”: “django_redis.client.DefaultClient”,
}
},
“verify_code”: { # 验证码
“BACKEND”: “django_redis.cache.RedisCache”,
“LOCATION”: “redis://127.0.0.1:6379/2”,
“OPTIONS”: {
“CLIENT_CLASS”: “django_redis.client.DefaultClient”,
}
},
}
SESSION_ENGINE = “django.contrib.sessions.backends.cache”
SESSION_CACHE_ALIAS = “session”
default:
默认的Redis配置项,采用0号Redis库。
session:
状态保持的Redis配置项,采用1号Redis库。
SESSION_ENGINE
验证码的Redis配置项,采用2号Redis库
verify_code
修改session存储机制使用Redis保存。
SESSION_CACHE_ALIAS:
使用名为"session"的Redis配置项存储session数据。
配置完成后:运行程序,测试结果。
STATIC_URL = ‘/static/’
配置静态文件加载路径
STATICFILES_DIRS = [os.path.join(BASE_DIR, ‘static’)]
配置完成后:运行程序,测试结果。
http://127.0.0.1:8000/static/images/test.png
1.准备apps包,用于管理所有应用
2.在apps包下创建应用users
$ cd ~/projects/meiduo_project/meiduo_mall/meiduo_mall/apps
$ python …/…/manage.py startapp users
重要提示:
若要知道如何导入users应用并完成注册,需要知道项目导包路径
已知导包路径
website/website
已知 'users’应用所在目录
website_project/website/website/apps/users
得到导入’users’应用的导包路路径是website/apps/user
class RegisterView(View):
“”“用户注册”""
def get(self, request):
"""
提供注册界面
:param request: 请求对象
:return: 注册界面
"""
return render(request, 'register.html')
urlpatterns = [
# users
url(r’^’, include(‘users.urls’, namespace=‘users’)),
]
2.子路由
urlpatterns = [
# 注册
url(r’^register/$’, views.RegisterView.as_view(), name=‘register’),
]
Django自带用户认证系统
它处理用户账号、组、权限以及基于cookie的用户会话。
Django认证系统位置
django.contrib.auth包含认证框架的核心和默认的模型。
django.contrib.contenttypes是Django内容类型系统,它允许权限与你创建的模型关联。
Django认证系统同时处理认证和授权
认证:验证一个用户是否它声称的那个人,可用于账号登录。
授权:授权决定一个通过了认证的用户被允许做什么。
Django认证系统包含的内容
用户:用户模型类、用户认证。
权限:标识一个用户是否可以做一个特定的任务,MIS系统常用到。
组:对多个具有相同权限的用户进行统一管理,MIS系统常用到。
密码:一个可配置的密码哈希系统,设置密码、密码校验。
Django认证系统中提供了用户模型类User保存用户的数据。
User对象是认证系统的核心。
Django认证系统用户模型类位置
django.contrib.auth.models.User
父类AbstractUser介绍
User对象基本属性
创建用户(注册用户)必选: username、password
创建用户(注册用户)可选:email、first_name、last_name、last_login、date_joined、is_active 、is_staff、is_superuse
判断用户是否通过认证(是否登录):is_authenticated
创建用户(注册用户)的方法
user = User.objects.create_user(username, email, password, **extra_fields)
用户认证(用户登录)的方法
from django.contrib.auth import authenticate
user = authenticate(username=username, password=password, **kwargs)
处理密码的方法
设置密码:set_password(raw_password)
校验密码:check_password(raw_password)
3. 自定义用户模型类
思考:为什么要自定义用户模型类?
但是Django默认用户模型类中没有mobile字段,所以要自定义用户模型类。
如何自定义用户模型类?
Django默认用户模型类的文档
继承自AbstractUser(可通过阅读Django默认用户模型类的源码得知) 。
新增mobile字段。
from django.db import models
from django.contrib.auth.models import AbstractUser
Create your models here.
class User(AbstractUser):
“”“自定义用户模型类”""
mobile = models.CharField(max_length=11, unique=True, verbose_name=‘手机号’)
class Meta:
db_table = 'tb_users'
verbose_name = '用户'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
思考:为什么Django默认用户模型类是User?
阅读源代码:‘django.conf.global_settings’
AUTH_USER_MODEL = ‘auth.User’
结论:
Django用户模型类是通过全局配置项 AUTH_USER_MODEL 决定的
配置规则:
在settings.py下进行配置
AUTH_USER_MODEL = ‘应用名.模型类名’
指定本项目用户模型类
AUTH_USER_MODEL = ‘users.User’
1.创建迁移文件
python manage.py makemigrations
2.执行迁移文件
对于接口的设计,我们要根据具体的业务逻辑,设计出适合业务逻辑的接口。
设计接口的思路:
分析要实现的业务逻辑:
明确在这个业务中涉及到几个相关子业务。
将每个子业务当做一个接口来设计。
分析接口的功能任务,明确接口的访问方式与返回数据:
请求方法(如GET、POST、PUT、DELETE等)。
请求地址。
请求参数(如路径参数、查询字符串、表单、JSON等)。
响应数据(如HTML、JSON等)。
1.请求方式
选项 方案
请求方法 POST
请求地址 /register/
2.请求参数:表单参数
参数名 类型 是否必传 说明
username string 是 用户名
password string 是 密码
password2 string 是 确认密码
mobile string 是 手机号
allow string 是 是否同意用户协议
3.响应结果:HTML
register.html
响应结果 响应内容
注册失败 响应错误提示
注册成功 重定向到首页
1.注册视图
class RegisterView(View):
“”“用户注册”""
def get(self, request):
"""
提供注册界面
:param request: 请求对象
:return: 注册界面
"""
return render(request, 'register.html')
def post(self, request):
"""
实现用户注册
:param request: 请求对象
:return: 注册结果
"""
pass
2.总路由
urlpatterns = [
# users
url(r’^’, include(‘users.urls’, namespace=‘users’)),
]
3.子路由
urlpatterns = [
# 注册
url(r’^register/$’, views.RegisterView.as_view(), name=‘register’),
]
为了学会使用Vue.js的双向绑定实现用户的交互和页面局部刷新效果。
绑定内容:变量、事件、错误提示等
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册title>
<link rel="icon" href="../static/image/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" type="text/css" href="../static/css/main.css">
<link rel="stylesheet" type="text/css" href="../static/css/reset.css">
<script src="../static/js/jquery-1.12.4.min.js">script>
<script type="text/javascript" src="../static/js/vue-2.5.16.js">script>
<script type="text/javascript" src="../static/js/axios-0.18.0.min.js">script>
head>
<body class="body_1">
<div class="register_con">
<div class="l_con fl">
<a class="reg_logo"><img src="../static/image/test.png">a>
<div class="reg_slogan">注册即是 · 巅峰div>
<div class="reg_banner">div>
div>
<div class="r_con fr">
<div class="reg_title clearfix">
<h1>用户注册h1>
<a href="/login/">登录a>
div>
<div class="reg_form clearfix" id="app" v-cloak>
<form method="post" @submit="on_submit">
{# {% csrf_token %}#}
{{ csrf_input }}
<ul>
<li>
<label>用户名:label>
<input type="text" name="user_name" id="user_name" v-model="username" @blur="check_username">
<span class="error_tip" v-show="error_name">[[ error_name_message ]]span>
li>
<li>
<label>密码:label>
<input type="password" name="pwd" id="pwd" v-model="password" @blur="check_pwd">
<span class="error_tip" v-show="error_password">[[ error_password_message ]]span>
li>
<li>
<label>确认密码:label>
<input type="password" name="cpwd" id="cpwd" v-model="password2" @blur="check_cpwd">
<span class="error_tip" v-show="error_check_password">[[ error_password2_message ]]span>
li>
<li>
<label>手机号:label>
<input type="text" name="phone" id="phone" v-model="mobile" @blur="check_phone">
<span class="error_tip" v-show="error_phone">[[ error_phone_message ]]span>
li>
<li>
<label>图形验证:label>
<input type="text" name="image_code" id="pic_code" class="msg_input" v-model="image_code"
@blur="send_check_image_code">
<img :src="image_code_url" alt="图形验证码" class="pic_code" @click="generate_image_code">
<span class="error_tip3" v-show="error_image_code">[[ error_image_code_message ]]span>
li>
<li class="agreement">
<input type="checkbox" name="allow" id="allow" checked="checked" v-model="allow" @click="click_allow">
<label>同意”用户使用协议“label>
<span class="error_tip" v-show="error_allow">[[ error_allow_message ]]span>
{% if register_errmsg %}
<span class="error_tip3">{{ register_errmsg }}span>
{% endif %}
li>
<li class="reg_sub">
<input type="submit" value="注 册">
li>
ul>
form>
div>
div>
div>
<script type="text/javascript" src="../static/js/register.js">script>
body>
html>
var vm = new Vue({
el: '#app',
// 修改Vue变量的读取语法,避免和django模板语法冲突
delimiters: ['[[', ']]'],
data: {
host: "http://127.0.0.1:8000",
error_name: false,
error_password: false,
error_check_password: false,
error_phone: false,
error_image_code: false,
error_allow: false,
error_name_message: '请输入5-20个字符的用户',
error_password_message: '请输入8-20位的密码',
error_password2_message: '两次输入的密码不一致',
error_phone_message: '请输入正确的手机号码',
error_image_code_message: '请填写图形验证码',
error_allow_message: '请勾选用户协议',
uuid: '',
image_code_url: '',
sending_flag: false,
username: '',
password: '',
password2: '',
mobile: '',
image_code: '',
allow: true
},
mounted: function () {
// 向服务器获取图片验证码
this.generate_image_code();
},
methods: {
generateUUID: function () {
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
},
// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
generate_image_code: function () {
// 生成一个编号 : 严格一点的使用uuid保证编号唯一, 不是很严谨的情况下,也可以使用时间戳
this.uuid = this.generateUUID();
// 设置页面中图片验证码img标签的src属性
this.image_code_url = this.host + "/image_codes/" + this.uuid + "/";
console.log(this.image_code_url);
},
// 检查用户名
check_username: function () {
var re = /^[a-zA-Z0-9_-]{5,20}$/;
if (re.test(this.username)) {
this.error_name = false;
} else {
this.error_name_message = '请输入5-20个字符的用户名';
this.error_name = true;
}
// 检查重名
if (this.error_name == false) {
var url = this.host + '/usernames/' + this.username + '/count/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
if (response.data.count > 0) {
this.error_name_message = '用户名已存在';
this.error_name = true;
} else {
this.error_name = false;
}
})
.catch(error => {
console.log(error.response);
})
}
},
// 检查密码
check_pwd: function () {
var re = /^[0-9A-Za-z]{8,20}$/;
if (re.test(this.password)) {
this.error_password = false;
} else {
this.error_password = true;
}
},
// 确认密码
check_cpwd: function () {
if (this.password != this.password2) {
this.error_check_password = true;
} else {
this.error_check_password = false;
}
},
// 检查手机号
check_phone: function () {
var re = /^1[3-9]\d{9}$/;
if (re.test(this.mobile)) {
this.error_phone = false;
} else {
this.error_phone_message = '您输入的手机号格式不正确';
this.error_phone = true;
}
if (this.error_phone == false) {
var url = this.host + '/mobiles/' + this.mobile + '/count/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
if (response.data.count > 0) {
this.error_phone_message = '手机号已存在';
this.error_phone = true;
} else {
this.error_phone = false;
}
})
.catch(error => {
console.log(error.response);
})
}
},
// 检查图片验证码
check_image_code: function () {
if (!this.image_code) {
this.error_image_code_message = '请填写图片验证码';
this.error_image_code = true;
} else {
this.error_image_code = false;
}
},
// 检查是否勾选协议
check_allow: function () {
if (!this.allow) {
this.error_allow = true;
} else {
this.error_allow = false;
}
},
click_allow: function () {
this.allow = !this.allow
this.check_allow()
},
// // 发送手机短信验证码
send_check_image_code: function () {
this.check_image_code();
var url = this.host +'/sms_code/'+ this.mobile + '/?image_code=' + this.image_code + '&uuid=' + this.uuid;
axios.get(url, {
responseType: 'json'
})
.then(response => {
if (response.data.code == '0') {
this.error_image_code = false;
console.log("res_success");
}else {
if (response.data.code == '4001') {
this.error_image_code_message = response.data.errmsg;
this.error_image_code = true;
} else { // 4002
this.error_image_code = true;
this.error_image_code_message = response.data.errmsg;
}
}
})
.catch(error => {
console.log(error.response);
})
},
// 表单提交
on_submit() {
this.check_username();
this.check_pwd();
this.check_cpwd();
this.check_allow();
if (this.error_name == true || this.error_password == true || this.error_check_password == true
|| this.error_phone == true || this.error_allow == true || this.error_image_code == true) {
// 不满足注册条件:禁用表单
window.event.returnValue = false;
}
}
}
});
Vue绑定页面的套路
导入Vue.js库和ajax请求的库
准备js文件
html页面绑定变量、事件等
js文件定义变量、事件等
错误提示
如果错误提示信息是固定的,可以把错误提示信息写死,再通过v-show控制是否展示
如果错误提示信息不是固定的,可以使用绑定的变量动态的展示错误提示信息,再通过v-show控制是否展示
修改Vue变量的读取语法,避免和Django模板语法冲突
delimiters: [’[[’, ‘]]’]
后续的页面中如果有类似的交互和刷新效果,也可按照此套路实现
说明:
如果需求是注册成功后即表示用户登入成功,那么此时可以在注册成功后实现状态保持
如果需求是注册成功后不表示用户登入成功,那么此时不用在注册成功后实现状态保持
美多商城的需求是:注册成功后即表示用户登入成功
django.contrib.auth.init.py文件中。
login(request, user, backend=None)
状态保持 session 数据存储的位置:Redis数据库的1号库
SESSION_ENGINE = “django.contrib.sessions.backends.cache”
SESSION_CACHE_ALIAS = “session”
2. login()方法登入用户
#保存注册数据
try:
user = User.objects.create_user(username=username, password=password, mobile=mobile)
except DatabaseError:
return render(request, ‘register.html’, {‘register_errmsg’: ‘注册失败’})
#登入用户,实现状态保持
login(request, user)
响应注册结果
return redirect(reverse(‘contents:index’))
3. 查看状态保持结果
1.请求方式
选项 方案
请求方法 GET
请求地址 /usernames/(?P[a-zA-Z0-9_-]{5,20})/count/
2.请求参数:路径参数
参数名 类型 是否必传 说明
username string 是 用户名
3.响应结果:JSON
响应结果 响应内容
code 状态码
errmsg 错误信息
count 记录该用户名的个数
3. 用户名重复注册后端逻辑
class UsernameCountView(View):
“”“判断用户名是否重复注册”""
def get(self, request, username):
"""
:param request: 请求对象
:param username: 用户名
:return: JSON
"""
count = User.objects.filter(username=username).count()
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'count': count})
1.请求方式
选项 方案
请求方法 GET
请求地址 /mobiles/(?P1[3-9]\d{9})/count/
2.请求参数:路径参数
参数名 类型 是否必传 说明
mobile string 是 手机号
3.响应结果:JSON
响应结果 响应内容
code 状态码
errmsg 错误信息
count 记录该用户名的个数
3. 手机号重复注册后端逻辑
class MobileCountView(View):
“”“判断手机号是否重复注册”""
def get(self, request, mobile):
"""
:param request: 请求对象
:param mobile: 手机号
:return: JSON
"""
count = User.objects.filter(mobile=mobile).count()
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'count': count})
1.请求方式
选项 方案
请求方法 GET
请求地址 image_codes/(?P[\w-]+)/
2.请求参数:路径参数
参数名 类型 是否必传 说明
uuid string 是 唯一编号
3.响应结果:image/jpg
class ImageCodeView(View):
“”“图形验证码”""
def get(self, request, uuid):
"""
:param request: 请求对象
:param uuid: 唯一标识图形验证码所属于的用户
:return: image/jpg
"""
pass
2.总路由
#verifications
url(r’^’, include(‘verifications.urls’)),
3.子路由
#图形验证码
url(r’^image_codes/(?P[\w-]+)/$’, views.ImageCodeView.as_view()),
可能出现的错误
报错原因:环境中没有Python处理图片的库:PIL
解决办法
安装Python处理图片的库:pip install Pillow
2. 准备Redis数据库
准备Redis的2号库存储验证码数据
“verify_code”: { # 验证码
“BACKEND”: “django_redis.cache.RedisCache”,
“LOCATION”: “redis://127.0.0.1:6379/2”,
“OPTIONS”: {
“CLIENT_CLASS”: “django_redis.client.DefaultClient”,
}
},
3. 图形验证码后端逻辑实现
class ImageCodeView(View):
“”“图形验证码”""
def get(self, request, uuid):
"""
:param request: 请求对象
:param uuid: 唯一标识图形验证码所属于的用户
:return: image/jpg
"""
# 生成图片验证码
name, text, image = captcha.generate_captcha()
# 保存图片验证码
redis_conn = get_redis_connection('verify_code')
redis_conn.setex('img_%s' % uuid, constants.IMAGE_CODE_REDIS_EXPIRES, text)
# 响应图片验证码
return http.HttpResponse(image, content_type='image/jpg')
mounted(){
// 生成图形验证码
this.generate_image_code();
},
methods: {
// 生成图形验证码
generate_image_code(){
// 生成UUID。generateUUID() : 封装在common.js文件中,需要提前引入
this.uuid = generateUUID();
// 拼接图形验证码请求地址
this.image_code_url = “/image_codes/” + this.uuid + “/”;
},
…
}
2.register.html
java
<li>
<label>图形验证码:</label>
<input type="text" name="image_code" id="pic_code" class="msg_input">
<img :src="image_code_url" @click="generate_image_code" alt="图形验证码" class="pic_code">
<span class="error_tip">请填写图形验证码</span>
</li>
<li>
<label>图形验证码:</label>
<input type="text" v-model="image_code" @blur="check_image_code" name="image_code" id="pic_code" class="msg_input">
<img :src="image_code_url" @click="generate_image_code" alt="图形验证码" class="pic_code">
<span class="error_tip" v-show="error_image_code">[[ error_image_code_message ]]</span>
</li>
2.register.js
check_image_code(){
if(!this.image_code) {
this.error_image_code_message = ‘请填写图片验证码’;
this.error_image_code = true;
} else {
this.error_image_code = false;
}
},
3.图形验证码校验效果
1.请求方式
选项 方案
请求方法 POST
请求地址 /login/
2.请求参数:表单
参数名 类型 是否必传 说明
username string 是 用户名
password string 是 密码
remembered string 是 是否记住用户
3.响应结果:HTML
字段 说明
登录失败 响应错误提示
登录成功 重定向到首页
3. 用户名登录接口定义
class LoginView(View):
“”“用户名登录”""
def get(self, request):
"""
提供登录界面
:param request: 请求对象
:return: 登录界面
"""
pass
def post(self, request):
"""
实现登录逻辑
:param request: 请求对象
:return: 登录结果
"""
pass
用户名登录后端逻辑
class LoginView(View):
“”“用户名登录”""
def get(self, request):
“”"
提供登录界面
:param request: 请求对象
:return: 登录界面
“”"
return render(request, ‘login.html’)
def post(self, request):
“”"
实现登录逻辑
“”"
# 接受参数
username = request.POST.get(‘username’)
password = request.POST.get(‘password’)
remembered = request.POST.get(‘r’)
# 认证登录用户
user = authenticate(username=username, password=password)
if user is None:
return render(request, 'login.html', {'account_errmsg': '用户名或密码错误'})
# 实现状态保持
login(request, user)
# 设置状态保持的周期
if remembered != 'on':
# 没有记住用户:浏览器会话结束就过期, 默认是两周
request.session.set_expiry(0)
# 响应登录结果
return redirect(reverse('contents:index'))
登录的核心思想:认证和状态保持
通过用户的认证,确定该登录用户是美多商场的注册用户。
通过状态保持缓存用户的唯一标识信息,用于后续是否登录的判断。
回顾登录:将通过认证的用户的唯一标识信息,写入到当前session会话中
退出登录:正好和登录相反(清理session会话信息)
logout()方法:
Django用户认证系统提供了logout()方法
封装了清理session的操作,帮助我们快速实现登出一个用户
logout()位置:
django.contrib.auth.init.py文件中
logout(request)
2. logout()方法使用
class LogoutView(View):
“”“退出登录”""
def get(self, request):
"""实现退出登录逻辑"""
# 清理session
logout(request)
# 退出登录,重定向到登录页
response = redirect(reverse('users:login'))
# 退出登录时清除cookie中的username
response.delete_cookie('username')
return response
退出登录的核心思想就是清理登录时缓存的状态保持信息。
由于首页中用户名是从cookie中读取的。所以退出登录时,需要将cookie中用户名清除。
Vue渲染首页用户名
1.index.html
<div v-if="username" class="login_btn fl">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="#">退出</a>
</div>
<div v-else class="login_btn fl">
<a href="login.html">登录</a>
<span>|</span>
<a href="register.html">注册</a>
</div>
2.index.js
mounted(){
// 获取cookie中的用户名
this.username = getCookie(‘username’);
},