Python: 前后端分离博客项目
- TAGS: Python
博客系统
数据库模型设计
分析
多人使用博客系统。采用BS架构实现。市面上多数某某系统归根结底都是这种设计。
博客系统,核心模块有:
- 用户管理
- 注册、登录
- 删除查用户
- 博文管理
- 增删改查博文
需要数据库,本次使用Mysql8+, InnoDB引擎。
需要支持多用户登录,各自可以管理自己的博文(增删改查),管理是不公开的,但是博文是不需要登录就可以公开预览的。
先不要思考过多的功能,先完成最小的核心需求代码。
数据库设计
创建数据库
CREATE DATABASE IF NOT EXISTS `blog2` DEFAULT CHARACTER SET utf8mb4;
需要用户表、文章表
用户表user
字段 | 说明 |
---|---|
id | 主键,唯一标识 |
username | 登录名,唯一 |
name | 用户姓名,描述性字段 |
电子邮箱,注册用信息,应该唯一。可用作登录名、可用于密码找回 | |
password | 密码存储。注意,不能明文存储密码。一般采用单向加密算法,如MD5 |
CREATE TABLE `user` ( `id` INT ( 11 ) NOT NULL AUTO_INCREMENT, `name` VARCHAR ( 48 ) NOT NULL, `email` VARCHAR ( 64 ) NOT NULL, `password` VARCHAR ( 128 ) NOT NULL, PRIMARY KEY ( `id` ), UNIQUE KEY `email` ( `email` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8;
密码算法
密码一般采用单向散列算法,算法保证不可逆。
- MD5(Message Digest Algorithm 5): 输出128位数值。被广泛采用,但已经极度不安全,不建议采用
- SHA(Secure Hash Algorithm): 较新算法
- 由于SHA-0, SHA-1相继被王小云教授破解
- SHA-2包含SHA-224, SHA-256, SHA-384, SHA-512
- SHA3于2012年由非NSA设计,内部算法设计不同SHA-1和SHA-2
from hashlib import md5, sha1, sha256, sha512, sha3_256, sha3_512 has = (md5, sha1, sha256, sha512, sha3_256, sha3_512) print(md5(b'abc').hexdigest()) # 输出 'abc' 的 MD5 哈希(十六进制) print(md5(b'abd').hexdigest()) s = b'abc' for h in has: o = h(s) y = o.digest() x = o.hexdigest() print(s,h) print(x, len(x), y, len(y)) print('-'*30) #digest():返回原始字节(bytes 对象),长度由算法决定(如 MD5=16 字节,SHA256=32 字节)。 #hexdigest():返回十六进制字符串,长度是原始字节长度的 2 倍。
系统表auth_user
在Django中也可以使用内建的用户系统,它提供了用户、用户组、权限表。
CREATE TABLE `auth_user` ( `id` INT (11) NOT NULL AUTO_INCREMENT, `PASSWORD` VARCHAR (128) NOT NULL, `last_login` DATETIME (6) DEFAULT NULL, `is_superuser` TINYINT (1) NOT NULL, `username` VARCHAR (150) NOT NULL, `first_name` VARCHAR (30) NOT NULL, `last_name` VARCHAR (150) NOT NULL, `email` VARCHAR (254) NOT NULL, `is_staff` TINYINT (1) NOT NULL, `is_active` TINYINT (1) NOT NULL, `date_joined` DATETIME (6) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE = InnoDB AUTO_INCREMENT = 2_DEFAULTCHARSET = utf8mb4;
这个表唯一的问题就是email并没有设定唯一键,可以迁移后,修改表手动增加。
一般是自己创建user表,不推荐使用内建的表。
文章表post
字段 | 说明 |
---|---|
id | 主键,唯一标识 |
title | 标题,描述性字段 |
author | 博文作者要求必须是注册用户,这个字段应该存储userid |
postdate | 发布日期,日期类型 |
content | 文章内容,博文内容可能很长,一般来说不会小于256个字符的 |
一对多关系:一篇博文属于一个作者,一个作者有多篇博文。
- 博客内容选取什么字段类型?
- 多大合适?
- 博文图片如何处理?
- 适合和其它字段放在同一张表吗?
Content字段的设计
- 字段类型 :博文一般很长,不可能只有几百个字符,需要大文本字段。MySQL中,选择TEXT类型,而不是char或者varchar类型。
- 大小 :text类型是65535个字符,如果不够用,选择longtext,有\(2^{32}-1\)个字符长度。足够使用了。
- 图片存储 :博文就像HTML一样,图片是通过*路径信息*将图片是嵌入在内容中的,所以保存的内容还是字符串。图片来源有2中:
- 外联:通过URL链接访问,本站不用存储该图片,但容易引起盗链问题。
- 本站存储:需要提供博文的在线文本编辑器,提供图片上传到网站存储,并生成图片URL,这个URL嵌入博客正文中,不会有盗链问题,但要解决众多图片存储问题、水印问题、在线压缩问题、临时或垃圾图片清理等等难题。
- 本博客项目不实现图片功能
- 字段考虑
- content字段存储文本类型大字段,一般不和数据频繁查询的字段放在一张表中,需要拆到另一张表中。
CREATE TABLE `post` ( `id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT, `title` VARCHAR ( 256 ) NOT NULL, `author_id` INT ( 11 ) NOT NULL, `postdate` datetime NOT NULL, PRIMARY KEY ( `id` ), KEY `author_id` ( `author_id` ), CONSTRAINT `fk_post_user` FOREIGN KEY ( `author_id` ) REFERENCES `user` ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8;
CREATE TABLE `content` ( `id` BIGINT ( 20 ) UNSIGNED NOT NULL, `content` text NOT NULL, PRIMARY KEY ( `id` ) USING BTREE, CONSTRAINT `fk_content_post` FOREIGN KEY ( `id` ) REFERENCES `post` ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8;
注:这里的SQL脚本本次不要用来生成表,使用ORM库写代码来创建表,用来检查实体类构建是否正确。
用户完成的功能有登录、注册、登出等,auth_user表基本满足。
博客功能有用户发文、文章列表、文章详情等,post、content表基本满足。
项目
项目构建
- 在Pycharm中创建一个项目,使用虚拟环境,Python使用版本是3.13.3
- 使用虚拟环境,Python挑选喜欢的版本(本次选用3.13.3)
- 注:Pycharm中可通过setting重新设置虚拟环境。
- 本次项目使用Django开发后台。
项目构建和基础知识
概述
- Django采用MVC架构设计的开源的WEB快速开发框架。
- 优点:
- 能够快速开发,自带ORM、Template、Form、Auth核心组件
- MVC设计模式
- 实用的管理后台Admin
- 简洁的url设计
- 周边插件丰富
- 缺点:架构重、同步阻塞
- 所有Django的设计目标就是一款大而全,便于企业快速开发项目的框架,因此企业应用较广。
安装Django
- Python实用3.13.3
- Python虚拟环境:https://docs.python.org/zh-cn/3.13/library/venv.html
- Django的下载地址https://www.djangoproject.com/download/
- Django文档:https://docs.djangoproject.com/
Python版本依赖,参看
Django version | Python versions |
---|---|
4.2 | 3.8, 3.9, 3.10, 3.11, 3.12 (added in 4.2.8) |
5.0 | 3.10, 3.11, 3.12 |
5.1 | 3.10, 3.11, 3.12, 3.13 (added in 5.1.3) |
5.2 | 3.10, 3.11, 3.12, 3.13 |
#安装python虚拟环境, 并启动 python -m venv .venv .\.venv\Scripts\activate #在虚拟环境中安装 python -m pip install Django==5.2.1
django-admin --version #查看当前django版本 django-admin --help #查看使用帮助 django-admin startproject --help #查看startproject命令帮助
注意:本文若未特殊声明,所有的命令操作都在项目的根目录下
创建Django项目
当前目录下,创建名为blog的django项目
django-admin startproject blog .
上句命令就在当前项目根目录中构建了Django项目的初始文件。 .
点代表项目根目录。
F:\CLASSES\TPROJECTS\BLOG10 ├─ manage.py └─ blog ├─ settings.py ├─ urls.py #路径映射 ├─ wsgi.py ├─ asgi.py └─ __init__.py
重要文件说明
- manage.py:本项目管理的命令行工具。应用创建、数据库迁移等都使用它完成
- blog/settings.py:本项目的核心配置文件。
- 应用、数据库配置
- 模板、静态文件
- 中间件、日志
- 第三方插件配置
- blog/urls.py:URL路径映射配置。项目初始,只配置了/admin的路由。
- blog/wsgi:定义WSGI接口信息。部署用,一般无需改动。
return WSGIHandler()
__call__(self, environ, start_response)
主路由文件url.py
from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理 ]
数据库配置
使用数据库,需要修改默认的数据库配置。
在主项目的settings.py下DATABASES。默认使用的sqlite,修改为mysql。
#https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'gdy', 'USER': 'gdy', 'PASSWORD': 'gdy', 'HOST': '127.0.0.1', 'PORT': '3306', } }
配置项 | 说明 |
---|---|
HOST | 数据库主机。缺省是空字符串,代表localhost。如果是=/=开头表示使用UnixSocket链接 |
POST | 端口 |
USER | 用户名 |
PASSWORD | 密码 |
NAME | 库名 |
OPTIONS | 选项、字典类型,参考MySQL文档 |
- 数据库引擎ENGINE
- 内建引擎有
- django.db.backends.postgresql
- django.db.backends.mysql
- django.db.backends.sqlite3
- django.db.backends.oracle
MySQL数据库驱动
https://docs.djangoproject.com/en/5.2/ref/databases/#mysql-db-api-drivers
Django支持MySQL8+
Django官方推荐使用本地驱动mysqlclient 1.4.3+
#安装mysql驱动 pip install mysqlclient
Linux、Mac请参照官网安装依赖库
创建应用
创建用户应用
python manage.py startapp user
创建应用后,项目根目录下产生一个user目录,有如下文件:
- admin.py: 应用后台管理声明文件
- models.py: 模型层Model类定义
- views.py: 定义URL响应函数或类
- migrations包: 数据迁移文件生成目录
- apps.py: 应用的信息定义文件
user应用创建后应该完成以下功能:
- 用户注册
- 用户登录
注册应用
在settings.py中,增加user应用。目的是为了 后台管理 admin使用,或 迁移 migrate使用
#settings.py文件中 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'user' ]
本次user应用使用内建的表auth_user,所以不用注册user表。删除post表外键和user表。
ALTER TABLE `blog`.`post` DROP FOREIGN KEY `post_ibfk_1` DROP TABLE IF EXISTS `blog`.`user`;
迁移
Django内部也有应用,它们需要表。这些表的迁移文件已经生成了,只需要迁移。
#python manage.py makemigrations #不用做。user 下面还没有modes类 python manage.py migrate
迁移后,产生下面这些表:
mysql> show tables ; +----------------------------+ | Tables_in_blog | +----------------------------+ | auth_group | | auth_group_permissions | | auth_permission | | auth_user | | auth_user_groups | | auth_user_user_permissions | | content | | django_admin_log | | django_content_type | | django_migrations | | django_session | | post | +----------------------------+
创建post表指向auth_user表的外键
ALTER TABLE `blog`.`post` ADD CONSTRAINT `post_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `blog`.`auth_user` (`id`);
auth_user表结构
CREATE TABLE `blog`.`Untitled` ( `id` int NOT NULL AUTO_INCREMENT, `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `last_login` datetime(6) NULL DEFAULT NULL, `is_superuser` tinyint(1) NOT NULL, `username` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `first_name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `last_name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `email` varchar(254) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `is_staff` tinyint(1) NOT NULL, `is_active` tinyint(1) NOT NULL, `date_joined` datetime(6) NOT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `username`(`username` ASC) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
如果使用自建use表
模型Model
- 字段类型
字段类 | 说明 |
---|---|
AutoField | 自增的整数字段。如果不指定,django会为模型类自动增加主键字段 |
BooleanField | 布尔值字段,True和False。对应表单控件CheckboxInput |
NullBooleanField | 比BooleanField多一个null值 |
CharField | 字符串,max_length设定字符长度。对应表单控件TextInput |
TextField | 大文本字段,一般超过4000个字符使用。对应表单控件Textarea |
IntegerField | 整数字段, 一般4字节 |
BigIntegerField | 更大整数字段,8字节 |
DecimalField | 使用Python的Decimal实例表示十进制浮点数。max_digits总位数,decimal_places小数点后的位数 |
FloatField | Python的Float实例表示的浮点数 |
DateField | 使用Python的datetime.date实例表示的日期 |
auto_now=False每次修改对象自动设置为当前时间。 | |
auto_now_add=False对象第一次创建时自动设置为当前时间。 | |
auto_now_add、auto_now、default互斥。 | |
对应控件为TextInput,关联了一个Js编写的日历控件 | |
TimeField | 使用Python的datetime.time实例表示的时间,参数同上 |
DateTimeField | 使用Python的datetime.datetime实例表示的时间,参数同上 |
FileField | 一个上传文件的字段 |
ImageField | 继承了FileField的所有属性和方法,但是对上传的文件进行校验,确保是一个有效的图片 |
EmailField | 能做Email检验,基于CharField,默认max_length=254 |
GenericIPAddressField | 支持IPv4、IPv6检验,缺省对应文本框输入 |
URLField | 能做URL检验,基于基于CharField,默认max_length=200 |
- 缺省主键
- 缺省情况下,Django的每一个Model都有一个名为id的AutoField字段,如下
id = models.AutoField(primary_key=True)
- 如果显示定义了主键,这种缺省主键就不会被创建了。Python之禅中说“显示优于隐试”,所以,尽量使用自己定义的主键,哪怕该字段名就是id,也是一种不错的选择。
- 缺省情况下,Django的每一个Model都有一个名为id的AutoField字段,如下
- 字段选项
值 | 说明 |
---|---|
db_column | 表中字段的名称。如果未指定,则使用属性名 |
primary_key | 是否主键 |
unique | 是否是唯一键 |
default | 缺省值。这个缺省值不是数据库字段的缺省值,而是新对象产生的时候被填入的缺省值 |
null | 表的字段是否可为null,默认为False |
blank | Django表单验证中,如果True,则允许该字段为空。默认为False |
db_index | 字段是否有索引 |
to_field | 关联对象的字段。默认情况下,Django 使用相关对象的主键。如果你引用了一个不同的字段,这个字段必须有 unique=True |
关系类型字段类
类 | 说明 |
---|---|
ForeignKey | 外键,表示一对多 |
ForeignKey('production.Manufacturer') | |
自关联ForeignKey('self') | |
ManyToManyField | 表示多对多 |
OneToOneField | 表示一对一 |
一对多时,自动创建会增加_id后缀。
- 从一访问多,使用
对象.小写模型类_set
- 从一访问一,使用
对象.小写模型类
访问id 对象.属性_id
创建User的Model类
- 基类models.Model
- 表名不指定默认使用
<appname>_<model_name>
。使用Meta类修改表名
#/user/models.py文件 from django.db import models # user表模型 class User(models.Model): class Meta: db_table = "user" id = models.AutoField(primary_key=True) name = models.CharField(max_length=48,null=False) email = models.CharField(max_length=64,unique=True,null=False) password = models.CharField(max_length=128,null=False) def __repr__(self): return "<user {} {}>".format(self.id,self.name) __str__ = __repr__
Meta类的使用,参考https://docs.djangoproject.com/en/1.11/ref/models/options/#django.db.models.Options.db_table
- 迁移Migration
- 迁移:从模型定义生成数据库的表
生成迁移文件。执行
python manage.py makemigrations
(venv) D:\MyPythonUse\DjangoWeb>python manage.py makemigrations Migrations for 'user': user\migrations\0001_initial.py - Create model User 生成如下文件 user ├─ migrations ├─ 0001_initial.py └─ __init__.py
- 修改MOdel类,还需要调用
python manage.py makemigrations
,然后migrate,迁移文件的序号会增加。 - 注意:
- 迁移的应用必须在settings.py的INSTALLED_APPS中注册。
- 不要谁便删除这些迁移文件,因为后面的改动都是要依据这些迁移文件的。
- 生成的迁移文件0001_initial.py内容如下:
from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='User', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), ('name', models.CharField(max_length=48)), ('email', models.CharField(max_length=64, unique=True)), ('password', models.CharField(max_length=128)), ], options={ 'db_table': 'user', }, ), ]
- 修改MOdel类,还需要调用
- 执行迁移生成数据库的表
python manage.py migrate
- 执行了迁移,还同时生成了admin管理用的表。
- 查看数据库,user表创建好了,字段设置完全正确。
Django后台管理
1、 创建管理员
- 命令=python manage.py createsuperuser=
- 管理会用户名admin
密码adminadmin
python manage.py createsuperuser Username: admin Email address: [email protected] Password: Password (again): The password is too similar to the email address.
会在auth_user表中创建admin用户
mysql> select username,password from auth_user; | admin | pbkdf2_sha256$1000000$Jxxxxx$xxxxxxxxxxxxxxxxxx= |
2、 本地化
settings.py中可以设置语言、时区。语言名称可以查看=django\contrib\admin\locale=目录
# 修改djweb/settings.py文件中对应变量 LANGUAGE_CODE = 'en-us' #'zh-Hans' TIME_ZONE = 'Asia/Shanghai' # 'UTC' USE_TZ = True
3、 启动WEB Server
python manage.py runserver #默认 127.0.0.1:8000 #python manage.py runserver 0.0.0.0:8000
- 访问http://127.0.0.1:8000/ 看到服务启动正常
- 访问http://127.0.0.1:8000/admin/ 可以登录后台管理界面
- 注意:用户名密码为之前第一步创建管理员时设定的
- 用户名:admin 密码:adminadmin
- 可以在页面创建普通用户
4、 注册应用模块
- 注册后user可以被管理员用户在web界面进行增删改操作
登录后如果希望后来能够管理这些表,就需要在admin文件中注册对应的models
举例
#models.py from django.db import models # Create your models here. class Post(models.Model): class meta: db_table="a" pass
在admin.py文件中注册
from django.contrib import admin # Register your models here. from .models import Post admin.site.register(Post)
登录成功后,可以看到注册的表了。
路由(重要)
路由功能就是实现URL模式匹配和处理函数之间的映射。
路由配置要在项目的urls.py中配置,也可以多级配置,在每一个应用中,建立一个urls.py文件配置路由映射。
官方文档:
- URL调度器: https://docs.djangoproject.com/zh-hans/5.2/topics/http/urls/
- path函数: https://docs.djangoproject.com/en/5.2/ref/urls/#django.urls.path
- url函数(Django 2.x之前)
- url(regex,view,kwargs=None,name=None) 进行模式匹配
- regex: 正则表达式,与之匹配的URL会执行对应的第二个参数view
- view: 用于执行与正则表达式匹配的URL请求
- kwargs: 视图使用的字典类型的参数
- name: 用来反向获取URL
- url(regex,view,kwargs=None,name=None) 进行模式匹配
- path函数(2.x+)
- path(route, view, kwargs=None, name=None)
- 对URL进行模式匹配
- 可以使用尖括号来捕获部分URL成分。一旦使用了尖括号提取成分,每一个成分都会变成一个 实参
- 注入 到视图函数中,视图函数必须增加形参接收
- 形参函数基本同url函数
- path(route, view, kwargs=None, name=None)
还有一个re_path函数(2.x+)可以使用正则表达式,本质上就是url函数。
urls.py 内容如下
from django.contrib import admin from django.urls import path from django.urls import re_path from django.http import HttpResponse,HttpRequest def index(request:HttpRequest): """视图函数:请求进来返回响应""" res = HttpResponse(b"Hello word xdd") print(res.charset) #utf-8 return res urlpatterns = [ path('admin/', admin.site.urls), re_path(r'^$',index), re_path(r'^index$',index) #不同路径可以指向同一个函数执行 ]
- 启动web服务=python manage.py runserver=
- url的定义与访问
url(r'^index/$',index)
- =http://127.0.0.1:8000/index/=%E5%8F%AF%E4%BB%A5%E8%AE%BF%E9%97%AE
http://127.0.0.1:8000/index=可以访问,但会自动补一个=/
url(r'^index$',index)
- 请求信息测试和JSON响应,修改上面index函数
from django.http import HttpResponse,HttpRequest,JsonResponse def index(request:HttpRequest): """视图函数:请求进来返回响应""" d = {} d["method"] = request.method d["path"] = request.path d["path_info"] = request.path_info d["GETparams"] = request.GET return JsonResponse(d)
- 在项目中首页数使用HTML显示,为了加载速度快,一般多使用静态页面。如果首页内容多,还有部分数据需要变化,将变化部分使用AJAX技术从后台获取数据。
- 本次,为了学习模板技术,只将首页采用Django的模板技术实现。
Django模板技术
[toc]
如果使用react实现前端页面,其实Django就没有必须使用模板,它其实就是一个纯后台服务程序,接收请求,响应数据,前端接口设计就可以是纯粹的Restful风格。
模板的目的就是为了可视化,将数据按照一定布局格式输出,而不是为了数据处理,所以一般不会有复杂的处理逻辑。模板的引入实现了业务逻辑和显示格式的分离。这样,在开发中,就可以分工协作,页面开发完成页面布局设计,后台开发完成数据处理逻辑实现。
- Python的模板引擎默认使用Django template language(DTL)构建
模板配置
- 在=djweb/settings.py=中,设置模板项目的路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #这句话取项目根目录 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.abspath(os.path.join(BASE_DIR,'templates'))], #制定模板文件夹路径 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
- DIRS:列表,定义模板文件的搜索路径顺序。os.path.join(BASE_DIR,'templates')即项目根目录下templates目录,请构建这个目录。
- 项目根目录中构建templates文件夹。模板文件目录
- APP_DIRS:是否运行在每个已经安装的应用中查找模板。应用自己目录下游templates目录,例如:=django/contrilb/admin/templates=。如果应用需要可分离、可重用,建议吧模板放到应用目录下
- *BASE_DIR*是项目根目录,=os.path.join(BASE_DIR,'templates')=就是在manage.py这一层建立一个目录templates。这个路径就是以后默认找模板的地方。
模板渲染
模板页
- 新建html文件,在=/templates/index.html=目录下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Django web 模板技术</title> </head> <body> 我是模板,数据是{{content}} </body> </html>
模板处理
- *加载模板*:模板是一个文件,需要从磁盘读取并加载。要将模板放置在指定的模板文件夹中
- *渲染*:模板需要使用内容数据渲染
- 测试:修改=djweb/urls.py=文件中的index函数。
from django.template import loader def index(request:HttpRequest): """视图函数:请求进来返回响应""" template = loader.get_template("index.html") #加载器模块搜索模板并加载它 print(template.origin) #显示模板路径 context = {"content":"www.xdd.com"} #字典数据 return HttpResponse(template.render(context,request))
- 运行server
python manage.py runserver
- 访问=http://127.0.0.1:8000/=%E7%95%8C%E9%9D%A2%E5%A6%82%E4%B8%8B%EF%BC%9A%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0
render快捷渲染函数
- 上面2个步骤代码编写繁琐,Django提供了对其的封装-------–—快捷函数render。
render(request,template_name,context=None)
返回HTTPResponse对象- template_name #模板名称
- context 数据字典
- render_to_string() 是其核心方法,其实就是拿数据替换HTML中的指定位置后返回一个字符串
from django.shortcuts import render def index(request:HttpRequest): """视图函数:请求进来返回响应""" return render(request,"index.html",{"content":"www.xdd.com"})
DTL语法(模板语法)
变量
- 语法:={{ variable }}=
- 变量名由字母、数字、下划线、点号组成。
- 点号使用的时候,例如foo.bar,遵循以下顺序:
- 字典查找,例如=foo["bar"]=,把foo当做字典,bar当做key
- 属性或方法的查找,例如=foo.bar=,把foo当做对象,bar当做属性或方法
- 数字索引查找,例如=foo[1]=,把foo当做列表一样,使用索引访问
示例: 修改=djweb/urls.py=文件中的index函数
from django.shortcuts import render import datetime def index(request:HttpRequest): """视图函数:请求进来返回响应""" my_dict = { "a":100, "b":0, "c":list(range(10,20)), "d":'abc', "date":datetime.datetime.now() } context = {"content":"www.xdd.com","my_dict":my_dict} return render(request,"index.html",context)
- 如果*变量未能找到,则缺省插入空字符串*。
- 在*模板中调用方法,不能加小括号*,自然也不能传递参数。
- ={{my_dict.a}}=复合第一条,当做字典的key就可以访问了
- ={{my_dict.keys}}=正确写法。符合第二条,当做my_dict对象的属性和方法。
- 错误写法={{my_dict.keys()}}=。
模板标签
- =if/else=标签
基本语法如下:
{% if condition %} ... display {% endif %}
{% if condition %} ... display 1 {% elif condition2 %} ... display 2 {% else %} ... display 3 {% endif %}
- 条件也支持=and、or、not=
- 注意,因为这些标签是断开的,所以不能像Python一样使用缩进就可以表示出来,必须有个结束标签,例如endif、endfor。
=for=标签
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Django web 模板技术</title> </head> <body> 我是模板,数据是{{content}} <ul> {% for athlete in athlete_list %} <li>{{ athlete.name }}</li> {% endfor %} </ul> <ul> {% for person in person_list %} <li> {{ person.name }}</li> {% endfor %} </ul> </body> </html>
变量 说明 forloop.counter
当前循环从1开始的计数 forloop.counter0
当前循环从0开始的计数 forloop.revcounter
从循环的末尾开始倒计数1 forloop.revcounter0
从循环的末尾开始倒计数到0 forloop.first
第一次进入循环 forloop.last
最后一次进入循环 forloop.parentloop
循环嵌套时,内层当前循环的外层循环 - 给标签增加一个reversed使得该列表被反向迭代:
{% for athlete in athlete_list reversed %} ... {% empty %} ... 如果被迭代的列表是空的或者不存在,执行empty {% endfor %}
- 可以嵌套使用{% for %}标签:
{% for athlete in athlete_list %} <h1>{{ athlete.name }}</h1> <ul> <% for sport in athlete.sports_played %> <li>{{ sport }}</li> <% endfor %> </ul> {% endfor %}
=ifequel/ifnotequal=标签
- ={% ifequal %}=标签比较两个值,当他们相等时,显示在={% ifequal %}=和={% endifequal %}=之中所有的值。下面的例子比较两个模板变量user和currentuser:
{% ifequal user currentuser %} <h1>Welcome!</h1> {% endifequal %}
- 和={% if %}=类似,={% ifequal %}=支持可选的={% else %}=标签:
{% ifequal section 'sitenews' %} <h1>Site News</h1> {% else %} <h1>No News Here</h1> {% endifequal %}
- 其他标签
- *csrf_token*用于跨站请求伪造保护,防止跨站攻击的。={% csrf_token %}=
注释标签
- 单行注释={# #}=
- 多行注释={% comment %}… {% endcomment %}=
{# 这是一个注释 #} {% comment %} 这是多行注释 {% endcomment %}
过滤器
- 模板过滤器可以在遍历被显示前修改它。
- 语法
{{ 变量|过滤器}}
- 过滤器使用管道字符=|=,例如={{ name|lower}}=,={{ name }}=变量被过滤器lower处理后,文档大写转换文本为小写。
- 过滤管道可以被*套接*,一个过滤器管道的输出又可以作为下一个管道的输入。
- 例如={{ my_list|first|upper }}=,将列表第一个元素并将其转化为大写。
- 过滤器传参
- 有些过滤器可以传递参数,过滤器的参数跟随冒号之后并且总是以双引号包含。
- 例如:
{{bio|truncatewords:"30"}}
,截取显示变量bio的前30个词。{{ my_list|join:"," }}
,将my_list的所有元素使用=,=逗号连接起来
- 其他过滤器
过滤器 说明 举例 cut
切掉字符串中的指定字符 ={{ value cut:"b"}}= lower
转换为小写 upper
转换为大写 truncatewords
指定的长度截取字符串 ={{ bio truncatewords:"30"}}= join
对序列拼接 ={{ d.e join:"//"}}= =first=取序列第一个元素 last
取序列最后元素 yesno
变量可以是True、False、Noneyesno的参数给定逗号分隔的三个值,返回3个值中的一个。True对应第一个False对应第二个None对应第三个如果参数只有2个,None等效False处理 ={{value yesno:"yeah,no,maybe"}}= add
加法,参数是负数就是减法 数字加={{value add:"100"}}=列表合并={{mylist add:newlist}}= divisibleby
能否被整除 ={{value divisibleby:"3" }}=能被3整除返回True addslashes
在反斜杠、单引号或者双引号前面加上反斜杠 ={{value addslashes }}= length
返回变量的长度 ={% if my_list length >1 %}= default
变量等价False则使用缺省值 ={{value default:"nothing"}}= default_if_none
变量为None使用缺省值 ={{value default_if_none:"nothing"}}= date
格式化date或者datetime对象 实例:={{my_dict.date date:'Y n j'}}=Y 2000年n 1-12月j 1-31日
模板实例
奇偶行列表输出
- 使用下面字典my_dict的c的列表,在模板网页中列表ul输出多行数据
- 要求奇偶行颜色不同
- 每行有行号(从1开始)
- 列表中所有数据都增大100
from django.http import HttpResponse,HttpRequest,JsonResponse from django.template import loader from django.shortcuts import render import datetime def index(request:HttpRequest): """视图函数:请求进来返回响应""" my_dict = { "a":100, "b":0, "c":list(range(10,20)), "d":'abc', "date":datetime.datetime.now() } context = {"content":"www.xdd.com","my_dict":my_dict} return render(request,"index.html",context)
- 模板页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Django web 模板技术</title> </head> <body> 我是模板,数据是{{content}} <ul> {% for athlete in my_dict.items %} {% ifequal athlete.0 "c" %} {% for dt in athlete.1 %} <li>{{ forloop.parentloop.counter }} | {{ forloop.counter }} | {{ athlete.0 }} -- {{ dt|add:"100" }}</li> {% endfor %} {% else %} {% ifequal athlete.0 "date" %} <li>{{ forloop.counter }} | {{ athlete.0 }} -- {{ athlete.1|date:'Y-n-j' }}</li> {% else %} <li>{{ forloop.counter }} | {{ athlete.0 }} </li> {% endifequal %} {% endifequal %} {% endfor %} </ul> </body> </html>
- 使用下面字典my_dict的c的列表,在模板网页中列表ul输出多行数据
附加–Pycharm模板自定义
- 第一步:
- 第二步
- 第三步
Restful-API设计
[toc]
RESTFul
- REST(Representational State Transfer),表现层状态转移。
- 它首次出现在2000年Roy Fielding的博士论文中,Roy
Fielding是HTTP规范的主要编写者之一。
- 表现层是资源的表现层,对于网络中的资源就需要URI(Uniform Resource Identifier)来指向。
1.协议
- 使用HTTP或者HTTPS。对外若有安全性要求,可以使用HTTPS。但是内部服务间调用可以使用HTTP或HTTPS。
2.HTTP方法
- HTTP请求中的方法表示执行的*动作*
常用方法(动词) | 说明 |
---|---|
GET | 获取资源 |
POST | 创建新的资源 |
PUT | 更新资源 |
PATCH | 部分更新资源 |
DELETE | 删除资源 |
3.使用名称
URL指向*资源*,在URL路径的描述中,只需要出现名称,而不要出现动词。动词由HTTP方法提供。
不要单复数混用,建议名称使用复数。
Restful的核心是资源,URL应该指向资源,所以应该是使用名称表达式,而不是动词表达。
方法 | 路径 | 说明 |
---|---|---|
GET | /posts |
返回所有文章 |
GET | /posts/10 |
返回id为10的文章 |
POST | /posts |
创建新的文章 |
PUT | /posts/10 |
更新id为10的文章 |
DELETE | /posts/10 |
删除id为10的文章 |
PATCH | /posts/10 |
部分更新id为10的文章数据 |
- 不要出现如下的访问路径
/getAllPosts /addPost /updatePost /delPost
- GET方法只是获取资源,而不是改变资源状态。改变资源请使用POST,PUT,DELETE等方法。
- 例如:使用=GET /posts/10=就可以获取资源了,但是却使用=Get /posts/10/del=或=GET /posts/10?v=del=,本意是想删除。但这样不好,GET方法请求只为获取资源,不要改变资源状态。
- 子资源的访问
方法 | 路径Endpoint | 说明 |
---|---|---|
GET | /posts/10/authors |
返回id为10的文章的所有作者 |
GET | /posts/10/authors/8 |
返回id为10的文章的作者中id为4的 |
4.集合功能
- 过滤Filtering
- 指定过滤条件=GET /posts?tag=python=
- 排序Sorting
- 指定排序条件。有很多种设计风格,例如使用=+=表示asc,=-
表示desc。=GET /posts?sort=+title,-id=获取=GET /posts?sort=title_asc,id_desc
- 指定排序条件。有很多种设计风格,例如使用=+=表示asc,=-
- 分页Pagination
- 一般情况下,查询返回的记录数非常多,必须分页。=GET /posts?page=58&size=20=
5.状态码
- 使用HTTP响应的状态码表示动作的成功与否。
- 2xx表示用户请求服务端成功的处理;4xx表示用户请求的错误;5xx表示服务器端出错了。
Status Code | 说明 | Method | 说明 |
---|---|---|---|
200 | OK | GET | 成功获取资源 |
201 | CREATED | POST,PUT,PATCH | 成功创建或修改 |
204 | NO CONTENT | DELETE | 成功删除资源 |
400 | Bad Request | ALL | 请求中有错误,例如:GET时参数有问题,PUT时提交的数据错误等 |
401 | Unauthorized | ALL | 权限未通过认证 |
403 | Forbidden | ALL | 有无权限都禁止访问该资源 |
404 | Forbidden | ALL | 请求资源不存在 |
500 | internal Server Error | ALL | 服务器端错误 |
6.错误处理
- 在Restful API设计中,错误处理也非常重要。单单从无状态码中无法详尽描述错误的信息。
返回消息
{error:"user NOT Found"}
从错误消息中了解到错误号、错误信息、错误描述等信息。甚至更详细的信息可以通过code查阅文档
{ "code":10056, "message":"Invalid ID", "description":"More details" }
7.版本
- 强烈要求使用版本、版本号使用简单数字,例如v2。
- 2种风格
http://api.xdd.com/v1/posts/10
这种风格会跨域,适合较大的项目http://www.xdd.com/api/v1/posts/10
8. 返回结果
方法 | 路径 | 说明 |
---|---|---|
GET | /posts | 返回所有文章的列表 |
GET | /posts/10 | 返回id为10的文章对象 |
POST | /posts | 创建更新的文章并返回这个对象 |
PUT | /posts/10 | 更新id为10的文章并返回这个对象 |
DELETE | /posts/10 | 删除id为10的文章返回一个空对象 |
PATCH | /posts/10 | 部分更新id为10的文章数据并返回这个对象 |
- 数据一律采用JSON格式
注册接口设计和实现
[toc]
- 提供用户注册处理
- 提供用户登录处理
- 提供路由配置
用户注册接口设计
- 接受用户通过Post方法提交的注册信息,提交的数据是JSON格式数据
- 检查email是否存在与数据库表中,如果存在返回错误状态码,例如4xx,如果不存在,将用户提交的数据存入表中
- 整个过程都采用AJAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON。
POST /users/ 创建用户 请求体 application/json { "password":"string", "name":"string", "email":"string" } 响应 201 创建成功 400 请求数据错误
路由配置
- 为了避免项目中的urls.py条目过多,也为了让应用自己管理路由,采用多级路由
# djweb/urls.py文件 from django.urls import include urlpatterns = [ path('admin/', admin.site.urls), re_path(r'^$',index), re_path(r'^index$',index),#不同路径可以指向同一个函数执行 re_path(r'^users/',include("user.urls")), ]
- include函数参数写*应用.路由模块*,该函数就会动态导入指定的包的模块,从模块里面读取urlpatterns,返回三元组。
- url函数第二参数如果不是可调用对象,如果是元组或列表,则会从路径中除去已匹配的部分,将剩余部分与应用中的路由模块的urlpatterns进行匹配。
- 新建=user/uls.py=文件
# user/uls.py from django.conf.urls import re_path from .views import reg urlpatterns = [ re_path(r'^$',reg), #/users/ ]
- 在=user/views.py=文件中添加reg方法
# user/views.py from django.shortcuts import render from django.http import HttpResponse,HttpRequest def reg(request:HttpRequest): return HttpResponse("user.reg")
- 浏览器中输入=http://127.0.0.1:8000/users/=%E6%B5%8B%E8%AF%95(%E8%BF%99%E6%98%AFGET%E8%AF%B7%E6%B1%82),%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0%E5%93%8D%E5%BA%94%E7%9A%84%E6%95%B0%E6%8D%AE%E3%80%82%E4%B8%8B%E9%9D%A2%E5%BC%80%E5%A7%8B%E5%AE%8C%E5%96%84%E8%A7%86%E5%9B%BE%E5%87%BD%E6%95%B0。
- 在=user/views.py=中编写*视图函数reg*
测试JSON数据
- 使用POST方法,提交数据类型为application/json,json字符串要使用*双引号*。
这个数据是注册用的,由客户端提交。
- 数据提交模板为:
{ "password":"abc", "name":"xdd", "email":"[email protected]" }
- 可以使用Postman软件测试。
CSRF处理
- 在Post数据的时候,发现出现了下面的提示
- 原因:*默认Django CsrfViewMiddleware中间件会对所有POST方法提交的信息做CSRF校验*。
- CSRF或XSRF(Cross-site Request Forgery),即跨站请求伪造。它也被称为:one click attack/session riding。是一种对网站的恶意利用。它伪造成来自受信任用户发起的请求,难以防范。
- 原理:
- 用户登录某网站A完成登录认证,网站返回敏感信息的Cookie,即使是会话级的Cookie
- 用户没有关闭浏览器,或认证的Cookie一段时间内不过期还持久化了,用户就访问攻击网站B
- 攻击网站B看似一切正常,但是某些页面里面有一些隐藏运行的代码,或者诱骗用户操作的按钮等
- 这些代码一旦运行就是悄悄地向网站A发起特殊请求,由于网站A的Cookie还有效,且访问的是网站A,则其Cookie就可以一并发给网站A
- 网站A看到这些Cookie就只能认为是登录用户发起的合理合法请求,就会执行
- CSRF解决
关闭CSRF中间件(不推荐)
#djweb/settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', #注释掉 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
- csrftoken验证
- 在表单POST提交时,需要发给服务器一个csrf_token
- 模板中的表单Form中增加={% csrf_token %}=,它返回到了浏览器端就会为cookie增加csrftoken字段,还会在表单中增加一个名为csrfiddlewaretoken隐藏控件=<input type='hidden' name='csrfmiddlewaretoken' value='jZTxU0v5mPoLvugcfLbS1B6vT8COYrKuxMzodWv8oNAr3a4ouWlb5AaYG2tQi3dD' />=
- POST提交表单数据时,需要将csrfmiddlewaretoken一并提交,Cookie中的csrf_token也一并会提交,最终在中间件中比较,相符通过,不相符就看到上面的403提示
- 双cookie验证
- 访问本站先获得csrftoken的cookie
- 如果使用AJAX进行POST,需要在每一次请求Header中增加自定义字段X-CSRFTOKEN,其值来自cookie中获取的csrftoken值
- 在服务端比较cookie和X-CSRFTOKEN中的csrftoken,相符通过
- 现在没有前端代码,为了测试方便,可以选择第一种方法先禁用中间件,测试完成后开启。
JSON数据处理
- simplejson标准库方便好用,功能强大。
pip install simplejson
- 浏览器端提交的数据放在了请求对象的body中,需要使用simplejson解析,解析的方式同Json模块,但是simplejson更方便。
错误处理
- Django中有很多异常类,定义在django.http下,这些类都继承自HttpResponse。
#user/views.py文件 from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse import simplejson def reg(request:HttpRequest): print(request.POST) print(request.body) # print("- " * 30) try: payload = simplejson.loads(request.body) email = payload['email'] name = payload['name'] password = payload["password"] print(email,name,password) return JsonResponse({},status=201) #创建成功返回201 except Exception as e: #有任何异常,都返回 print(e) return HttpResponseBadRequest() #这里返回实例,这不是异常类
- 将上面代码增加邮箱检查、用户信息保存功能,就要用到Django的模型操作。
- 本次采用Restful实践的设计,采用返回错误状态码+JSON错误信息方式。
注册代码 v1
# user/views.py文件 from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse import simplejson from .models import User def reg(request:HttpRequest): try: payload = simplejson.loads(request.body) email = payload['email'] query = User.objects.filter(email=email) print(query) print(query.query) #查看sQL语句 if query.first(): return JsonResponse({"error":"用户已存在"},status=400); name = payload['name'] password = payload["password"] print(email,name,password) user = User() user.email = email user.name = name user.password = password user.save() return JsonResponse({},status=201) #创建成功返回201 except Exception as e: #有任何异常,都返回 print(e) # 失败返回错误信息和400,所有其他错误一律用户名密码错误 return JsonResponse({"error":"用户名或密码错误"},status=400)
- 邮箱检查
- 邮箱检查需要查user表,需要使用filter方法。
- email=email,前面是字段名email,后面是email变量。查询后返回结果,如果查询有结果,则说明该email已经存在,邮箱已经注册,返回400到前端
- 用户信息存储
- 创建User类实例,属性存储数据,最后调用save方法。Django默认是在save()、delete()的时候事务*自动提交*。
- 如果提交抛出任何错误,则捕获此异常做相应处理。
- 如果没有异常,则返回201,不要返回任何用户信息。之后可能需要验证、用户登录等操作。
- 异常处理
- 出现获取输入框提交信息异常,就返回400
- 查询邮箱存在,返回400
- save()方法保存数据,有异常,则返回400
- 注意一点,Django的异常类继承自HttpResponse类,所以不能raise,只能return
- 前端通过状态码判断是否成功
- 由于采用Restful实战,所有异常全部返回JSON的错误信息,所以一律使用了JsonResponse
Django日志
- Django的日志配置在settings.py中
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'handlers': ['console'], 'level': "DEBUG", }, }, }
- 配置后,就可以在控制台看到执行的SQL语句。
- 注意,必须*DEBUG=True,同时level是DEBUG*,否则从控制台看不到SQL语句。
模型操作
管理器对象
- Django会为模型类提供一个*objects对象*,它是django.db.models.manager.Manager类型,用于与数据库交互。当定义模型类的时候没有指定管理器,则Django会为模型类提供一个objects的管理器。
- 如果在模型类中手动指定管理器后,Django不再提供默认的objects的管理器了。
- 管理器是Django的模型进行数据库*查询*操作的接口,Django应用的每个模型都至少拥有一个管理器。
- Django ORM
- 数据的校验validation是在对象的Save、update方法上
- 对模型对象的CRUD,被Django ORM转换成相应的SQL语句操作不同的数据源。
查询
- 查询集
- 查询会返回结果的集,它是django.db.models.query.QuerySet类型。
- 它是惰性求值,和sqlalchemy一样。结果就是查询的集。
- 它是可迭代对象。
- 惰性求值
- 创建查询集不会带来任何数据库的访问,直到调用方法使用数据时,才会访问数据库。在迭代、序列化、if语句中都会立即求值。
- 缓存:
- 每一个查询集都包含一个缓存,来最小化对数据库的访问。
- 新建查询集,缓存为空。首次对查询集求值时,会发生数据库查询,Django会把查询的结果存在这个缓存中,并返回请求的结果,接下来对查询集求值将使用缓存的结果。
- 观察下面的2个例子是要看真正生成的语句了
没有使用缓存,每次都要去查库,查了2次库
[user.name for user in User.objects.all()] [user.name for user in User.objects.all()]
下面的语句使用缓存,因为使用同一个结果集
qs = User.objects.all() [user.name for user in qs] [user.name for user in qs]
- 限制查询集(切片)
- 查询集对象可以直接使用索引下标的方式(不支持负索引),相当于SQL语句中的limit和offset子句。
- 注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。
qs = User.objects.all()[20:40] # LIMIT 20 OFFSET 20 qs = User.objects.all()[20:30] # LIMIT 10 OFFSET 20
- 过滤器
- 返回*查询集*的方法,称为过滤器,如下:
名称 说明 all() filter() 过滤,返回满足条件的数据 exclude() 排除,排除满足条件的数据 order_by() 排序,注意参数是字符串 values() 返回一个对象字典的列表,列表的元素是字典,字典内是字段和值的键值对 filter(k1=v1).filter(k2=v2)=等价于=filter(k1=v1,k2=v2)
filter(pk=10)=这里pk指的就是主键,不用关心主键字段名,当然也可以使用主键名=filter(emp_no=10)
mgr = User.objects print(mgr.all()) print(mgr.values()) print(mgr.filter(pk=1).values()) print(mgr.exclude(pk=4)) print(mgr.exclude(id=1).values()) print(mgr.exclude(id=6).order_by("-id")) print(mgr.exclude(id=6).order_by("-id").values()) print(mgr.exclude(id=6).values().order_by("-pk"))
- 返回*单个值*的方法
名称 说明 get() 仅返回单个满足条件的对象如果未能返回对象则抛出DoesNotExist异常;如果能返回多条,抛出MultipleObjectsReturned异常 count() 返回当前查询的总条数 first() 返回第一个对象 last() 返回最后一个对象 exit() 判断查询集中是否有数据,如果有则返回True email = "[email protected]" mgr = User.objects print(mgr.get(pk=4)) #使用主键查询 print(mgr.get(email=email)) #只能返回一个结果 print(mgr.filter(id=1).get()) print(mgr.first()) #使用limit 1查询,返回实例或None print(mgr.filter(pk=4,email=email).first()) # and条件
- 字段查询(Field Lookup)表达式
- 字段查询表达式可以作为filter(),exclude(),get()的参数,实现where子句。
- 语法:=属性名称__比较运算符=值=
- 注意:属性名和运算符之间使用*双下划线*
- 比较运算符如下
名称 举例 说明 exact
filter(isdeleted=False)==filter(isdeleted__exact=False)
严格等于,可省略不写 contains
exclude(title__contains
"天")=是否包含,大小写敏感,等价于=like '%天%'= statswith==endswith
filter(title__startswith
'天')=以什么开头或结尾,大小写敏感 isnull==isnotnull
filter(title__isnull=False)
是否为null iexact==icontains==istartswith==iendswith
i的意思是忽略大小写 in
filter(pk__in
[1,2,3,100])=是否在指定范围数据中 gt
,=gte=,=lt=,=lte=filter(id__get=3)==filter(pk__lte=6)==filter(pub_date__get=date(2000,1,1))
大于,小于等 year、month、day==week_day==hour、minute==second
filter(pub_date__year=2000)
对日期类型属性处理 mgr = User.objects print(mgr.filter(id__exact=4)) print(mgr.filter(email__contains='xdd')) print(mgr.filter(email__istartswith='xdda')) print(mgr.filter(id__in=[1,4])) print(mgr.filter(id__gt=3))
- Q对象
- 虽然Django提供传入条件的方式,但是不方便,它还提供了Q对象来解决。
- Q对象是django.db.models.Q,可以使用=&、|=操作符来组成逻辑表达式。=~=表示not。
from django.db.models import Q mgr = User.objects print(mgr.filter(Q(pk__lt=6))) #不如直接写filter(pk__lt=6) print(mgr.filter(pk__lt=6).filter(pk__gt=1)) #与 print(mgr.filter(Q(pk__lt=6) & Q(pk__gt=1))) #与 print(mgr.filter(Q(pk=4) | Q(pk=1))) #或 print(mgr.filter(~Q(pk__lt=6))) #非
- 可使用=&|=和Q对象来构造复杂的逻辑表达式
- 过滤器函数可以使用一个或多个Q对象
- 如果混用关键字参数和Q对象,那么Q对象必须位于关键字参数的面前。所有参数都将and和一起
- 新增、更新、删除方法
- 更新数据
user = User(email='test3',name='test3') #没有主键 user.save() #会新建一个数据 user = User(id=100,email='test4',name='test4') #有自增主键,如果不存在,则是插入 user.save() user = User(id=100,email='test4',name='test4') # 有自增主键,如果存在,则是更新 user.save()
- update在查询集上同时更新数据
# 更新所有查询的结果 User.objects.filter(id__get=4).update(password='xyz') #将pk大于4的查询结果更新,所有用户的密码修改
- delete删除查询集数据
ret = User.objects.filter(id__gt=4).delete() print(ret) # 运行结果 # DELETE FROM `user` WHERE `user`.`id` > 4 ; args=(4,) # (3,{"user.User":3})
注册接口设计完善
- *认证*:HTTP协议是无状态协议,为了解决它产生了cookie和session技术。
- 传统的session-cookie机制
- 浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为这是第一次请求,会返回一个新的session id给浏览器,浏览器只要不关闭,这个session id就会随着每一次请求重新发给服务端,服务器端查找这个session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。
- session是会话级的,服务端还可以在这个会话session中创建很多数据session键值对。
- 这个session id有过期的机制,一段时间如果没有发起请求,认为用户已经断开,服务端就清除本次会话所有session。浏览器端也会清除相应的cookie信息。
- 服务端保存着大量session信息,很消耗服务器内存,而且如果多服务器部署,可以考虑session复制集群,也可以考虑session共享的问题,比如redis、memcached等方案。
- 无ssession方案
- 既然服务端就是需要一个ID来表示身份,那么不使用session也可以创建一个ID返回给客户端。但是,要保证客户端不可篡改该信息。
- 服务端生成一个标识,并使用某种算法对标识签名。
- 服务端收到客户端发来的标识,需要检查签名。
- 这种方案的缺点是,加密、解密需要消耗CPU计算资源,无法让浏览器自己主动检查过期的数据以清除。这种技术称作JWT(json WEB Token)。
JWT
- JWT(Json WEB Token)是一种采用Json方式安装传输信息的方式。本次使用PyJWT,它是Python对JWT的实现。
- 安装=pip install pyjwt=
- jwt原理
import jwt import base64 key = "secret" token = jwt.encode({'payload':'abc123'},key,'HS256') print(token) print(jwt.decode(token,key,algorithms=["HS256"])) # b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXlsb2FkIjoiYWJjMTIzIn0.lZc1PBGdbYDKq9k43tNTt1f0MHy4DjwT8NGTnHEIaVE' # token分为3部分,用.断开 header,payload,signature = token.split(b'.') print("- "*30) print(header,payload,signature,sep='\n') print("- "*30) def addeq(b:bytes): """ 为base64编码补齐等号""" rest = 4 - len(b) % 4 return b + b'='* rest print("header=",base64.urlsafe_b64decode(addeq(header))) print("payload=",base64.urlsafe_b64decode(addeq(payload))) print("signature=",base64.urlsafe_b64decode(addeq(signature))) print(" ="*30) # 根据jwt算法,重新生成签名 # 1 获取算法对象 from jwt import algorithms alg = algorithms.get_default_algorithms()["HS256"] newkey = alg.prepare_key(key) # key 为secret # 2获取前两部分 header.payload signing_input,_,_ = token.rpartition(b".") print(signing_input) # 3使用key签名 signature = alg.sign(signing_input,newkey) print("--------------") print(signature) print(base64.urlsafe_b64encode(signature))
- 由此,可知jwt生成的token分为三部分
- header,由数据类型、加密算法构成
- payload,负载就是要传输的数据,一般来说放入python对象即可,会被json序列化的
- signature,签名部分。是签名2部分数据分别base64编码后使用点号链接后,加密算法使用key计算好一个结果,再被base64编码,得到签名
- 所有数据都是明文传输的,只是做了base64,如果是敏感信息,请不要使用jwt。
- 数据签名的目的不是为了隐藏数据,而是保证数据不被篡改。如果数据篡改了,发回到服务器端,服务器使用自己的key再计算一遍,然后进行签名比对,一定对不上签名。
- jwt使用场景
- 认证:这是jwt最常用的场景,一旦用户登录成功,就会得到wt,然后请求中就可以带上这个jwt。服务器中jwt验证通过,就可以被允许访问资源。甚至可以在不同域名中传递,在单点登录(Single Sign On)中应用广泛。
- 数据交换:jwt可以防止数据被篡改,它还可以使用公钥、私钥加密,确保请求的发送者是可信的
- 密码
- 使用邮箱+密码方式登录。
- 邮箱要求唯一就行了,但是,密码存储需要加密。早期,都是用明文的密码存储。后来,使用MD5存储,但是,目前也不安全,网上有很多MD5的网站,使用反查方式找到密码。
- *加盐*,使用hash(password+salt)的结果存入数据库中,就算拿到数据库的密码反查,也没有用了。如果是固定加盐,还是容易被找到规律,或者从源码中泄露。随机加盐,每一次盐都变,就增加了破解的难度。
- *暴力破解*:什么密码都不能保证不被暴力破解,例如穷举。所以要使用慢hash算法,例如bcrypt,就会让每一次计算都很慢,都是秒即的,这样穷举的时间就会很长,为了一个密码破解的时间在当前CPU或者GPU的计算能力下可能需要几十年以上。
bcrypt
- 安装=pip install bcrypt=
import bcrypt import datetime password = b'12345xdd' # 每次拿到盐都不一样 print(1,bcrypt.gensalt()) print(2,bcrypt.gensalt()) salt = bcrypt.gensalt() # 拿到的盐不同,计算等到的密文相同 print("===============same salt ===============") x = bcrypt.hashpw(password,salt) y = bcrypt.hashpw(password,salt) print(3,x) print(4,y) # 每次拿到的盐不同,计算生成的密文也不一样 print("============different salt===============") xx = bcrypt.hashpw(password,bcrypt.gensalt()) yy = bcrypt.hashpw(password,bcrypt.gensalt()) print(5,x) print(6,y) # 校验 print(bcrypt.checkpw(password,xx),len(xx)) print(bcrypt.checkpw(password+b' ',xx),len(xx)) # 计算时长 start = datetime.datetime.now() y3 = bcrypt.hashpw(password,bcrypt.gensalt()) delta = (datetime.datetime.now() - start).total_seconds() print(10,'duration={}'.format(delta)) # 检验时长 start = datetime.datetime.now() y4 = bcrypt.checkpw(password,xx) delta = (datetime.datetime.now() - start).total_seconds() print(y4) print(11,'duration={}'.format(delta)) start = datetime.datetime.now() y5 = bcrypt.checkpw(b'1',xx) delta = (datetime.datetime.now() - start).total_seconds() print(y5) print(12,'duration={}'.format(delta))
- 从耗时看出,bcrypt加密、验证非常耗时,所有如果穷举,非常耗时。而且碰巧攻破一个密码,由于盐不一样,还等穷举另一个。
salt=b'$2b$12$jwBD7mg9stvIPydF2bqoPO' b'$2b$12$jwBD7mg9stvIPydF2bqoPOodPwWYVvdmZb5uWWuWvlf9iHqNlKSQO' $是分隔符 $2b$,加密算法 12,表示2^12 key expansion rounds 这是盐b'jwBD7mg9stvIPydF2bqoPO',22个字符,Base64 这是密文b'odPwWYVvdmZb5uWWuWvlf9iHqNlKSQO',31个字符,Base64
注册代码 v2
全局变量
- 项目的settings.py文件实际上就是全局变量的配置 文件。
- SECRET_KEY一个强KEY
from django.conf import settings print(settings.SECRET_KEY)
- 使用jwt和bcrypt,修改注册代码
from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse import simplejson from .models import User import jwt import datetime import bcrypt from django.conf import settings # 对id签名 def gen_token(user_id): # 时间戳用来判断是否过期,以便重发token或重新登录 return jwt.encode({ "user_id":user_id, "timestamp":int(datetime.datetime.now().timestamp()) #取整 },settings.SECRET_KEY).decode() def reg(request:HttpRequest): try: payload = simplejson.loads(request.body) email = payload['email'] query = User.objects.filter(email=email) print(query) print(query.query) #查看sQL语句 if query.first(): return JsonResponse({"error":"用户已存在"},status=400); name = payload['name'] password = payload["password"].encode() print(email,name,password) # 密码加密 password = bcrypt.hashpw(password,bcrypt.gensalt()).decode() print(password) user = User() user.email = email user.name = name user.password = password user.save() return JsonResponse({"token":gen_token(user.id)},status=201) #创建成功返回201 except Exception as e: #有任何异常,都返回 print(e) # 失败返回错误信息和400,所有其他错误一律用户名密码错误 return JsonResponse({"error":"用户名或密码错误"},status=400)
- save方法会自动提交事务
- 数据库中内容
登录接口设计和实现
[toc]
- 提供用户注册处理
- 提供用户登录处理
- 提供路由配置
用户登录接口设计
POST /users/login 用户登录 请求体 application/json { "password":"string", "email":"string" } 响应 200 登录成功 400 用户名密码错误
- 接收用户通过POST方法提交的登录信息,提交的数据是JSON格式数据
{ "password":"abc", "email":"[email protected]" }
- 从user表中email找出匹配的一条记录,验证密码是否正确
- 验证通过说明是合法用户登录,显示欢迎页面。
- 验证失败返回错误状态码,例如4xx
- 整个过程都采用AJAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON。
路由配置
# 修改user/urls.py文件 from django.conf.urls import re_path from .views import reg,login urlpatterns = [ re_path(r'^$',reg), #/users/ re_path(r'^login$',login), ]
- 在user/views.py文件中实现登录代码
# user/views.py文件中增加如下代码 import jwt import datetime import bcrypt from django.conf import settings # 筛选所需要的字段 def jsonify(instance,allow=None,exclude=[]): # allow优先,如果有,就使用allow指定的字段,这时候exclude无效 # allow如果为空,就全体,但要看看有exclude中的要排除 modelcls = type(instance) if allow: fn = (lambda x:x.name in allow) else: fn = (lambda x:x.name not in exclude) # from django.db.models.options import Options # m:Options = modelcls._meta # print(m.fields,m.pk) return {k.name:getattr(instance,k.name) for k in filter(fn,modelcls._meta.fields)} # 登录接口 def login(request:HttpRequest): try: payload = simplejson.loads(request.body) print(payload) email = payload["email"] password = payload["password"] user = User.objects.get(email=email) # only one print(user.password) if bcrypt.checkpw(password,user.password.encode()): # 验证成功 token = gen_token(user.id) res = JsonResponse({ "user":jsonify(user,exclude=["password"]), "token":token }) #返回200 res.set_cookie("jwt",token) return res else: return JsonResponse({"error":"用户名或密码错误"},status=400) except Exception as e: print(e) #失败返回错误信息和400,所有其他错误一律用户名密码错误 return JsonResponse({"error":"用户名或密码错误"},status=400)
- 注册
- 登录验证
认证接口
- 如何获取浏览器提交的token信息?
- 使用Header中的Authorization
- 通过这个header增加token信息。
- 通过header发送数据,方法可以是Post、Get
- 自定义header
- 在Http请求头中使用X-JWT字段来发送token
- 使用Header中的Authorization
- 本次选择第二种
认证流程
- 基本上所有的业务都有需要认证用户的信息。
- 在这里比较实际戳,如果过期,就直接抛未认证成功401,客户端收到后就改直接跳转到登录页。
- 如果没有提交user id,就直接重新登录。如果用户查到了,填充user对象。
- request->时间戳比较->user id比较->向后执行
Django的认证
- django.contrilb。auth中提供了许多方法,这里主要介绍其中的三个:
- authenticate(**credentials)
- 提供了用户认证,即验证用户名以及密码是否正确
- user = authentica(username='someone',password='somepassword')
- login(HttpRequest,user,backend=None)
- 该函数接受一个HttpRequest对象,以及一个认证了的User对象
- 此函数使用django的session框架给某个已认证的用户附加上session id等信息。
- logout(request)
- 注销用户
- 该函数接受一个HttpRequest对象,无返回值。
- 当调用该函数时,当前请求的session信息会全部清除
- 该用户即使没有登录,使用该函数也不会报错
- 还提供了一个装饰器来判断是否登录django.contrib.auth.decorators.login_required
- 本项目使用了无session机制,且用户信息自己建表管理,所以,认证需要自己实现。
- authenticate(**credentials)
中间件技术Middleware
- 官方定义,在Django的request和response处理过程中,由框架提供的hook钩子
- 中间技术在1.10后发生了改变,我们当前使用1.11版本,可以使用新的方式定义。
- 参考https://docs.djangoproject.com/en/1.11/topics/http/middleware/#writing-your-own-middleware
- 原理
# 测试代码添加在user/views.py class SimpleMiddleware1: def __init__(self,get_response): self.get_response = get_response # One-time configuration and initialization. def __call__(self,request): # Conde to be executed for each request before # the view (and later middleware) are called. print(1,'- '*30) print(isinstance(request,HttpRequest)) print(request.GET) print(request.POST) print(request.body) # 之前相当于老板本的process_request #return HttpResponse(b'',status = 404) response = self.get_response(request) #Code to be executed for each request/response after #the view is called. print(101,'- '* 30) return response def process_view(self,request,view_func,view_args,view_kwargs): print(2,"-" * 30) print(view_func.__name__,view_args,view_kwargs) # 观察view_func名字,说明在process_request之后,process_view之前已经做好了路径映射 return None # 继续执行其他的process_view或view # return HttpResponse("111",status=201) class SimpleMiddleware2: def __init__(self,get_response): self.get_response = get_response # One-time configuration and initialization. def __call__(self, request): # Conde to be executed for each request before # the view (and later middleware) are called. print(3,"-" * 30) # return HttpResponse(b'',status=404) response = self.get_response(request) # Code to be executed for each request/response after # the view is called. print(102,"- "* 30) return response def process_view(self,request,view_func,view_args,view_kwargs): print(4,"- "* 30) print(view_func,__name__,view_args,view_kwargs) # return None #继续执行其他的process_view或view return HttpResponse("2222",status=201)
- 修改=djweb/settings.py=文件,添加消息中间件
# 修改djweb/settings.py文件,添加消息中间件 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', #注释掉 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'user.views.SimpleMiddleware1', 'user.views.SimpleMiddleware2', ]
- 运行结果(使用浏览器访问web)
- 流程图
- 结论
- Django中间件使用的洋葱式,但有特殊的地方
- 新版本中间件现在=__call__=中get_response(request)之前代码(相当于老版本中的process_request)
- settings中的顺序先后执行所有中间件的get_response(request)之前代码
- 全部执行完解析路径映射得到view_func
- settings中顺序先后执行process_view部分
- return None 继续向后执行
- return HttpResponse() 就不在执行其他函数的preview函数了,此函数返回值作为浏览器端的响应
- 执行view函数,前提是签名的所有中间件process_view都返回None
- 逆序执行所有中间件的get_response(request)之后代码
- 特别注意,如果get_response(request)之前代码中return HttpResponse(),将从当前中间件立即返回给浏览器端,从洋葱中依次反弹
- 自定义中间件用户验证
class BlogAuthMiddleware(object): """自定义中间件""" def __init__(self,get_response): self.get_response = get_response # 初始化执行一次 def __call__(self,request): # 视图函数之前执行 # 认证 print(type(request),"++++++++++++++") print(request.GET) print(request.POST) print(request.body) #json数据 print("- "* 30) response = self.get_response(request) #视图函数之后执行 return response # 要在setting的MIDDLEWARE中注册
- 中间件拦截所有视图函数,但是只有一部分请求需要提供认证,所以考虑其他方法。
- 如果绝大多数都需要拦截,个别例外,采用中间件比较合适。
- 中间件有很多用户,适合拦截所有请求和响应。例如浏览器端的IP是否禁用、UserAgent分析、异常响应的统一处理。
- 用户验证装饰器
- 在需要认证的view函数上增强认证功能,写一个装饰器函数。谁需要认证,就在这个view函数上应用这个装饰器。
定义常量,可以在当前模块中,也可以定义在settings.py中。本次在=djweb/setting.py=中添加
# 在`djweb/setting.py`中添加 #自定义常量 AUTH_EXPIRE = 8* 60 * 60 #8小时过期 AUTH_HEADER = "HTTP_JWT" #浏览器端是jwt,服务器端被改写为全大写并加HTTP_前缀
用户验证装饰器代码
# 本次写在user、views.py # 登录验证装饰器 def authenticate(viewfunc): def wrapper(request:HttpRequest): # 认证越早越好 jwtheader = request.META.get(settings.AUTH_HEADER,"") # print(request.META.keys()) # print(request.META["HTTP_COOKIE"].get(settings.AUTH_HEADER,"")) # print(request.META["HTTP_COOKIE"]) print("- ------------") if not jwtheader: return HttpResponse(status=401) print(jwtheader) # 解码 try: payload = jwt.decode(jwtheader,settings.SECRET_KEY,algorithms=["HS256"]) # payload = "aa" print(payload) except Exception as e: #解码有任何异常,都不能通过认证 print(e) return HttpResponse(status=401) # 是否过期ToDO print("- "*30) try: user_id = payload.get("user_id",0) if user_id == 0: return HttpResponse(status=401) user = User.objects.get(pk=user_id) request.user = user except Exception as e: print(e) return HttpResponse(status=401) response = viewfunc(request) return response return wrapper @authenticate #在有需要的视图函数上加上此装饰器 def test(request): print("- "*30,"test") print(request.user) return JsonResponse({},status=200) # 修改原先gen_token(user_id)函数 # 对id签名 def gen_token(user_id): # 时间戳用来判断是否过期,以便重发token或重新登录 return jwt.encode({ "user_id":user_id, "exp":int(datetime.datetime.now().timestamp()) + settings.AUTH_EXPIRE #取整 },settings.SECRET_KEY,algorithm="HS256").decode()
- 注册函数
from django.conf.urls import re_path from .views import reg,login,test urlpatterns = [ re_path(r'^$',reg), #/users/ re_path(r'^login$',login), #/users/login re_path(r"^test$",test), #/users/test ]
- 测试方法(先登录后测试)
- *JWT过期问题*(pyjwt过期)
- 在decode的时候,默认开启过期验证,如果过期,则抛出异常
- 需要在payload中增加claim exp,也就是exp的键值对,记录过期的时间点
- exp要求是一个整数int的时间戳,或时间
- exp键值对存在,才会进行过期校验
测试
import jwt import datetime import threading event = threading.Event() key = "xdd" #在jwt的payload中增加exp claim exp = int(datetime.datetime.now().timestamp()+10) data = jwt.encode({"name":'tom',"age":20,'exp':exp},key) print(jwt.get_unverified_header(data)) #不校验签名提取header try: while not event.wait(1): print(jwt.decode(data,key)) #过期校验就会抛出异常 print(datetime.datetime.now().timestamp()) except jwt.ExpiredSignatureError as e: print(e)
- view装饰器
- 注册、登录函数都是只支持POST方法,可以在试图函数内部自己判断,也可以使用官方提供的装饰器指定方法。
from django.views.decorators.http import require_http_methods,require_POST,require_GET @require_http_methods(["POST"])
代码参考
user/views.py
from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse import simplejson from .models import User import jwt import datetime import bcrypt from django.conf import settings from django.views.decorators.http import require_http_methods,require_POST,require_GET # 对id签名 def gen_token(user_id): # 时间戳用来判断是否过期,以便重发token或重新登录 return jwt.encode({ "user_id":user_id, "exp":int(datetime.datetime.now().timestamp()) + settings.AUTH_EXPIRE #取整 },settings.SECRET_KEY,algorithm="HS256").decode() # 注册接口 @require_http_methods(["POST"]) def reg(request:HttpRequest): try: payload = simplejson.loads(request.body) email = payload['email'] query = User.objects.filter(email=email) print(query) print(query.query) #查看sQL语句 if query.first(): return JsonResponse({"error":"用户已存在"},status=400) name = payload['name'] password = payload["password"].encode() print(email,name,password) # 密码加密 password = bcrypt.hashpw(password,bcrypt.gensalt()).decode() print(password) user = User() user.email = email user.name = name user.password = password user.save() return JsonResponse({"token":gen_token(user.id)},status=201) #创建成功返回201 except Exception as e: #有任何异常,都返回 print(e) # 失败返回错误信息和400,所有其他错误一律用户名密码错误 return JsonResponse({"error":"用户名或密码错误"},status=400) # 筛选所需要的字段 def jsonify(instance,allow=None,exclude=[]): # allow优先,如果有,就使用allow指定的字段,这时候exclude无效 # allow如果为空,就全体,但要看看有exclude中的要排除 modelcls = type(instance) if allow: fn = (lambda x:x.name in allow) else: fn = (lambda x:x.name not in exclude) # from django.db.models.options import Options # m:Options = modelcls._meta # print(m.fields,m.pk) # print("----------") return {k.name:getattr(instance,k.name) for k in filter(fn,modelcls._meta.fields)} # 登录接口 @require_POST def login(request:HttpRequest): try: payload = simplejson.loads(request.body) print(payload) email = payload["email"] password = payload["password"].encode() user = User.objects.get(email=email) # only one print(user.password) if bcrypt.checkpw(password,user.password.encode()): # 验证成功 token = gen_token(user.id) res = JsonResponse({ "user":jsonify(user,exclude=["password"]), "token":token }) #返回200 res.set_cookie("jwt",token) return res else: return JsonResponse({"error":"用户名或密码错误"},status=400) except Exception as e: print(e) #失败返回错误信息和400,所有其他错误一律用户名密码错误 return JsonResponse({"error":"用户名或密码错误"},status=400) # 登录验证装饰器 def authenticate(viewfunc): def wrapper(request:HttpRequest): # 认证越早越好 jwtheader = request.META.get(settings.AUTH_HEADER,"") # print(request.META.keys()) # print(request.META["HTTP_COOKIE"].get(settings.AUTH_HEADER,"")) # print(request.META["HTTP_COOKIE"]) print("- ------------") if not jwtheader: return HttpResponse(status=401) print(jwtheader) # 解码 try: payload = jwt.decode(jwtheader,settings.SECRET_KEY,algorithms=["HS256"]) # payload = "aa" print(payload) except Exception as e: #解码有任何异常,都不能通过认证 print(e) return HttpResponse(status=401) # 是否过期ToDO print("- "*30) try: user_id = payload.get("user_id",0) if user_id == 0: return HttpResponse(status=401) user = User.objects.get(pk=user_id) request.user = user except Exception as e: print(e) return HttpResponse(status=401) response = viewfunc(request) return response return wrapper @require_POST @authenticate #在有需要的视图函数上加上此装饰器 def test(request): print("- "*30,"test") print(request.user) return JsonResponse({},status=200)
博文接口实现
[toc]
- 功能分析
POST /posts/ 文章发布,试图类PostView 请求体 application/json { "title":"string", "content":"string" } 响应 201 发布成功 400 请求数据错误
GET /posts/(\d+) 查看指定文章,试图函数getpost 响应 200 成功返回文章内容 404 文章不存在
GET /posts/ 文章列表页,视图类PostView 响应 200 成功返回文章列表
创建博文应用
python manage.py startapp post
注意:*一定要把应用post加入到settings.py中*,否则不能迁移
# djweb/settings.py中修改 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'user', "post" ]
- 添加对应路由
修改djweb/urls.py文件
# 修改djweb/urls.py文件 urlpatterns = [ path('admin/', admin.site.urls), re_path(r'^$',index), re_path(r'^index$',index),#不同路径可以指向同一个函数执行 re_path(r'^users/',include("user.urls")), re_path(r'^posts/',include("post.urls")) ]
新建post/urls.py文件
# 新建post/urls.py文件 配置路由信息 from django.conf.urls import re_path from .views import PostView,getpost urlpatterns = [ re_path(r'^$',PostView.as_view()), #/posts/ 视图函数PostView re_path(r'^(\d+)$',getpost), ]
修改post/views.py文件
from django.http import HttpResponse,HttpRequest,JsonResponse from django.views.decorators.http import require_GET from django.views import View class PostView(View): pass def getpost(request:HttpRequest,id): print(id) return JsonResponse({},status=201)
构建数据库模型
- 修改post/models.py文件
from django.db import models from user.models import User # post表 class Post(models.Model): class Meta: db_table = "post" id = models.AutoField(primary_key=True) title = models.CharField(max_length=256,null=False) postdate = models.DateTimeField(null=False) # 从post查看作者,从post查看内容 author = models.ForeignKey(User,on_delete=models.PROTECT) #指定外键,migrate会生成author_id字段 # self.content可以访问Content实例,其内容是self.content.content def __repr__(self): return '<Post {} {} {} {} >'.format( self.id,self.title,self.author_id,self.content ) __str__ = __repr__ # content表 class Content(models.Model): class Meta: db_table = "content" # 没有主键,会自动创建一个自增主键 post = models.OneToOneField(Post,primary_key=True,on_delete=models.PROTECT) #一对一,这边会有一个外键post_id引用post.id content = models.TextField(null=False) def __repr__(self): return "<Content {} {} >".format(self.post.pk,self.content[:20]) __str__ = __repr__
- 注意:on_delete在Django2.0开始,on_delete必须提供,参考https://docs.djangoproject.com/en/1.11/ref/models/fields/#django.db.models.ForeignKey.on_delete
- models.CASCADE 级联删除。Django在DELETE级联上模拟SOL约束的行为,并删除包含外键的对象。
- models.PROTECT 通过引发ProtectedError (django.db.IntegrityError的子类)防止删除引用的对象。
- models.SET_NULL 设置外键为空;这只有在null为真时才有可能。
- models.SET_DEFAULT 将外键设置为其默认值;必须为外键设置默认值。
- models.DO_NOTHING 不采取任何行动。如果数据库后端强制执行引用完整性,这将导致IntegrityError,除非手动向数据库字段添加一个SOL ON DELETE约束。
- 视图分类
- function-based view 视图函数:视图功能有函数实现
- class-based view 视图类:视图功能有基于django.views.View类的子类实现
django.views.View类原理
- django.views.View类本质就是一个对请求方法分发到与请求方法同名函数的调度器。
- django.views.View类,定义了http的方法的小写名称列表,这些小写名称其实就是处理请求的方法名的小写。as_views()方法就是返回一个内建的=view(request,*args,**kwargs)=函数,本质上其实还是url映射到了函数上,只不过view函数内部会调用=dispatch(request,*args,**kwargs)=分发函数。
- dispatch函数中使用request对象的请求方法小写和http_method_names中允许的HTTP方法匹配,匹配说明是正确的HTTP请求方法,然后尝试再View子类中找该方法,调用后返回结果。找不到该名称方法,就执行http_method_not_allowed方法返回405状态码
- 看到了getattr等反射函数,说明基于反射实现的。
- as_view()方法,用在url映射配置中
- 本质上,as_view()方法还是把一个类伪装成了一个视图函数。
- 这个视图函数,内部使用了一个分发函数,使用请求方法名称吧请求分发给存在的同名函数处理。
# 修改post/urls.py文件 from django.conf.urls import re_path from .views import PostView,getpost from user.views import authenticate #登录验证装饰器 urlpatterns = [ # 路径/posts/ # View类调用as_view()之后类等价一个视图函数,可以被装饰 # 装饰器函数返回新函数 re_path(r'^$',authenticate(PostView.as_view())), #/posts/ 视图函数PostView re_path(r'^(\d+)$',getpost), ]
- 但是这种方式适合吧PostView类所有方法都认证,但是实际上就post方法要认证。所以,authenticate还是需要加载到post方法上去。因此,要修改authenticate函数
# 修改user/views.py文件中对应内容 # 登录验证装饰器 def authenticate(viewfunc): def wrapper(*args): *s,request = args #保证最后一个取到request对象 print(s) print(request) # 认证越早越好 jwtheader = request.META.get(settings.AUTH_HEADER,"") # print(request.META.keys()) # print(request.META["HTTP_COOKIE"].get(settings.AUTH_HEADER,"")) # print(request.META["HTTP_COOKIE"]) print("- ------------") if not jwtheader: return HttpResponse(status=401) print(jwtheader) # 解码 try: payload = jwt.decode(jwtheader,settings.SECRET_KEY,algorithms=["HS256"]) # payload = "aa" print(payload) except Exception as e: #解码有任何异常,都不能通过认证 print(e) return HttpResponse(status=401) # 是否过期ToDO print("- "*30) try: user_id = payload.get("user_id",0) if user_id == 0: return HttpResponse(status=401) user = User.objects.get(pk=user_id) request.user = user except Exception as e: print(e) return HttpResponse(status=401) response = viewfunc(*args) #参数解构 return response return wrapper
- 修改post/views.py文件
# post/views.py from django.http import HttpResponse,HttpRequest,JsonResponse from django.views.decorators.http import require_GET from django.views import View from user.views import authenticate class PostView(View): #不需要装饰器决定请求方法了 def get(self,request:HttpRequest): #获取全体文章走这里 print("get ~~~~~~~~~~~~~~~~~~") return JsonResponse({},status=200) # 注意:由于PostView类中并不是所有方法都需要登录认证,所有将urls路径映射中的登录认证去掉了,在这里加上。 @authenticate def post(self,request:HttpRequest):#提交文章数据走这里 print("post ++++++++++++++++++") return JsonResponse({}, status=200) @require_GET def getpost(request:HttpRequest,id): print(id) return JsonResponse({},status=201)
发布接口实现
- 用户从浏览器端提交json数据,包含title,content.
- 提交博文需要认证用户,从请求的header中验证jwt。
request:POST 标题、内容-》@authenticate ->视图 post -> json新文章对象
新建工具包,调整jsonify函数,放入工具包内
# utils/__init__.py # 筛选所需要的字段 def jsonify(instance,allow=None,exclude=[]): # allow优先,如果有,就使用allow指定的字段,这时候exclude无效 # allow如果为空,就全体,但要看看有exclude中的要排除 modelcls = type(instance) if allow: fn = (lambda x:x.name in allow) else: fn = (lambda x:x.name not in exclude) # from django.db.models.options import Options # m:Options = modelcls._meta # print(m.fields,m.pk) # print("----------") return {k.name:getattr(instance,k.name) for k in filter(fn,modelcls._meta.fields)}
显示事务处理
- Django中每一次save()调用就会自动提交,那么在第一次事务提交后如果第二次提交前出现异常,则post.save()不会回滚。为了解决,可以使用事务的原子方法:参考https://docs.djangoproject.com/en/1.11/topics/db/transactions/#django.db.transaction.atomic
- 事务的使用方法
装饰器用法
@transaction.atomic #装饰器用法 def viewfunc(request): # This code executes inside a transaction do_stuff()
上下文用法
def viewfunc(request): # This code executes in autocommit mode (Django's default). do_stuff() with transaction.atomic(): #上下文用法 # This code executes inside a transaction. do_more_stuff()
修改=post/views.py=文件
# 修改`post/views.py`文件 class PostView(View): #不需要装饰器决定请求方法了 def get(self,request:HttpRequest): #获取全体文章走这里 print("get ~~~~~~~~~~~~~~~~~~") return JsonResponse({},status=200) # 注意:由于PostView类中并不是所有方法都需要登录认证,所有将urls路径映射中的登录认证去掉了,在这里加上。 @authenticate def post(self,request:HttpRequest):#提交文章数据走这里 print("post ++++++++++++++") post = Post() content = Content() try: payload = simplejson.loads(request.body) post.title = payload["title"] post.author = User(id=request.user.id) # post.author = request.user post.postdate = datetime.datetime.now( datetime.timezone(datetime.timedelta(hours=8)) ) with transaction.atomic(): #原子操作 post.save() content.post = post content.content = payload["content"] content.save() return JsonResponse({ "post":jsonify(post,allow=["id","title"]) },status=200) except Exception as e: print(e) return HttpResponse(status=400)
- 启动后测试
文章接口实现
- 根据post_id查询博文并返回。
- 如果博文只能作者看到,就需要认证,本次是公开,即所有人都能看到,所以不需要认证。同样,下面的list接口也是不需要认证的。
request: GET post's id-> getpost 视图函数 -> Json post + content
# 修改`post/views.py`文件 @require_GET def getpost(request:HttpRequest,id): try: id = int(id) post = Post.objects.get(pk=id) #only one return JsonResponse({ "post":{ "id":post.id, "title":post.title, "author":post.author.name, "author_id":post.author_id, #post.author.id "postdate":post.postdate.timestamp(), "content":post.content.content } }) except Exception as e: print(e) return HttpResponse(status=404)
列表页接口实现
- 发起GET请求,通过查询字符串=http://url/posts/?page=2=%E6%9F%A5%E8%AF%A2%E7%AC%AC%E4%BA%8C%E9%A1%B5%E6%95%B0%E6%8D%AE
request: GET ?pate=5&size=20 ->视图 get -> json文章列表
GET /posts/?page=3&size=20 文章列表,视图类PostView 响应 200 成功返回文章列表
- 完善分页
- 分页信息,一般有:当前页/总页数、每页条数,记录总数。
- *当前页*:page
- *每页条数*:size ,每页最多多少行
- *总页数*:pages = math.ceil(count/size)
- *记录总数*:total,从select * from table来
- 分页信息,一般有:当前页/总页数、每页条数,记录总数。
# 修改post/views.py文件 from django.http import HttpResponse,HttpRequest,JsonResponse from django.views.decorators.http import require_GET from django.views import View from user.views import authenticate from post.models import Post,Content from user.models import User import simplejson,datetime,math from django.db import transaction from utils import jsonify class PostView(View): #不需要装饰器决定请求方法了 def get(self,request:HttpRequest): #获取全体文章走这里 print("get ~~~~~~~~~~~~~~~~~~") try: #页码 page = int(request.GET.get("page",1)) page = page if page >0 else 1 except: page = 1 try: #每页条数 #注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围 size = int(request.GET.get("size",20)) size = size if size >0 and size <101 else 20 except: size = 20 try: #每页条目数 start = (page - 1) * size posts = Post.objects.order_by("-pk") print(posts.query) total = posts.count() posts = posts[start:start + size] print(posts.query) return JsonResponse({ "posts":[jsonify(post,allow=["id","title"]) for post in posts], "pagination":{ "page":page, "size":size, "total":total, "pages":math.ceil(total / size) } }) except Exception as e: print(e) return HttpResponse(status=400)
- 也可以使用Django提供的Paginator类来完成。
- Paginator文档https://docs.djangoproject.com/en/1.11/topics/pagination/
- 但是,还是自己更加简单明了些。
改写校验函数
- 修改post/views.py文件
# 修改post/views.py文件 def validate(d:dict,name:str,type_func,default,validate_func): try: ret = type_func(d.get(name,default)) ret = validate_func(ret,default) except: ret = default return ret class PostView(View): #不需要装饰器决定请求方法了 def get(self,request:HttpRequest): #获取全体文章走这里 print("get ~~~~~~~~~~~~~~~~~~") # 页码 page = validate(request.GET,"page",int,1,lambda x,y:x if x>0 else y) # 每页条数 #注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围 size = validate(request.GET,"page",int,20,lambda x,y:x if x>0 and x<101 else y) try: #每页条目数 start = (page - 1) * size posts = Post.objects.order_by("-pk") print(posts.query) total = posts.count() posts = posts[start:start + size] print(posts.query) return JsonResponse({ "posts":[jsonify(post,allow=["id","title"]) for post in posts], "pagination":{ "page":page, "size":size, "total":total, "pages":math.ceil(total / size) } }) except Exception as e: print(e) return HttpResponse(status=400)
前端开发及登录功能实现
[toc]
开发环境设置
- 使用react-mobx-starter-master脚手架,解压更名为DjangoNode。
- 在src中新增component、service、css目录
- 注意:没有特别说明,js开发都在src目录下
- 目录结构
DjangoNode/ │-.babelrc │-.gitignore #git的忽略文件 │-.npmrc #npm的服务器配置,本次使用的是阿里 │-index.html #dev server使用的首页 │-jsconfig.json │-LICENSE │-package-lock.json │-package.json #项目的根目录安装文件,使用npm install可以构建项目 │-README.md │-webpack.config.dev.js #开发用的配置文件 │-webpack.config.prod.js #生产环境,打包用的配置文件 └─src/ │- componet/ #自己组件文件夹 │- service/ #服务程序 │- css/ #样式表 │- index.html #模板页面 │- index.js
修改项目信息=package.json=文件
{ "name":"blog", "description":"blog project", "author":"xdd" }
修改=webpack.config.dev.js=
devServer: { compress: true, /* gzip */ //host:'192.168.61.109', /* 设置ip */ port: 3000, publicPath: '/assets/', /* 设置bundled files浏览器端访问地址 */ hot: true, /* 开启HMR热模块替换 */ inline: true, /* 控制浏览器控制台是否显示信息 */ historyApiFallback: true, stats: { chunks: false }, proxy: { //代理 '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } }
安装依赖
npm install
- npm会按照package.json中依赖的包。也可以使用新的包管理工具yarn安装模块
- 使用yarn替换npm安装,速度会更快写,yarn是并行安装,npm是串行安装。
yarn安装 $ npm install -g yarn 或者,去https://yarn.bootcss.com/docs/install/ 相当于npm install $ yarn * 如果想自己构建脚手架,可以使用如下命令 $npm install #构建脚手架 相当于npm install react-router #添加react-router组件 $ yarn add react-router # 安装路由,即项目前端web路由 $ yarn add react-router-dom #
相关命令
npm命令 Yarn命令 解释 npm install
yarn install
安装 npm install [package] --save
yarn add [package]
安装运行时依赖 npm install [package] --save-dev
yarn add [package] --dev
安装开发时依赖 npm install [package] --global
yarn global add [package]
全局安装 npm uninstall [package]
yarn remove [package]
卸载
开发
前端路由
- 前端路由使用react-router组件完成
- 官方文档https://reacttraining.com/react-router/web/guides/quick-start
- 基本例子https://reacttraining.com/react-router/web/example/basic
- 使用react-route主键,更改src/index.js
/* src/index.js文件 */ import React from 'react'; import ReactDom from 'react-dom'; import {Route,Link,BrowserRouter as Router,Switch} from "react-router-dom"; function App() { return ( <Router> <div> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/about">About</Link> </li> <li> <Link to="/users">Users</Link> </li> </ul> <hr /> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/users" component={Users} /> </div> </Router> ); } function Home() { return ( <div> <h2>Home</h2> </div> ); } function About() { return ( <div> <h2>About</h2> </div> ); } const Users = () => { return ( <div> <h2>Users</h2> </div> ) } ReactDom.render(<App />, document.getElementById('root'));
- 注意:
- Link组件相当于a标签
- *Route*组件不可见,用来做Router的路由定义。当网页中的地址栏中地址发生改变,会从Route中匹配对应的路径加载对应的组件。
*启动项目*=yarn run start=
- start是在配置文件=dockerNode/package.json=中已经定义好了,如下:
{ "scripts": { "test": "jest", "start": "webpack-dev-server --config webpack.config.dev.js --hot --inline", "build": "rimraf dist && webpack -p --config webpack.config.prod.js" }, }
- 在地址栏中输入=http://127.0.0.1:3000/=%E6%88%96=http://127.0.0.1:3000/about=%E8%83%BD%E5%A4%9F%E7%9C%8B%E5%88%B0%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%8F%98%E5%8C%96。
- App中,使用了*Router路由组件,Router是根,且它只能有一个元素,所以添加了Div*
- 前端路由:通过一个url加载一个组件,将原来的组件替换。
- Route指令
- 它负责静态路由,只能和Route指定的path匹配,组件就可以显示。URL变化,将重新匹配路径
- component属性设置目标组件
- path是匹配路径,如果匹配则显示组件
exact
:布尔值strict
:布尔值
- 没有path属性,组件将总是显示,例如=<Route component={Always} />=
- path属性还支持路径数组,意思是多个路径都可以匹配
/* 修改src/index.js文件*/ function App() { return ( <Router> <div> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/about">About</Link> </li> <li> <Link to="/users">Users</Link> </li> </ul> <hr /> <Route exact path={["/","/index"]} component={Home} /> <Route path="/about" component={About} /> <Route path="/users" component={Users} /> <Route component={Always} /> </div> </Router> ); } function Always(){ return( <div id="footer"> <span>Copyright 2009-2019 xdd.com</span> </div> ) } ReactDom.render(<App />, document.getElementById('root'));
路由配置
- exact 只能匹配本路径,不包含子路径
- strict 路径尾部有=/=,则必须匹配这个=/=,也可以匹配子路径|
- exact strict 一起用,表示严格的等于当前指定路径
路径 /about
/about/
/about/123
path
"/about"=√ √ √ exact path
"/about"=√ √ exact path
"about"=√ √ strict path
"/about"=√ √ √ exact strict path
"/about"=√ strict path
"about"=√ √ exact strict path
"about"=√ Switch指令
- 也可以将Route组织到一个Switch中,一旦匹配Switch中的一个Route,就不再匹配其他。但是Route是匹配所有,如果匹配就会显示组件,无path的Route始终匹配。
- 示例:
/* 修改src/index.js文件*/ function App() { return ( <Router> <div> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/about">About</Link> </li> <li> <Link to="/users">Users</Link> </li> </ul> <hr /> <Switch> <Route path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/users" component={Users} /> <Route component={Always} /> </Switch> </div> </Router> ); }
- 注意这个时候Always组件,其实是404组件了,因为只有Switch中其上的Route没有匹配,才轮到它。
登录组件
- 在component目录下构建react组件
- 登录页面模板https://codepen.io/colorlib/pen/rxddKy?q=login&limit=all&type=type-pens
<div class="login-page"> <div class="form"> <form class="register-form"> <input type="text" placeholder="name"/> <input type="password" placeholder="password"/> <input type="text" placeholder="email address"/> <button>create</button> <p class="message">Already registered? <a href="#">Sign In</a></p> </form> <form class="login-form"> <input type="text" placeholder="username"/> <input type="password" placeholder="password"/> <button>login</button> <p class="message">Not registered? <a href="#">Create an account</a></p> </form> </div> </div>
- 使用这个HTML模板来构建组件
- 特别注意
- 搬到React组件中的时候,要*将class属性改为className*.
- 所有标签,需要闭合。
login.js
- 在component目录下新建login.js的登录组件。
- 使用上面的模板的HTML中的登录部分,挪到render函数中。
- 修改*class为className*
- 将=<a>=标签替换成=<Link to="?">=组件
- 注意标签闭合问题
/* 新建文件src/component/login.js */ import React from "react"; import {Link} from "react-router-dom"; export default class Login extends React.Component { render() { return ( <div className="login-page"> <div className="form"> <form className="login-form"> <input type="text" placeholder="username"/> <input type="password" placeholder="password"/> <button>登录</button> <p className="message">还未注册? <Link to="/reg">请注册</Link></p> </form> </div> </div> ) } }
在=src/index.js=路由中增加登录组件
/* 修改src/index.js文件内容 */ import React from 'react'; import ReactDom from 'react-dom'; import {Route,Link,BrowserRouter as Router,Switch} from "react-router-dom"; import Login from "./component/login"; function App() { return ( <Router> <div> <Route path="/about" component={About} /> <Route path="/login" component={Login} /> <Route exact path="/" component={Home} /> </div> </Router> ); } function Home() { return ( <div> <h2>Home</h2> </div> ); } function About() { return ( <div> <h2>About</h2> </div> ); } ReactDom.render(<App />, document.getElementById('root'));
样式表
- 在=src/css=中,创建login.css,放入以下内容,然后=src/component/login.js=中导入样式
/* src/component/login.js中导入样式表 */ import React from "react"; import {Link} from "react-router-dom"; import "../css/login.css";
/* 新建src/css/login.css文件 */ body { background: #456; font-family: SimSun; font-size: 14px; } .login-page { width: 360px; padding: 8% 0 0; margin: auto; } .form { font-family: "Microsoft YaHei", SimSun; position: relative; z-index: 1; background: #FFFFFF; max-width: 360px; margin: 0 auto 100px; padding: 45px; text-align: center; box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); } .form input { outline: 0; background: #f2f2f2; width: 100%; border: 0; margin: 0 0 15px; padding: 15px; box-sizing: border-box; font-size: 14px; } .form button { text-transform: uppercase; outline: 0; background: #4CAF50; width: 100%; border: 0; padding: 15px; color: #FFFFFF; font-size: 14px; cursor: pointer; } .form button:hover,.form button:active,.form button:focus { background: #43A047; } .form .message { margin: 15px 0 0; color: #b3b3b3; font-size: 12px; } .form .message a { color: #4CAF50; text-decoration: none; }
注册组件
- 与登录组件编写方式差不多,创建=src/component/reg.js=,使用login.css
/*新建src/component/reg.js文件 */ import React from "react"; import {Link} from "react-router-dom"; import "../css/login.css" export default class Reg extends React.Component { render(){ return( <div className="login-page"> <div className="form"> <form className="register-form"> <input type="text" placeholder="name"/> <input type="text" placeholder="email" /> <input type="password" placeholder="密码"/> <input type="password" placeholder="确认密码"/> <button>注册</button> <p className="message">如果已经注册 <Link to="/login">请登录</Link></p> </form> </div> </div> ) } }
- 在=src/index.js=中增加一条静态路由
/* src/index.js中修改*/ import Reg from "./component/reg"; function App() { return ( <Router> <div> <Route path="/about" component={About} /> <Route path="/login" component={Login} /> <Route path="/reg" component={Reg} /> <Route exact path="/" component={Home} /> </div> </Router> ); }
导航栏链接
- 在index.js中增加导航栏链接,方便页面切换
/* 修改src/index.js文件*/ function App() { return ( <Router> <div> <div> <ul> <li><Link to="/">主页</Link></li> <li><Link to="/login">登录</Link></li> <li><Link to="/reg">注册</Link></li> <li><Link to="/about">关于</Link></li> </ul> </div> <Route path="/about" component={About} /> <Route path="/login" component={Login} /> <Route path="/reg" component={Reg} /> <Route exact path="/" component={Home} /> </div> </Router> ); }
分层
层次 | 作用 | 路径 |
---|---|---|
视图层 | 负责数据呈现,负责用户交互界面 | src/component/xxx.js |
服务层 | 负责业务逻辑处理 | src/service/xxx.js |
Model层 | 数据持久化 |
登录功能实现
- view层,登录组件和用户交互。相当于button点击触发onclick,调用事件响应函数handleClick,handleClick中调用服务service层login函数。
- service层,负责业务逻辑处理。调用Model层数据操作函数
- 新建=src/service/user.js=处理调用逻辑
/* 新建src/service/user.js逻辑 */ export default class UserService { login (email,password) { //Tood } }
- 修改=src/component/login.js=文件
/* 修改`src/component/login.js`文件 */ import React from "react"; import {Link} from "react-router-dom"; import "../css/login.css"; export default class Login extends React.Component { handleClick(event){ console.log(event.target) } render() { return ( <div className="login-page"> <div className="form"> <form className="login-form"> <input type="text" placeholder="email"/> <input type="password" placeholder="password"/> <button onClick={this.handleClick.bind(this)}>登录</button> <p className="message">还未注册? <Link to="#">请注册</Link></p> </form> </div> </div> ) } }
- 问题:
- 页面提交
- 这次发现有一些问题,按钮点击会提交,导致页面刷新了。要阻止页面刷新,其实就是阻止提交。使用event.preventDefault()。
- 如何拿到邮箱和密码?
- =event.target.form=返回暗流所在表单,可以看做一个数组。
- =fm[0].value=和=fm[1].value=就是文本框的值。
- 如何在Login组件中使用UserService实例呢?
- 使用全局变量,虽然可以,但不好。
- 可以在Login的构造器中通过属性注入。
- 也可以在外部使用props注入。使用这种方式。
- 页面提交
- 修改,保证在login组件中使用UserService,使用属性注入
/* 修改src/component/login.js文件 */ import React from "react"; import {Link} from "react-router-dom"; import "../css/login.css"; import UserService from "../service/user"; const serviec = new UserService(); export default class Login extends React.Component{ render(){ return <_Login service={serviec} />; } } class _Login extends React.Component { handleClick(event){ event.preventDefault(); /* 阻止from表单提交 */ let fm = event.target.form; this.props.service.login(fm[0].value,fm[1].value) } render() { return ( <div className="login-page"> <div className="form"> <form className="login-form"> <input type="text" placeholder="email"/> <input type="password" placeholder="password"/> <button onClick={this.handleClick.bind(this)}>登录</button> <p className="message">还未注册? <Link to="#">请注册</Link></p> </form> </div> </div> ) } }
- UserService的login方法实现
代理配置
- 修改webpack.config.dev.jsw文件中proxy部分,保证proxy的target是后台服务的地址和端口,且要开启后台服务。
- 注意:修改这个配置,需要重启dev server
/* 修改webpack.config.dev.jsw文件 */ devServer: { compress: true, /* gzip */ //host:'192.168.61.109', /* 设置ip */ port: 3000, publicPath: '/assets/', /* 设置bundled files浏览器端访问地址 */ hot: true, /* 开启HMR热模块替换 */ inline: true, /* 控制浏览器控制台是否显示信息 */ historyApiFallback: true, stats: { chunks: false }, proxy: { //代理 '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } }
- axios异步库
- axios是一个基于Promise的HTTP异步库,可以用在浏览器或nodejs中。
- 使用axios发起异步调用,完成POST、GET方法的数据提交。可以查照官网的例子。中文说明https://www.kancloud.cn/yunye/axios/234845
- 安装npm
$ npm install axios=或=yarn add axios
- 注意:如果使用yarn安装,就不要再使用npm安装包了,以免出现问题。
导入
import axios from 'axios'
;- 修改=service/user.js=如下
import axios from "axios"; export default class UserService { login (email,password) { console.log(email,password); axios.post("/api/users/login",{ email:email, password:password }) /* dev server会代理 */ .then( /* 成功后返回执行函数 */ function (response){ console.log(response); console.log(response.data) console.log("response.status: " + response.status); console.log(response.statusText); console.log(response.headers); console.log(response.config); } ).catch(/* 出错后执行函数 */ function(error){ console.log(error); } ) } }
- 问题:
- 404 填入邮箱,密码,点击登录,返回404,查看发现访问的地址是=http://127.0.0.1:3000/api/users/login=,%E4%B9%9F%E5%B0%B1%E6%98%AF%E5%A4%9A%E4%BA%86=/api=。
- 解决:
- 修改blog server的代码的路由匹配规则(不建议这么做,影响比较大)
- rewrite,类似httpd,nginx等的rewrite功能。本次测试使用的是dev server,去官方看看。https://webpack.js.org/configuration/dev-server/#devserver-proxy可以看到pathRewrite可以完成路由重写。
修改=webpack.config.dev.js=文件
/* 修改webpack.config.dev.js文件中对应内容*/ devServer: { compress: true, /* gzip */ //host:'192.168.61.109', /* 设置ip */ port: 3000, publicPath: '/assets/', /* 设置bundled files浏览器端访问地址 */ hot: true, /* 开启HMR热模块替换 */ inline: true, /* 控制浏览器控制台是否显示信息 */ historyApiFallback: true, stats: { chunks: false }, proxy: { //代理 '/api': { target: 'http://127.0.0.1:8000', pathRewrite: {"^/api" : ""}, //将所有代理亲戚中已/api开头的请求中对应字符替换成空 changeOrigin: true } } }
- 重启dev server.使用正确的邮箱,密码登录,返回了json数据,在response.data中可以看到token、user。
token持久化–LocalStorage
- 使用LocalStorage来存储token。
- LocalStorage是HTML5标准增加的技术,是浏览器端持久化方案之一。
- LocalStorage是为了存储浏览器得到的数据,例如JSON。
- 数据存储时键值对。数据会存储在不同的域名下面。
- 不同浏览器对单个域名下存储的数据的长度支持不同,有的最多支持2MB。
- 在Charmo浏览器中查看,如下
- SessionStorage和LocalStorage功能差不多,只不过SessionStorage是会话级的,浏览器关闭,会话结束,数据清除。而LocalStorage可以持久保存。
- indexedDB
- 一个域一个datatable
- key-value检索方式
- 建立在关系型的数据模型之上,具有索引表、游标、事务等概念
- store.js
- store.js是一个兼容所有浏览器的LocalStorage包装器,不需要借助Cookie或者Flash。
- store.js会根据浏览器自动选择使用localStorage、globalStorage或者userData来实现本地存储功能。
- *安装*=npm i store=或=yarn add store=
- 测试代码
编写一个test.js,使用node exec插件按F8执行
let store = require("store"); store.set("user","xdd"); console.log(store.get("user")); store.remove("user"); console.log(store.get("user")); // undefined console.log(store.get("user","a")); //a store.set("user",{name:"xdd",age:30}); console.log(store.get("user").name); store.set("school",{name:"magedu"}); // 遍历所有键值对 store.each(function(value,key){ //注意这里key,value是反的 console.log(key,"-->",value) }) //清除所有键值对 store.clearAll() console.log(store.get("user"));// undefined
安装store的同时,也安装了expire过期插件,可以在把kv对存储到LS中的时候顺便加入过期时长。
let store = require("store"); //一定要加载插件,否则key不会过期 store.addPlugin(require("store/plugins/expire")); let d = new Date(); store.set("user","xdd",(new Date()).getTime() + (5 * 1000)); //注意时间单位 setInterval(() => console.log(store.get("user","abc")),1000);
下面是准备写在service中的代码
import store from "store"; import expire from "store/plugins/expire"; store.addPlugin(expire) //存储token store.set("token",res.data.token,(new Date()).getTime() + (8*3600*1000));
/* 修改对应的src/service/user.js文件 */ import axios from "axios"; import store from "store"; import expire from "store/plugins/expire"; // 过期插件 store.addPlugin(expire) export default class UserService { login (email,password) { console.log(email,password); axios.post("/api/users/login",{ email:email, password:password }) /* dev server会代理 */ .then( /* 成功后返回执行函数 */ function (response){ console.log(response.data) console.log("response.status: " + response.status); // 存储token,注意需要重开一次chrome的调试窗口才能看到 store.set("token",response.data.token,(new Date()).getTime() + (8*3600*1000)); } ).catch(/* 出错后执行函数 */ function(error){ console.log(error); } ) } }
Mobx状态管理
- Redux和Mobx
- 社区提供的状态管理库,有Redux和Mobx。
- Redux代码优秀,使用严格的函数式编程思想,学习曲线陡峭,小项目使用的优劣不明显。
- Mobx,非常优秀稳定的库,简单方便,适合中小项目使用。使用面向对象的方式,容易学习和接受。现在在中小项目中使用也非常广泛。Mobx和React也是一对强力组合。
- Mobx官网https://mobx.js.org/
- Mobx中文网https://cn.mobx.js.org/
- Mobx是由Mendix、Coinbase、Facebook开源,它实现了观察者模式。
- 观察者模式
- 观察者模式,也称为*发布订阅模式*。观察者观察某个目标,目标对象(Obserable)状态发生了变化,会通知自己内部注册了的观察者Observer。
- 状态管理
- 需求:
- 一个组件的onClick触发事件响应函数,此函数会调用后台服务。但是后台服务比较耗时,等处理完,需要引起组件的渲染操作。
- 要组件渲染,就需要改变组件的props或state。
- 同步调用
- 同步调用中,实际上就是等着耗时的函数返回
- 异步调用
- 思路一,使用setTimeout问题
- 无法向内部的等待执行函数传入参数,比如Root实例。
- 延时执行的函数的返回值无法取到,所以无法通知Root
思路二、Promise异步执行
- Promise异步执行,如果成功,将调用回调。
- 不管render中是否显示state的值,只要state改变,都会触发render执行
/* 可以在src/index.js中修改测试代码如下 */ import React from 'react'; import ReactDom from 'react-dom'; class Service{ handle(obj){ //Promise new Promise((resolve,reject) => { //定时器5秒后返回ok setTimeout(() => resolve("ok"), 5000); }).then(value => { //成功后执行 //使用obj obj.setState({ret:(Math.random()*1000)}); } ) } } class Root extends React.Component{ state = {ret:null} handleClick(event){ //异步不能直接使用返回值 this.props.service.handle(this); } render(){ console.log("*****************") return ( <div> <button onClick={this.handleClick.bind(this)}>触发handleClick函数</button> <span style={{color:"red"}}> {new Date().getTime()} Service中修改state的值是{this.state.ret}</span> </div> ) } } ReactDom.render(<Root service={new Service()} />, document.getElementById('root'));
- Promise异步执行,如果成功,将调用回调。
- 思路一,使用setTimeout问题
- 需求:
Mobx实现
- observable装饰器:设置被观察者
- observer装饰器:设置观察者,将React组件转换为响应式组件
/* 可以在src/index.js中修改测试代码如下 */ import React from 'react'; import ReactDom from 'react-dom'; import {observable} from 'mobx'; import {observer} from "mobx-react" class Service{ @observable ret = -100; handle(obj){ //Promise new Promise((resolve,reject) => { //定时器5秒后返回ok setTimeout(() => resolve("ok"), 5000); }).then(value => { //成功后执行 this.ret = Math.random()*1000; } ) } } @observer //将react组件转换为响应式组件 class Root extends React.Component{ // state = {ret:null} //不使用state了 handleClick(event){ //异步不能直接使用返回值 this.props.service.handle(this); } render(){ console.log("*****************") return ( <div> <button onClick={this.handleClick.bind(this)}>触发handleClick函数</button> <span style={{color:"red"}}> {new Date().getTime()} Service中修改state的值是{this.props.service.ret /* 如果不使用,当值改变render就不会被调用 */}</span> </div> ) } } ReactDom.render(<Root service={new Service()} />, document.getElementById('root'));
- Service中被观察者ret变化,导致了观察者调用render函数。
- 被观察者变化不引起渲染的情况:
- 将root中的rander中={this.props.service.ret}=注释={* this.props.service.ret *}=。可以看到,如果render中不使用这个被观察者,render函数就不会调用。
- 注意:在观察者render函数中,一定要使用这个被观察对象。
跳转
- 如果service中ret发生了变化,观察者Login就会被通知到。一般来说,就会跳转到用户界面,需要使用Redirect组件。
// 导入Redirect import {Link,Redirect} from 'react-router-dom'; //render函数中return return <Redirect to="/" />; //to表示跳转到哪里
Login登录功能代码实现
=src/service/user.js=文件内容
/*`src/service/user.js`文件内容*/ import axios from "axios"; import store from "store"; import expire from "store/plugins/expire"; import {observable} from "mobx"; // 过期插件 store.addPlugin(expire) export default class UserService { @observable loggedin = false; //被观察者 login (email,password) { console.log(email,password); axios.post("/api/users/login",{ email:email, password:password }) /* dev server会代理 */ .then( /* 成功后返回执行函数 */ (function (response){ console.log(response.data) console.log("response.status: " + response.status); // 存储token,注意需要重开一次chrome的调试窗口才能看到 store.set("token",response.data.token,(new Date()).getTime() + (8*3600*1000)); this.loggedin = true; // 修改被观察者 }).bind(this) /*注意绑定this,如果不想可以使用箭头函数*/ ).catch(/* 出错后执行函数 */ function(error){ console.log(error); } ) } }
=src/component/login.js=文件内容
/* src/component/login.js文件内容 */ import React from "react"; import {Link,Redirect} from "react-router-dom"; import "../css/login.css"; import UserService from "../service/user"; import {observer} from "mobx-react"; const userService = new UserService(); export default class Login extends React.Component{ render(){ return <_Login service={userService} />; } } @observer //将react组件转换为响应式组件 class _Login extends React.Component { handleClick(event){ event.preventDefault(); /* 阻止from表单提交 */ let fm = event.target.form; this.props.service.login(fm[0].value,fm[1].value) } render() { console.log(this.props.service.loggedin) if (this.props.service.loggedin) { return <Redirect to="/" />; //已经登录,直接跳转 } return ( <div className="login-page"> <div className="form"> <form className="login-form"> <input type="text" placeholder="email"/> <input type="password" placeholder="password"/> <button onClick={this.handleClick.bind(this)}>登录</button> <p className="message">还未注册? <Link to="#">请注册</Link></p> </form> </div> </div> ) } }
- 注意:测试时,开启Django编写的后台服务程序
- 测试成功,成功登录,写入Localstorage,也实现了跳转
注册功能代码实现
[toc]
注册功能实现
在service/user.js中增加reg注册函数
import axios from "axios"; import store from "store"; import expire from "store/plugins/expire"; import {observable} from "mobx"; // 过期插件 store.addPlugin(expire) export default class UserService { @observable loggedin = false; //被观察者 // 登录函数 login (email,password) { console.log(email,password); axios.post("/api/users/login",{ email:email, password:password }) /* dev server会代理 */ .then( /* 成功后返回执行函数 */ (function (response){ console.log(response.data) console.log("response.status: " + response.status); // 存储token,注意需要重开一次chrome的调试窗口才能看到 store.set("token",response.data.token,(new Date()).getTime() + (8*3600*1000)); this.loggedin = true; // 修改被观察者 }).bind(this) ).catch(/* 出错后执行函数 */ function(error){ console.log(error); } ) } // 注册函数 reg(name,email,password){ console.log(name,email,password) axios.post('/api/users/',{ email:email, password:password, name:name })/* dev server会代理 */ .then( response => {//此函数要注意this的问题 console.log(response.data); console.log(response.status); // * 存储token,注意需要重开一次chrome的调试窗口才能看到 store.set("token",response.data.token,(new Date()).getTime()+(8*3600*1000)); this.loggedin=true; //修改被观察者 } ).catch( error => { console.log(error); } ) } }
修改Reg.js组件
import React from "react"; import {Link, Redirect} from "react-router-dom"; import "../css/login.css" import UserServer from '../service/user'; import { observer } from 'mobx-react'; const userserver = new UserServer; export default class Reg extends React.Component { render() { return <_Reg service={userserver} /> } } @observer class _Reg extends React.Component { validate(password,confirmpwd){ //表单验证函数 return password.value === confirmpwd.value } handleClick(event){ event.preventDefault(); const [name,email,password,confirmpwd] = event.target.form; console.log(this.validate(password,confirmpwd));// +要验证表单数据后才发往后台 this.props.service.reg(name.value,email.value,password.value); } render(){ if (this.props.service.loggedin){ //已经登录的用户不允许注册 return <Redirect to="/" />; } return ( <div className="login-page"> <div className="form"> <form className="register-form"> <input type="text" placeholder="name"/> <input type="text" placeholder="email" /> <input type="password" placeholder="密码"/> <input type="password" placeholder="确认密码"/> <button onClick={this.handleClick.bind(this)}>注册</button> <p className="message">如果已经注册 <Link to="/login">请登录</Link></p> </form> </div> </div> ) } }
- 测试发现,重新登录,登录成功后,点击注册链接,没有跳转到首页,或者先注册,注册成功后,点击登录,还可以再次登录。原因是构造并使用了不同的UserServer实例。
- 如下,使用同一个UserServer实例(在service/user.js中导出唯一的UserService实例即可),其他模块直接导入并使用这个实例即可。
修改service/user.js
class UserService { //此处省略 } const userService = new UserService(); export {userService}; // 注意,还需要修改对应的login.js以及对应的reg.js文件中同时去掉实例化语句。导入语句修改为: // import {userService} from '../service/user';
Ant Design
- Ant Design蚂蚁金服开源的React UI库。
- 官网 https://ant.design/index-cn
- 官方文档 https://ant.design/docs/react/introduce-cn
- 安装
npm install antd
或者=yarn add antd= 使用
import {List} from 'antd'; //加载antd的组件 import 'antd/lib/list/style/css'; //加载组件的样式css ReactDom.render(<list />, mountNode);
每一种组件的详细使用例子,官网都有,需要时查阅官方文档即可
信息显示
- 网页开发中,不管操作成功与否,有很多提示信息,目前信息都是控制台输出,用户看不到。
- 使用Antd的message组件显示油耗提示信息。
在service/user.js中增加一个被观察对象
import axios from "axios"; import store from "store"; import expire from "store/plugins/expire"; import {observable} from "mobx"; // 过期插件 store.addPlugin(expire) class UserService { @observable loggedin = false; //被观察者 @observable errMsg = ''; //+ 被观察者 login (email,password) { console.log(email,password); axios.post("/api/users/login",{ email:email, password:password }) /* dev server会代理 */ .then( /* 成功后返回执行函数 */ (function (response){ console.log(response.data) console.log("response.status: " + response.status); // 存储token,注意需要重开一次chrome的调试窗口才能看到 store.set("token",response.data.token,(new Date()).getTime() + (8*3600*1000)); this.loggedin = true; // 修改被观察者 }).bind(this) ).catch(/* 出错后执行函数 */ function(error){ console.log(error); this.errMsg = '登录失败'; }.bind(this) ) } reg(name,email,password){ console.log(name,email,password) axios.post('/api/users/',{ email:email, password:password, name:name })/* dev server会代理 */ .then( response => {//此函数要注意this的问题 console.log(response.data); console.log(response.status); // * 存储token,注意需要重开一次chrome的调试窗口才能看到 store.set("token",response.data.token,(new Date()).getTime()+(8*3600*1000)); this.loggedin=true; //修改被观察者 } ).catch( error => { console.log(error); this.errMsg = "注册失败" //+信息显示 } ) } } const userService = new UserService(); export {userService};
在component/login.js文件中的 _Login组件,增加Antd的message组件
import React from "react"; import {Link,Redirect} from "react-router-dom"; import "../css/login.css"; import {userService} from "../service/user"; import {observer} from "mobx-react"; import {message} from 'antd'; import 'antd/lib/message/style'; //使用消息框样式表 // const userService = new userService; export default class Login extends React.Component{ render(){ return <_Login service={userService} />; } } @observer //将react组件转换为响应式组件 class _Login extends React.Component { handleClick(event){ event.preventDefault(); /* 阻止from表单提交 */ let fm = event.target.form; this.props.service.login(fm[0].value,fm[1].value) } render() { if (this.props.service.loggedin) { return <Redirect to="/" />; //已经登录,直接跳转 } let em = this.props.service.errMsg; //(必须使用)才能触发 return ( <div className="login-page"> <div className="form"> <form className="login-form"> <input type="text" placeholder="email"/> <input type="password" placeholder="password"/> <button onClick={this.handleClick.bind(this)}>登录</button> <p className="message">还未注册? <Link to="/reg">请注册</Link></p> </form> </div> </div> ) } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.errMsg){ //登录失败,则弹出消息框 message.info(prevProps.service.errMsg,3,setTimeout(() => prevProps.service.errMsg = '',1000)) } } }
component/reg.js中_Reg组件同样增加message组件
import React from "react"; import {Link, Redirect} from "react-router-dom"; import "../css/login.css" import {userService} from '../service/user'; import { observer } from 'mobx-react'; import {message} from 'antd'; import 'antd/lib/message/style'; // const userserver = new UserServer; export default class Reg extends React.Component { render() { return <_Reg service={userService} /> } } @observer class _Reg extends React.Component { validate(password,confirmpwd){ //表单验证函数 return password.value === confirmpwd.value } handleClick(event){ event.preventDefault(); const [name,email,password,confirmpwd] = event.target.form; console.log(this.validate(password,confirmpwd));// +要验证表单数据后才发往后台 this.props.service.reg(name.value,email.value,password.value); } render(){ if (this.props.service.loggedin){ //已经登录的用户不允许注册 return <Redirect to="/" />; } let em = this.props.service.errMsg; return ( <div className="login-page"> <div className="form"> <form className="register-form"> <input type="text" placeholder="name"/> <input type="text" placeholder="email" /> <input type="password" placeholder="密码"/> <input type="password" placeholder="确认密码"/> <button onClick={this.handleClick.bind(this)}>注册</button> <p className="message">如果已经注册 <Link to="/login">请登录</Link></p> </form> </div> </div> ) } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.errMsg){ //注册失败,则弹出消息框 message.info(prevProps.service.errMsg,3,setTimeout(() => prevProps.service.errMsg = '',1000)) } } }
进阶装饰器
新建src/utils.js放入以下内容
import React from 'react'; const inject = obj => Comp => props => <Comp {...obj} {...props} /> export {inject}
- 将登陆、注册组件装饰一下
reg.js修改如下:
import React from "react"; import {Link, Redirect} from "react-router-dom"; import "../css/login.css" import {userService as service} from '../service/user'; import { observer } from 'mobx-react'; import {message} from 'antd'; import 'antd/lib/message/style'; import {inject} from '../utils'; // export default class Reg extends React.Component { // render() { // return <_Reg service={userService} /> // } // } // 注意装饰器顺序 @inject({service}) //+ 装饰器注入属性 @observer export default class _Reg extends React.Component { validate(password,confirmpwd){ //表单验证函数 return password.value === confirmpwd.value } handleClick(event){ event.preventDefault(); const [name,email,password,confirmpwd] = event.target.form; console.log(this.validate(password,confirmpwd));// +要验证表单数据后才发往后台 this.props.service.reg(name.value,email.value,password.value); } render(){ if (this.props.service.loggedin){ //已经登录的用户不允许注册 return <Redirect to="/" />; } let em = this.props.service.errMsg; return ( <div className="login-page"> <div className="form"> <form className="register-form"> <input type="text" placeholder="name"/> <input type="text" placeholder="email" /> <input type="password" placeholder="密码"/> <input type="password" placeholder="确认密码"/> <button onClick={this.handleClick.bind(this)}>注册</button> <p className="message">如果已经注册 <Link to="/login">请登录</Link></p> </form> </div> </div> ) } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.errMsg){ //注册失败,则弹出消息框 message.info(prevProps.service.errMsg,3,setTimeout(() => prevProps.service.errMsg = '',1000)) } } }
login.js修改如下
import React from "react"; import {Link,Redirect} from "react-router-dom"; import "../css/login.css"; import {userService as service} from "../service/user"; import {observer} from "mobx-react"; import {message} from 'antd'; import 'antd/lib/message/style'; //使用消息框样式表 import {inject} from '../utils'; // export default class Login extends React.Component{ // render(){ // return <_Login service={service} />; // } // } // 注意装饰器顺序 @inject({service}) //+ 装饰器注入属性 @observer //将react组件转换为响应式组件 export default class _Login extends React.Component { handleClick(event){ event.preventDefault(); /* 阻止from表单提交 */ let fm = event.target.form; this.props.service.login(fm[0].value,fm[1].value) } render() { if (this.props.service.loggedin) { return <Redirect to="/" />; //已经登录,直接跳转 } let em = this.props.service.errMsg; //(必须使用)才能触发 return ( <div className="login-page"> <div className="form"> <form className="login-form"> <input type="text" placeholder="email"/> <input type="password" placeholder="password"/> <button onClick={this.handleClick.bind(this)}>登录</button> <p className="message">还未注册? <Link to="/reg">请注册</Link></p> </form> </div> </div> ) } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.errMsg){ //登录失败,则弹出消息框 message.info(prevProps.service.errMsg,3,setTimeout(() => prevProps.service.errMsg = '',1000)) } } }
博文业务代码实现和Antd组件
[toc]
导航菜单
- 菜单网址,https://ant.design/components/menu-cn/
- Menu 菜单组件
- mode有水平、垂直、内嵌
- Menu.Item 菜单项
- key菜单项item的唯一标识
- 修改src/index.js导航菜单
import React from 'react'; import ReactDom from 'react-dom'; import {Route,Link,BrowserRouter as Router} from "react-router-dom"; import Login from "./component/login"; import Reg from "./component/reg"; import {Menu} from 'antd'; //加载antd的组件 import 'antd/lib/menu/style'; //加载组件的样式css import { HomeFilled,LoginOutlined,RotateRightOutlined,AccountBookFilled } from '@ant-design/icons'; const App = () => ( <Router> <div> <div> <Menu mode="horizontal" theme="dark" style={{ lineHeight: '64px' }} defaultSelectedKeys= {['home']} > <Menu.Item key="home" icon={<HomeFilled />} ><Link to="/">主页</Link></Menu.Item> <Menu.Item key="login" icon={<LoginOutlined />}><Link to="/login">登录</Link></Menu.Item> <Menu.Item key="reg" icon={<RotateRightOutlined />}><Link to="/reg">注册</Link></Menu.Item> <Menu.Item key="about" icon={<AccountBookFilled />}><Link to="/about">关于</Link></Menu.Item> </Menu> </div> <div> <Route path="/about" component={About} /> <Route path="/login" component={Login} /> <Route path="/reg" component={Reg} /> <Route exact path="/" component={Home} /> </div> </div> </Router> ); function Home() { return ( <div> <h2>Home</h2> </div> ); } function About() { return ( <div> <h2>About</h2> </div> ); } ReactDom.render(<App />, document.getElementById('root'));
页面布局
- 采用上中下布局,参考:https://ant.design/components/layout-cn/
- 修改src/index.js导航菜单
import React from 'react'; import ReactDom from 'react-dom'; import {Route,Link,BrowserRouter as Router} from "react-router-dom"; import {Menu,Layout} from 'antd'; //加载antd的组件 import Login from "./component/login"; import Reg from "./component/reg"; import 'antd/lib/menu/style'; //加载组件的样式css import 'antd/lib/layout/style'; import { HomeFilled,LoginOutlined,RotateRightOutlined,AccountBookFilled } from '@ant-design/icons'; const {Header,Content,Footer} = Layout; // 上中下 const App = () => ( <Router> <Layout> <Header> <Menu mode="horizontal" theme="dark" style={{ lineHeight: '64px' }} defaultSelectedKeys= {['home']} > <Menu.Item key="home" icon={<HomeFilled />} ><Link to="/">主页</Link></Menu.Item> <Menu.Item key="login" icon={<LoginOutlined />}><Link to="/login">登录</Link></Menu.Item> <Menu.Item key="reg" icon={<RotateRightOutlined />}><Link to="/reg">注册</Link></Menu.Item> <Menu.Item key="about" icon={<AccountBookFilled />}><Link to="/about">关于</Link></Menu.Item> </Menu> </Header> <Content style={{ padding: '8px 50px' }}> <div style={{ background: '#fff', padding: 24, minHeight: 280 }}> <Route path="/about" component={About} /> <Route path="/login" component={Login} /> <Route path="/reg" component={Reg} /> <Route exact path="/" component={Home} /> </div> </Content> <Footer style={{ textAlign: 'center' }}>xddweb©2019-2020 </Footer> </Layout> </Router> ); function Home() { return ( <div> <h2>Home</h2> </div> ); } function About() { return ( <div> <h2>About</h2> </div> ); } ReactDom.render(<App />, document.getElementById('root'));
博文业务
url | method | 说明 |
---|---|---|
posts | POST | 提交博文的title、content,成功返回json,包含post_id,title |
/posts/id | GET | 返回博文详情返回json,id,title、author_id、postdate(时间戳)、content |
posts | GET | 返回博文标题列表,分页 |
业务层
- 创建service/post.js文件,新建PostService类。
import axios from 'axios'; import {observable} from 'mobx'; class PostService{ @observable msg = ''; pub(title,content){ console.log(title); axios.post('/api/posts/',{ title,content })/* dev server 会代理 */ .then( response => { console.log(response.data); console.log(response.status); this.msg = '博文提交成功'; } ).catch( error => { console.log(error); this.msg = '博文提交失败'; //+信息显示 } ) } } const postService = new PostService(); export {postService};
发布组件
- 使用Form组件,https://ant.design/components/form-cn/
- 创建component/pub.js文件
// component/pub.js import React from 'react'; import {observer} from 'mobx-react'; import {Form,Icon,Input,Button,message} from 'antd'; import {inject} from '../utils'; import {postService as service} from '../service/post'; import 'antd/lib/form/style'; import 'antd/lib/icon/style'; import 'antd/lib/input/style'; import 'antd/lib/button/style'; import 'antd/lib/message/style'; const FormItem = Form.Item; const { TextArea } = Input; @inject({service}) @observer export default class Pub extends React.Component { handleSubmit(event){ event.preventDefault(); const [ title,content ] = event.target;// event.target返回form,而form是表单控件的数组 this.props.service.pub(title.value,content.value); } render(){ let msg = this.props.service.msg; return ( <Form layout="vertical" onSubmitCapture={this.handleSubmit.bind(this)}> <Form.Item label="标题" labelCol={{span:4}} wrapperCol={{span:14}} > <Input placeholder="标题" /> </Form.Item> <Form.Item label="内容" labelCol={{span:4}} wrapperCol={{span:14}} > <TextArea rows={10} /> </Form.Item> <FormItem wrapperCol={{span:14,offset:4}}> <Button type="primary" htmlType="submit" >提交</Button> </FormItem> </Form> ) } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.msg){ message.info(prevProps.service.msg,3,setTimeout(() => prevProps.service.msg = '',1000)) } } }
- Form 表单组件,layout是垂直,onSubmitCapture提交,注意这个提交的this就是表单自己
- FormItem表单项
- label 设置控件前的标题
- labelCol设置label的宽度,wrapperCol是label后占用的宽度,这些单位都是栅格系统的宽度
- 参考Antd/Form表单/表单布局https://ant.design/components/form-cn/#components-form-demo-layout
- 栅格系统:AntD提供了一个类似于Bootstrap的栅格系统,她将页面分成了24等分的列
- span代表占几个格子:offset表示左边空出多个格子
- Input输入框,placeholder提示字符
- TextArea文本框,rows行数
- Button按钮
- type是按钮的类型,也决定它的颜色
- htmlType使用HTML中的Type值,submit是提交按钮会触发提交行为,一定要写,但是handleSubmit中要阻止默认行为。
富文本编辑器
使用yarn安装最新版本 $ yarn add braft-editor 安装指定版本 $ yarn add [email protected] 安装2.x.x最高版本 $ yarn add braft-editor@^2.3.1 使用npm安装 $ npm install braft-editor --save 组件使用 import BraftEditor from 'braft-editor'; import 'braft-editor/dist/index.css';
- 参考https://braft.margox.cn/demos/basic
- 修改component/pub.js文件
import React from 'react'; import {observer} from 'mobx-react'; import {Form,Icon,Input,Button,message} from 'antd'; import {inject} from '../utils'; import {postService as service} from '../service/post'; import BraftEditor from 'braft-editor'; import 'braft-editor/dist/index.css' import 'antd/lib/form/style'; import 'antd/lib/icon/style'; import 'antd/lib/input/style'; import 'antd/lib/button/style'; import 'antd/lib/message/style'; const FormItem = Form.Item; const { TextArea } = Input; @inject({service}) @observer export default class Pub extends React.Component { //创建一个state,关联组件和组件输出的HTML内容 state = { editorState : BraftEditor.createEditorState('<p><a href="">xdd网页</a></p>'), outputHTML : "<p></p>" } handleSubmit(event){ event.preventDefault(); const [ title,content ] = event.target;// event.target返回form,而form是表单控件的数组 console.log(title,content); const {outputHTML = ''} = this.state; console.log(outputHTML); // this.props.service.pub(title.value,content.value); this.props.service.pub(title.value,outputHTML); } // 组件内容变化更新state,引发渲染 handleChange = (editorState) =>{ this.setState({ editorState:editorState, outputHTML:editorState.toHTML() }); } render(){ let msg = this.props.service.msg; // 排除按钮 const excludeControls = [ 'letter-spacing', 'line-height', 'clear', 'headings', 'list-ol', 'list-ul', 'remove-styles', 'subscript', 'hr', 'text-align' ]; const {editorState,outputHTML} = this.state; return ( <Form layout="vertical" onSubmitCapture={this.handleSubmit.bind(this)}> <Form.Item label="标题" labelCol={{span:4}} wrapperCol={{span:14}} > <Input placeholder="标题" /> </Form.Item> <Form.Item label="内容" labelCol={{span:4}} wrapperCol={{span:14}} > <BraftEditor value={editorState} excludeControls={excludeControls} onChange = {this.handleChange.bind(this)} contentStyle={{height:400}} /> </Form.Item> <FormItem wrapperCol={{span:14,offset:4}}> <Button type="primary" htmlType="submit" >提交</Button> </FormItem> </Form> ) } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.msg){ message.info(prevProps.service.msg,3,setTimeout(() => prevProps.service.msg = '',1000)) } } }
业务层改进
- header中的jwt,由于与后台Django Server通信,身份认证需要jwt,这个要放到request header中。使用axios的API =axios.post(url[data,config])=可以使用第三个参数config。config是一个对象,字典中设置headers字段,该字段的值依然是对象,都是键值对形式。
- 修改service/post.js文件
// service/post.js import axios from 'axios'; import {observable} from 'mobx'; import store from 'store'; import expire from 'store/plugins/expire'; store.addPlugin(expire) class PostService{ @observable msg = ''; constructor(){ this.axios = axios.create({ baseURL: '/api/posts' }) } getJwt(){ return store.get("token",'') } pub(title,content){ console.log(title); axios.post('/api/posts/',{ title,content },{ headers:{'jwt': this.getJwt()} })/* dev server 会代理 */ .then( response => { console.log(response.data); console.log(response.status); this.msg = '博文提交成功'; } ).catch( error => { console.log(error); this.msg = '博文提交失败'; //+信息显示 } ) } } const postService = new PostService(); export {postService};
详情页组件
- =index.jsp=中增加如下代码
import Post from "./component/post"; //详情页 <Route path="/post/:id" component={Post} />
- 新建component/post.js,创建Post组件。使用antd Card布局。
- 安装日期处理库,=yarn add moment=。
// component/post.js import React from 'react'; import { observer } from 'mobx-react'; import {message,Card,Empty} from 'antd'; import {inject} from '../utils'; import {postService as service} from '../service/post'; import moment from 'moment'; import 'antd/lib/message/style'; import 'antd/lib/card/style'; import 'antd/lib/empty/style'; @inject({service}) @observer export default class Post extends React.Component { constructor(props){ super(props); let {id = -1} = props.match.params; console.log(id); this.props.service.getPost(id); } render(){ let msg = this.props.service.msg; console.log(this.props.service.post); const {title = "", content = "",author,postdate} = this.props.service.post; if (title){ return ( <Card title={title} extra={<a href="#">{author}</a>} > <p>{moment(postdate * 1000).format("YYYY-MM-DD hh:mm:ss")}</p> <p dangerouslySetInnerHTML={{__html:content}}></p> </Card> ); } else { return <Empty />; } } componentDidUpdate(prevProps,prevState){ //+ 渲染后显示消息组件 if (prevProps.service.msg){ message.info(prevProps.service.msg,3,()=> prevProps.service.msg = ''); } } }
- 如果使用了富文本编辑器,那么显示的时候,发现不能按照网页标签显示。原因是为了安全,防止xss攻击,React不允许直接按照HTML显示。
使用dangerouslySetInnerHTML属性,这个名字提醒使用者很危险。
<p>{content}</p> 修改为 <p dangerouslySetInnerHTML={{__html:content}}> </p>
- 修改service/post.js代码如下
// service/post.js import axios from 'axios'; import {observable} from 'mobx'; import store from 'store'; import expire from 'store/plugins/expire'; store.addPlugin(expire) class PostService{ @observable msg = ''; @observable post = {}; //文章 constructor(){ this.axios = axios.create({ baseURL: '/api/posts/' }) } getJwt(){ return store.get("token",'') } pub(title,content){ console.log(title); axios.post('/api/posts/',{ title,content },{ headers:{'jwt': this.getJwt()} })/* dev server 会代理 */ .then( response => { console.log(response.data); console.log(response.status); this.msg = '博文提交成功'; } ).catch( error => { console.log(error); this.msg = '博文提交失败'; //+信息显示 } ) } getPost(id){ this.axios.get(id).then( response => { //此函数要注意this的问题 this.post = response.data.post; } ).catch( error => { console.log(error); this.post = {} this.msg = "文章加载失败"; //信息显示 } ) } } const postService = new PostService(); export {postService};
文章列表页组件
- 创建component/list.js,创建List组件。在index.js中提交菜单项和路由。
- 使用L是为了避免和AntD的List冲突。
- 修改service/index.js文件
// service/index.js import React from 'react'; import ReactDom from 'react-dom'; import {Route,Link,BrowserRouter as Router} from "react-router-dom"; import {Menu,Layout} from 'antd'; //加载antd的组件 import Login from "./component/login"; import Reg from "./component/reg"; import Pub from "./component/pub"; // 发布页 import Post from "./component/post"; //详情页 import L from './component/list'; //列表页 import 'antd/lib/menu/style'; //加载组件的样式css import 'antd/lib/layout/style'; import { HomeFilled,LoginOutlined,RotateRightOutlined,AccountBookFilled,BarcodeOutlined,PullRequestOutlined} from '@ant-design/icons'; const {Header,Content,Footer} = Layout; // 上中下 const App = () => ( <Router> <Layout> <Header> <Menu mode="horizontal" theme="dark" style={{ lineHeight: '64px' }} defaultSelectedKeys= {['home']} > <Menu.Item key="home" icon={<HomeFilled />} ><Link to="/">主页</Link></Menu.Item> <Menu.Item key="login" icon={<LoginOutlined />}><Link to="/login">登录</Link></Menu.Item> <Menu.Item key="reg" icon={<RotateRightOutlined />}><Link to="/reg">注册</Link></Menu.Item> <Menu.Item key="pub" icon={<PullRequestOutlined />}><Link to="/pub">发布</Link></Menu.Item> <Menu.Item key="bars" icon={<BarcodeOutlined />}><Link to="/list">文章列表</Link></Menu.Item> <Menu.Item key="about" icon={<AccountBookFilled />}><Link to="/about">关于</Link></Menu.Item> </Menu> </Header> <Content style={{ padding: '8px 50px' }}> <div style={{ background: '#fff', padding: 24, minHeight: 280 }}> <Route path="/about" component={About} /> <Route path="/login" component={Login} /> <Route path="/reg" component={Reg} /> <Route path="/pub" component={Pub} /> <Route path="/list" component={L} /> <Route path="/posts/:id" component={Post} /> <Route exact path="/" component={Home} /> </div> </Content> <Footer style={{ textAlign: 'center' }}>xddweb©2019-2020 </Footer> </Layout> </Router> ); function Home() { return ( <div> <h2>Home</h2> </div> ); } function About() { return ( <div> <h2>About</h2> </div> ); } ReactDom.render(<App />, document.getElementById('root'));
List组件
- Ant
design的List,需要使用3.x版本,修改package.json的版本信息="antd":"^3.1.5"
。然后=$ npm update
,更新成功后,就可以使用List组件了。或者=$yarn upgrade antd@^3.1.5=. - 新增component/list.js代码如下
// component/list.js import React from 'react'; import {observer} from 'mobx-react'; import {message,List,Empty} from 'antd'; import {inject} from '../utils'; import {postService as service} from '../service/post'; import {Link} from 'react-router-dom'; import 'antd/lib/message/style'; import 'antd/lib/list/style'; @inject({service}) @observer export default class L extends React.Component{ constructor(props) { super(props); props.service.list(); } render() { const {posts:data=[],pagination} = this.props.service.posts; if (data.length){ return ( <List header={<div>博文列表</div>} footer={<div>Footer</div>} bordered dataSource = {data} renderItem={item => ( <List.Item> <Link to={'/posts/'+item.id}>{item.title}</Link> </List.Item> )} /> ) } return <Empty />; } }
- List列表组件
- bordered有边线
- dataSource给定数据源
- renderItem渲染每一行,给定一个一参函数迭代每一行
- List.Item每一行的组件
- 使用Link组件增加链接
<List bordered dataSource = {data} renderItem={item => ( <List.Item> <Link to={'/posts/'+item.id}>{item.title}</Link> </List.Item> )} />
- 如果需要根据复杂的效果可以使用List.Item.Meta。
<Link to={'/post/' + item.id}>{item.title} </Link> 这是详情页的链接
- 修改service/post.js文件中的postService类,如下:
import axios from 'axios'; import {observable} from 'mobx'; import store from 'store'; import expire from 'store/plugins/expire'; store.addPlugin(expire) class PostService{ @observable msg = ''; @observable post = {}; //文章 @observable posts = {'posts':[],'pagination':{page:1,size:20,pages:0,total:0}}; //列表 constructor(){ this.axios = axios.create({ baseURL: '/api/posts/' }) } getJwt(){ return store.get("token",'') } pub(title,content){ console.log(title); axios.post('/api/posts/',{ title,content },{ headers:{'jwt': this.getJwt()} })/* dev server 会代理 */ .then( response => { console.log(response.data); console.log(response.status); this.msg = '博文提交成功'; } ).catch( error => { console.log(error); this.msg = '博文提交失败'; //+信息显示 } ) } getPost(id){ this.axios.get(id).then( response => { //此函数要注意this的问题 this.post = response.data.post; } ).catch( error => { console.log(error); this.post = {} this.msg = "文章加载失败"; //信息显示 } ) } list(){ this.axios.get().then( response => { //注意此函数的this问题 this.posts = response.data; } ).catch( error =>{ console.log(error); this.post = {} this.msg = '文章列表加载失败';// 信息显示 } ) } } const postService = new PostService(); export {postService};
分页功能
- 分页使用了Pagination组件,在L组件的render函数的List组件中使用pagination属性,这个属性放入一个pagination对象,有如下属性
- current,当前页
- pageSize, 页面内行数
- total,记录总数
- onChange,页码切换时调用,回调函数为=(pageNo,pageSize)=>{}=,即切换是获得当前页码和页内行数。
- 可参考https://ant.design/components/list-cn/#components-list-demo-vertical
- 修改component/list.js代码如下:
// component/list.js import React from 'react'; import {observer} from 'mobx-react'; import {message,List,Empty} from 'antd'; import {inject} from '../utils'; import {postService as service} from '../service/post'; import {Link} from 'react-router-dom'; import 'antd/lib/message/style'; import 'antd/lib/list/style'; @inject({service}) @observer export default class L extends React.Component{ constructor(props) { super(props); props.service.list(); } onChange(pageNumber){ console.log(pageNumber); this.props.service.list(pageNumber); } render() { const {posts:data=[],pagination} = this.props.service.posts; if (data.length){ const {page:current=1,total ,size:pageSize} = pagination; console.log(current,total,pageSize); return ( <List header={<div>博文列表</div>} bordered dataSource = {data} renderItem={item => ( <List.Item> <Link to={'/posts/'+item.id}>{item.title}</Link> </List.Item> )} pagination={{ current, total, pageSize, onChange:this.onChange.bind(this) }} /> ) } return <Empty />; } }
- 修改service/post.js中的list方法
import axios from 'axios'; import {observable} from 'mobx'; import store from 'store'; import expire from 'store/plugins/expire'; store.addPlugin(expire) class PostService{ @observable msg = ''; @observable post = {}; //文章 @observable posts = {'posts':[],'pagination':{page:1,size:5,pages:0,total:0}}; //列表 constructor(){ this.axios = axios.create({ baseURL: '/api/posts/' }) } getJwt(){ return store.get("token",'') } pub(title,content){ console.log(title); axios.post('/api/posts/',{ title,content },{ headers:{'jwt': this.getJwt()} })/* dev server 会代理 */ .then( response => { console.log(response.data); console.log(response.status); this.msg = '博文提交成功'; } ).catch( error => { console.log(error); this.msg = '博文提交失败'; //+信息显示 } ) } getPost(id){ this.axios.get(id).then( response => { //此函数要注意this的问题 this.post = response.data.post; } ).catch( error => { console.log(error); this.post = {} this.msg = "文章加载失败"; //信息显示 } ) } list(page=1,size=2){ this.axios.get(`?page=${page}&size=${size}`).then( response => { //注意此函数的this问题 this.posts = response.data; } ).catch( error =>{ console.log(error); this.post = {} this.msg = '文章列表加载失败';// 信息显示 } ) } } const postService = new PostService(); export {postService};
国际化
- 上面分页中,当鼠标放在左右两端时发现上一页和下一页是英文。修改方法如下
- index.js修改如下(部分代码)
import {LocaleProvider} from 'antd'; import moment from 'moment'; import zh_CN from 'antd/lib/locale-provider/zh_CN' import 'moment/locale/zh-cn' moment.locale('zh-cn') ReactDom.render(<LocaleProvider locale={zh_CN}><App /></LocaleProvider>, document.getElementById('root'));
项目部署
[toc]
Django 打包
- 生成项目依赖插件版本信息
## 应用程序的根目录下生成 $ pip freeze >requirements
- 构建setup.py文件(在应用程序根目录下面)
from distutils.core import setup import glob setup(name='blog', version='1.0', description='www.mumuxi.online', author='mumuxi', author_email="[email protected]", url='https://www.mumuxi.online', packages=["MicroBlog",'post','user','utils','djweb'], py_modules=["manage"], data_files=glob.glob("templates/*.html") + ["requirements"] )
## 应用程序的跟目录下打包 $ python setup.py sdist --formats=gztar #gz
- 在Linux系统中创建一个python虚拟环境目录,使用pyenv
- 安装pyenv
# yum install git python-devel mysql-devel # yum -y install gcc make patch gdbm-devel openssl-devel sqlite-devel readline-devel zlib-devel bzip2-devel python依赖,mysqlclient依赖 # yum install python-devel mysql-devel # useradd python # echo python | passwd python --stdin # su - python 进入python用户执行如下操作 $ curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash $ vim ~/.bashrc export PATH="/home/root/.pyenv/bin:$PATH" eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" $ pyenv install 3.6.6 -vv 准备pip配置文件 $ mkdir ~/.pip $ vim ~/.pip/pip.conf [global] index-url = https://mirrors.aliyun.com/pypi/simple/ [install] trusted-host=mirrors.aliyun.com 安装虚拟环境 $ pyenv virtualenv 3.6.6 blog366 $ mkdir -p projects $ cd projects $ tar xf blog-1.0.tar.gz $ ln -sv blog-1.0 web "web" -> "blog-1.0" $ cd web $ pwd /home/python/projects/web $ pyenv local blog366 $ pip list $ pip install -r requirements #安装依赖包 $ sed -i -e 's/DEBUG.*/DEBUG = False/' -e 's/ALLOWED_HOSTS.*/ALLOWED_HOSTS = ["*"]/' djweb/setting.py # 修改Django配置 $ python manage.py runserver 0.0.0.0:9112 # 测试
- 使用=http://www.mumuxi.online:9112/posts/?page=2&size=2= 成功返回数据,说明Django应用成功。
- 至此,Django应用部署完成。Django带了个开发用Web Server. 生成环境不用,需要借助其他Server。
- 注意:ALLOWED_HOSTS=[“*”] 这是所有都可以访问,生产环境应指定具体可以访问的IP,而不是所有。
WSGI
- Web Server Gateway Interface,是Python中定义的WSGI Server与应用程序的接口定义。
- 应用程序符合WSGI规范的Django框架负责,
uWSGI
- uWSGI是一个c语言的项目,提供一个WEB服务器,它支持WSGI协议,可以和Python的WGSI应用程序通信。
- 官方文档https://uwsgi-docs.readthedocs.io/en/latest/
- uWSGI可以直接启动HTTP服务,接收HTTP请求,并调用Django应用。
- 安装
$ pip install uwsgi $ uwsgi --help
uWSGI+Django部署
- 在Django项目根目录下,运行=$ uwsgi –http :8000 –wsgi-file djweb/wsgi.py –stats :8001 –stats-http=,使用下面链接测试
- =stats=能够显示服务器状态值。 =–stats-http=选项可以使用http访问这个值。
- 安装uwsgitop获取这个stat值。注意使用这个命令不要使用=–stats-http=选项。
$ pip install uwsgitop $ uwsgitop --frequency www.mumuxi.online:8001
- 使用uwsgi启动项目(http协议启动)
uwsgi --http 127.0.0.1:8889 --wsgi-file djweb/wsgi.py
- 使用uwsgi启动项目(uwsgi协议启动,二进制通信协议。速度快)
uwsgi --socket 127.0.0.1:8889 --wsgi-file djweb/wsgi.py
uwsgi配置文件
- 本次pyenv的虚拟目录是=/home/python/package/web=,将Django项目所有项目文件和目录放在这个目录下面。uwsgi的配置文件blog.ini也放在这个目录中(blog.ini文件配置后可以快捷启动服务)
- blog.ini配置如下:
[uwsgi] socket = 127.0.0.1:8889 chdir = /home/python/package/web wsgi-file = djweb/wsgi.py
配置 | 说明 |
---|---|
socket=127.0.0.1:8889 | 使用uwsgi协议通信 |
chdir = /home/python/package/web | Django项目的根目录 |
wsgi-file = djweb/wsgi.py | 指定App文件,blog下wsgi.py |
- 使用配置文件blog.ini启动命令为(项目跟目录下启动):
uwsgi blog.ini
React项目打包
- rimraf递归删除文件,rem -rf
$ npm install rimraf --save-dev 或者 $ yarn add rimraf --dev 在package.json中替换 "build": "rimraf dist && webpack -p --config webpack.config.prod.js" $ npm run build 或者 $ yarn run build 编译成功。查看项目目录中的dist目录。
- 将编译成功后的静态文件放入对应的web静态文件中即可(即nginx相应的web目录)
nginx uwsgi部署
tengine安装
- 淘宝提供的nginx
# yum install gcc openssl-devel pcre-devel -y # tar xf tengine-1.2.3 # cd tengine-1.2.3 # ./configure --help | grep wsgi # ./configure # 第一步 # make && make install # 第二步、第三步 # cd /usr/local/nginx/ # 默认安装位置
http部署
- nginx配置
- =^~ /api/=左前缀匹配
rewrite ^/api(/.*) $1 break;
重写请求的path
server { listen 8888 ; listen [::]:8888 ; server_name 0.0.0.0 ; location ^~ /api/ { rewrite ^/api(/.*) $1 break; proxy_pass http://127.0.0.1:8889; } location / { root /data/web; index index.html; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }
- 修改后启动nginx。
uwsgi部署
- 目前nginx和uwsgi直接使用的是HTTP通信,效率低。改为使用uwsgi通信。
- 使用uwsgi协议的命令行写法如下
uwsgi --socket 127.0.0.1:8889 --wsgi-file djweb/wsgi.py
- 在nginx中配置uwsgi http://nginx.org/en/docs/http/ngx_http_uwsgi_module.html
server { listen 8888 ; listen [::]:8888 ; server_name 0.0.0.0 ; location ^~ /api/ { rewrite ^/api(/.*) $1 break; #proxy_pass http://127.0.0.1:8889; include uwsgi_params; uwsgi_pass 127.0.0.1:8889; } location / { root /data/web; index index.html; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }
- 重新加载nginx配置文件,成功运行。至此,前后端分离的开发、动静分离的部署的博客项目大功告成!
- 参看 https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html
- uwsgi协议https://uwsgi-docs.readthedocs.io/en/latest/Protocol.html
部署图
- 浏览器通过互联网HTTP协议访问NGINX
- 静态内容(图片、JS、CSS、文件)都是Nginx负者提供WEB服务
- Nginx配置代理。可以死Http和Socket通信。本次使用uwsgi协议
- uWSGI服务程序提供uwsgi协议的支持,将从Nginx发来的请求封装后调用WSGI的Application。这个Application可能很复杂,有可能是基于Django框架编写。这个程序将获得请求信息。
- 通过Django的路由,将请求交给视图函数(类)处理,可能需要访问数据库数据,也可能使用了模板。最终数据返回给浏览器。
MVC设计模式
- Controller控制器:负者接收用户请求,调用Model完成数据,调用view完成对用户的响应
- Model模型:负责业务数据的处理
- View视图:负责用户的交互界面
- Model层
- ORM建立对象关系映射,提供数据库操作
- Template层
- 负责数据的可视化,使用HTML、CSS等构成模板,将数据应用到模板中,并返回给浏览器。
- View层
- Django完成URL映射后,把请求交给View层的视图函数处理,调用Model层完成数据,如有必要调用Template层响应客户端,如果不需要,直接返回数据。
Session
Session-Cookie机制
- 网景公司发明了Cookie技术,为了解决浏览器端数据存储问题。
- 每一次request请求时,会把此域名相关的Cookie发往服务器端。服务器端也可以使用response中的set-cookie来
设置cookie值。
- 每一次request请求时,会把此域名相关的Cookie发往服务器端。服务器端也可以使用response中的set-cookie来
设置cookie值。
- 动态网页技术,也需要知道用户身份,但是HTTP是无状态协议,无法知道。必须提出一种技术,让客户端提交的信息可以表明身份,而且不能更改。这就是Session技术。
- Session开启后,会为浏览器端设置一个Coolie值,即SessionID.
- 这个放置SessionID的Cookie是会话级的,浏览器不做持久化存储只放在内存中,并且浏览器关闭自动清除。浏览器端发起一个HTTP请求后,这个SessionID会通过Cookie发到服务器端,服务端就可以通过这个ID查到对应的一个字典结构。如果查无此ID,就为此浏览器重新生成一个SessionID,为它建立一个SessionID和空字典的映射关系。
- 可以在这个ID对应的Session字典中,存入键值对来保持与当前会话相关的信息。
- Session会定期过期清除
- Session占用服务端内存
- Session如果没有持久化,如果服务程序崩溃,那么所有Session信息丢失
- Session可以持久化到数据库中,如果服务程序崩溃,那么可以从数据库中恢复
开启session支持
- Django可以使用Session
- 在settings中,MIDDLEWARE设置中,启用=django.contrib.sessions.middleware.SessionMiddleware=。
- 在INSTALLED_APPS设置中,启用=django.contrib.sessions=。它是基于数据库存储的Session。
- Session不使用,可以关闭上述配置,以减少开销
- 在数据库的表中的django_session表,记录session信息。但可以使用文件系统或其他cache来存储
登录登出实现
登录实现
- 原来登录成功,会使用jwt的token发往客户端。现在不需要了,只需要在Session中记录登录信息即可。(Django默认请求session会持久化数据库中的django_session表中)
def jsonify(obj,allow=None,exclude=set()): """# 筛选对象中的属性,allow白名单,exclude黑名单""" if allow: allow = set(map(lambda x: str.lower(x), allow)) fn = lambda x: x.name.lower() in allow else: exclude = set(map(lambda x: str.lower(x), exclude)) fn = lambda x: x.name.lower() not in exclude names = type(obj)._meta.fields #表的列 return {i.name:getattr(obj,i.name) for i in filter(fn,names)} # 用户登录 @require_POST def login(request:HttpRequest): try: palyload = simplejson.loads(request.body) email = palyload["email"] password = palyload["password"].encode() name = palyload["name"] user = User.objects.get(email=email) # 验证密码 if not bcrypt.checkpw(password,user.password.encode()): return JsonResponse({"error": XddError.USERPW_ERROR},404) # token = get_token(user.id) # res = JsonResponse({ # "user":jsonify(user,exclude={"password"}), # "token":token # },status=201) # res.set_cookie(settings.TOKEN,token) from django.contrib.sessions.backends.db import SessionStore #持久的session session:SessionStore = request.session print(session,type(session)) print(session.keys()) session.set_expiry(300) #设置session会话过期时间300秒 session["user_id"] = user.id # session["user_info"] = "<{} {} {}>".format(user.id,user.name,user.email) #会存储在服务端,数据库中 uobj = jsonify(user,exclude=["password"]) session["user_info"] = uobj # u = session["user_info"] # print(u) # print("-"*30) res = JsonResponse({ "user":uobj },status=201) return res except Exception as e: logging.info(e) return JsonResponse({ "error": XddError.USERPW_ERROR },status=404) # 404错误
- 登录成功,浏览器端,就能收到一条set-Cookie
sessionid=hjg0j01pbvadxdjd5jf9pn91larvv10t; path=/; domain=localhost; HttpOnly; Expires=Wed, 24 Jul 2019 13:19:01 GMT;
- session.set_expiry(300)方法。设置session过期时间
- 300秒过期
- None表示使用全局session过期策略
- 0表示会话级session,即浏览器关闭过期
- datetime对象表示在指定时间点过期
- 建议不要再Session中保存太多数据,也不要保存过于复杂的类型。
认证实现
- 取消原有判断HTTP hander中是否提供了JWT信息,改为判断该SessionID是否能找到一个字典,这个字典中是否有登录成功后设置的user_id键值对信息。
# 身份认证 def authenticate(fn): def _wapper(*args,**kwargs): try: *_,request = args # # 从请求cooker中获取token # token = request.META.get(settings.HTTP_TOKEN) # if not token: # logging.info("身份认证失败") # return HttpResponse(status=401) # # 验证 # id = jwt.decode(token,settings.SECRET_KEY,alogorithms=[settings.JWT_ARITHMETIC])["user_id"] from django.contrib.sessions.backends.db import SessionStore # 持久的session session:SessionStore = request.session id = session["user_id"] #如果获取失败,说明session过期,或没有 user = User.objects.get(id=id) request.user = user res = fn(*args, **kwargs) return res except Exception as e: logging.info(e) return JsonResponse({"error":XddError.LOGIN_OUT_TIME},status=401) return _wapper
登录代码
@require_POST #配置url路由 @authenticate #在有需要的视图函数上加上装饰器 #没有登录过,就不用登出了,所以要认证 def logout(request:HttpRequest): info = "{} logout.".format(request.session["user_id"]) del request.session["user_id"] # 不会清除数据库中记录 request.session.flush() # 情况当前session,删除表中对应记录 return JsonResponse({},status=200)
- 登出时,需要调用flush方法,清除session,清除数据库记录。
- 登录成功,为当前session在django_session表中增加一条记录,如果没有登出操作,那么该记录不会消失。Django也没有自动清除记录的功能。但是Django提供了一个命令clearsessions,建议在cron中定期执行。
django-admin.py clearsessions manage.py clearsessions
不需要认证的view函数中使用Session
- 在当前会话中,每次request请求中都会得到浏览器端发出的SessionID,因此都可以在服务器端找到该ID对应的Session字典,可以使用request.session访问。
- 所有请求都可以使用request.session对象,ID找不到可以认为返回个空字典。
def test1(request): #视图函数,需要配置url映射 if request.session.get("user_id"): #访问Session字典中的值 print(request.session.items()) return HttpResponse("test1 ok") else: return HttpResponseBadRequest() #没有此信息,说明没有登录成功过
Celery调度+Redis安装
Celery是一个使用Python开发的*分布式任务调度模块*,因此对于大量使用Python构建的系统,使用起来方便。
Celery目前爸爸4.x,仅支持Django 1.8以上版本。 Celery
3.1只可以支持Django1.8一下版本。
- Celery官网http://www.celeryproject.org/
- Celery帮助文档http://www.celeryproject.org/docs-and-support/
- 优点:
- 简单:调用接口简单易用
- 高可用:客户端、Worker之间连接自动重试,Broker自身支持HA
- 快速:单个Celery进程每分钟可用数以百万计的任务
- 灵活:Celery的每一个部分都能扩展,或直接使用,或用户自己定义。
常用应用
- Celery可用支持实时异步任务处理,也支持任务的定时调度。
- 异步发邮件
- cleery执行队列
- 间隔半小时同步天气信息等
- celery定时操作
- 异步发邮件
角色
- 任务Task:对应一个Python函数
- 队列Queue:待执行任务的队列
- 工人Worker:一个新的进程,负者执行任务
- 代理Broker:负者调度,在任务环境中使用RabbitMQ、Redis等
- Celery需要依靠RabbitMQ等作为消息代理,同时也支持Redis甚至是Mysql、Mongo等,当然,官方默认推荐的是RabbitMQ,如果使用Redis需要配置。
- 本次采用Redis来作为Broker,也是Redis存储任务结果
安装
$ pip install celery==4.2.0 安装对redis的支持,并自动升级相关依赖 $ pip install -U "celery[redis]"
测试
Celery库使用前,必须初始化,所得示例叫做”应用application或app”。应用是线程安全的。不同应用在同一进程中,可以使用不同拍照、不同组件、不同结果
from celery import Celery app = Celery('mytask') print(app) @app.task def add(x,y): return x+y print(add.name) #mytask.add print(add) print(app.tasks) print(app.conf) print(*list(app.conf.items()),sep = '\n')
- 默认使用amqp链接到本地amqp://guest:**?? (????)/
- 本次使用Redis
Redis安装配置
- 使用Epel源的rpm安装
redis安装,使用提供的rpm安装,redis依赖jemalloc # yum install jemalloc-3.6.0-1.el7.x86_64.rpm redis-3.2.12-2.el7.x86_64.rpm # rpm -pql redis-3.2.12-2.el7.x86_64.rpm /etc/logrotate.d/redis /etc/redis-sentinel.conf /etc/redis.conf /usr/bin/redis-cli /usr/bin/redis-sentinel /usr/bin/redis-server /usr/lib/systemd/system/redis-sentinel.service /usr/lib/systemd/system/redis.service # 编辑redis配置文件 # vi /ect/redis.conf port 6379 #启动时的默认端口 bind 192.168.61.109 #redis启动时的主机 protected-mode no #是否开启保护模式
- 启动、停止redis服务
## 启动服务 # systemctl start redis ## 停止服务 # systemctl stop redis ## 添加开机启动 # systemctl enable redis
broker配置使用
- redis链接字符串格式=redis://password@hostname:port/db_number=
#指定服务器的redis端口6379,使用0号库 app.conf.broker_url = 'redis://192.168.61.109:6379/0'
Celery使用
- 生成任务
from celery import Celery import time app = Celery('mytask') # print(app) # print(add.name) #mytask.add # print(add) # print(app.tasks) # print(app.conf) #配置redis派发任务,任务存放地址 app.conf.broker_url = 'redis://192.168.61.109:6379/0' #配置redis任务状态返回值数据库 app.conf.result_backend = 'redis://192.168.61.109:6379/1' # 重复执行问题解决 # 如果超过visibility_timeout,Celery会认为此任务失败 # 会重分配其他worker执行该任务,这样会造成重复执行。visibility_timeout这个值大一些 # 注意,如果慢任务执行时长超过visibility_timeout依然会多执行 #配置任务超时时间12小时 app.conf.broker_transport_options = {"visibility_timeout":3600*12} #12 hours app.conf.update( enable_utc = True, timezone = "Asia/Shanghai" ) @app.task(name="my_add") def add(x,y): print("start run add x={},y={}".format(x,y)) ret = x+y time.sleep(5) print("end run ret = {}".format(ret)) return ret if __name__ == "__main__": add.delay(4,5) #下发一个任务到broker的queue add.apply_async((10,20),countdown=60) #派发一个任务,延迟60秒后执行
- 注意:上面代码执行,使用add.delay等加入任务到Redis中,在启动celery命令消费Redis的任务,执行并返回结果到Redis中。
- 添加任务的常用方法
- T.delay(arg,kwarg=value) #快捷添加任务的方法
- T:被app.task装饰的函数
- args :位置参数
- kwargs:关键字参数
- T.apply_async((args,),{'kwarg':value},countdown=?,expires=?)
- args 位置参数,是个元组
- kwargs 关键字参数,是个字典
- contdown 延迟多久执行,默认秒
- expires 多久后过期,默认秒
- T.delay(arg,kwarg=value) #快捷添加任务的方法
- 执行任务:如果在Linux下能出现下面问题,可如下配置
from celery import platforms # Linux下,默认不允许root用户启动celery,可使用下面的配置 platforms.C_FORCE_ROOT = True
- 使用命令执行Redis中的任务(默认在pycharm控制台中使用)
$ celery -A test1 worker --loglevel=INFO --concurrency=5 -n worker1@%n
- -A APP, –app App指定app名称,App是模块名
- –loglevel 指定日志级别
- -n 名称,%n指主机名
- –concurrency 指定并发多进程数,缺省值是CPU数
- windows下可能出现下面问题.
[2019-07-25 17:09:29,477: ERROR/MainProcess] Task handler raised error: ValueError('not enough values to unpack (expected 3, got 0)') Traceback (most recent call last): File "d:\mypythonuse\mygitpythonthree\venv\lib\site-packages\billiard\pool.py", line 358, in workloop result = (True, prepare_result(fun(*args, **kwargs))) File "d:\mypythonuse\mygitpythonthree\venv\lib\site-packages\celery\app\trace.py", line 544, in _fast_trace_task tasks, accept, hostname = _loc ValueError: not enough values to unpack (expected 3, got 0) [2019-07-25 17:09:30,354: ERROR/MainProcess] Task handler raised error: ValueError('not enough values to unpack (expected 3, got 0)') Traceback (most recent call last): File "d:\mypythonuse\mygitpythonthree\venv\lib\site-packages\billiard\pool.py", line 358, in workloop result = (True, prepare_result(fun(*args, **kwargs))) File "d:\mypythonuse\mygitpythonthree\venv\lib\site-packages\celery\app\trace.py", line 544, in _fast_trace_task tasks, accept, hostname = _loc ValueError: not enough values to unpack (expected 3, got 0)
- 安装eventlet解决问题
pip install eventlet
包括win10,建议还是按照eventlet
重新执行任务
celery -A test1 worker -p eventlet –loglevel=INFO –concurrency=5 -n worker1@%n
- -P ,–pool 指定进程池实现,默认prefork,windows下使用eventlet
- 运行日志如下
(venv) D:\MyPythonUse\MyGitPythonThree>celery -A text worker -P eventlet --loglevel=INFO --concurrency=4 -n worker1%n -------------- celery@worker1gdy v4.4.0rc2 (cliffs) ---- **** ----- --- * *** * -- Windows-10-10.0.17134-SP0 2019-07-25 17:39:37 -- * - **** --- - ** ---------- [config] - ** ---------- .> app: mytask:0x1a93ee6cc18 - ** ---------- .> transport: redis://192.168.61.109:6379/0 - ** ---------- .> results: redis://192.168.61.109:6379/1 - *** --- * --- .> concurrency: 4 (eventlet) -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) --- ***** ----- -------------- [queues] .> celery exchange=celery(direct) key=celery [tasks] . my_add [2019-07-25 17:39:37,834: INFO/MainProcess] Connected to redis://192.168.61.109:6379/0 [2019-07-25 17:39:37,859: INFO/MainProcess] mingle: searching for neighbors [2019-07-25 17:39:38,909: INFO/MainProcess] mingle: all alone [2019-07-25 17:39:38,963: INFO/MainProcess] celery@worker1gdy ready. [2019-07-25 17:39:38,981: INFO/MainProcess] pidbox: Connected to redis://192.168.61.109:6379/0. [2019-07-25 17:39:39,067: INFO/MainProcess] Received task: my_add[c3e8cfe7-e9d9-49c6-94ff-e6cf5f6a8d83] [2019-07-25 17:39:39,069: WARNING/MainProcess] start run add x=4,y=5 [2019-07-25 17:39:39,076: INFO/MainProcess] Received task: my_add[ac9bd504-c027-4c9a-9ab2-13c4e5c791f1] ETA:[2019-07-25 17:39:58.654164+08:00] [2019-07-25 17:39:44,070: WARNING/MainProcess] end run ret = 9 [2019-07-25 17:39:44,077: INFO/MainProcess] Task my_add[c3e8cfe7-e9d9-49c6-94ff-e6cf5f6a8d83] succeeded in 5.0s: 9 [2019-07-25 17:39:58,663: WARNING/MainProcess] start run add x=10,y=20 [2019-07-25 17:40:03,669: WARNING/MainProcess] end run ret = 30 [2019-07-25 17:40:03,672: INFO/MainProcess] Task my_add[ac9bd504-c027-4c9a-9ab2-13c4e5c791f1] succeeded in 5.0s: 30
发邮件
- 在用户注册完激活时,或修改了用户信息,遇到故障等情况时,都会发送邮件或发送短信息,这些业务场景不需要一直阻塞等待这些发送任务完成,一般都会采用异步执行。也就是说,都会向队列中添加一个任务后,直接返回。
- 发邮件帮助可以参考https://docs.djangoproject.com/en/2.2/topics/email/
- Django中发送邮件需要在settings.py中配置如下
# SMTP EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = "smtp.exmail.qq.com"3 EMAIL_PORT = 465 #缺省25,SSL的异步465 EMAIL_USE_SSL = True #缺省False EMAIL_HOST_USER = "[email protected]" EMAIL_HOST_PASSWORD = "Python123" EMAIL_USE_TLS = False #缺省值False # ## qq邮件配置 # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # EMAIL_HOST = "smtp.qq.com" # EMAIL_PORT = 465 #缺省25,SSL的异步465 # EMAIL_USE_SSL = True #缺省False # EMAIL_USE_TLS = False #缺省值False # EMAIL_HOST_USER = "[email protected]" # EMAIL_HOST_PASSWORD = "*************" #需要在qq邮箱设置红开通POP3/SMTP服务 服务
- 注意:不同邮箱服务器配置不太一样。
- 邮件发送测试代码如下:
from django.core.mail import send_mail from blog import settings def email(): """发送邮件""" print("开始发送") send_mail( "active_email", "Here is the message", settings.EMAIL_HOST_USER, #服务器的发件箱 ["[email protected]"],#目标,收件箱 fail_silently = False, html_message="点击此链接激活邮件<a href='{0}'>{0}</a>".format("www.baidu.com") ) print("发送完成") # 测试函数 def text(request): try: print("aa------- --------") email() return HttpResponse("发送成功",status=201) except Exception as e: logging.info(e) return JsonResponse({"error":"邮件发送失败"},status=400)
Celery在Django中的集成方法
- 新版本Celery集成到Django方式改变了。
- 目录结构
- proj/ - manage.py - proj/ #Django全局目录 - __init__.py - settings.py - celery.py - urls.py - app1 #应用程序目录 - __init__.py - tasks.py - view.py - models.py
- 在Django全局目录中(settings.pys所在目录)
定义一个celery.py文件
""" author:xdd date:2019-07-25 20:21 """ from __future__ import absolute_import, unicode_literals import os from celery import Celery os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings') #必须修改模块名 app = Celery("xdd") app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() app.conf.update( enable_utc=True, timezone="Asia/Shanghai" ) # 配置redis派发任务,任务存放地址 app.conf.broker_url = 'redis://192.168.61.109:6379/0' # 配置redis任务状态返回值数据库 app.conf.result_backend = 'redis://192.168.61.109:6379/1' # 如果超过visibility_timeout,Celery会认为此任务失败 # 会重分配其他worker执行该任务,这样会造成重复执行。visibility_timeout这个值大一些 # 注意,如果慢任务执行时长超过visibility_timeout依然会多执行 app.conf.broker_transport_options = {"visibility_timeout": 3600 * 12} # 12 hours
修改=__init__.py=文件
from __future__ import absolute_import, unicode_literals from .celery import app as celery_app __all__ = ('celery_app',)
在user应用下
新建文件=tasks.py=中新建任务
""" author:xdd date:2019-07-25 20:32 """ from __future__ import absolute_import, unicode_literals from blog.celery import app from django.core.mail import send_mail from blog import settings import datetime @app.task(name="server_email") def email(active_url,email=[]): """发送邮件""" print("开始发送") send_mail( "激活邮件", "Here is the message", settings.EMAIL_HOST_USER, #服务器的发件箱 email,#目标,收件箱 fail_silently = False, html_message="点击此链接激活邮件<a href='{0}'>{0}</a> 时间:{1:%Y-%m-%d %H:%M:%S}".format(active_url,datetime.datetime.now()) ) print("发送完成")
views.py视图函数中调用
from .tasks import email # 测试函数 def text(request): try: print("aa------- --------") email.delay("www.baidu.com",["[email protected]"]) return HttpResponse("发送成功",status=201) except Exception as e: logging.info(e) return JsonResponse({"error":"邮件发送失败"},status=400)
- 以后放在注册函数中发送邮件。
- 注意:目前在Python3.7部署时,发邮件可能会报wrap_socket()错。可以使用python3.6版本
urls.py路由视图函数
""" author:xdd date:2019-07-17 22:09 """ from django.conf.urls import url from .views import reg,login,text,logout from django.http import HttpResponse from user.models import User urlpatterns = [ url(r'^$',reg), #用户注册 url(r'^login$',login), #用户登录 url(r'^logout$',logout), #用户登出 url(r'^text$',text), ]
- 访问=http://127.0.0.1:8000/users/text=%E4%BC%9A%E8%B0%83%E7%94%A8text%E8%A7%86%E5%9B%BE%E5%87%BD%E6%95%B0%EF%BC%8C%E6%89%A7%E8%A1%8Cemail.delay()%EF%BC%8C%E4%BC%9A%E5%9C%A8redis%E4%B8%AD%E5%A2%9E%E5%8A%A0%E4%BB%BB%E5%8A%A1。
- 执行任务,下面命令会从redis中拿任务执行
celery -A blog -P eventlet worker –loglevel=INFO –concurrency=4 -n worker@%n
Flask框架简单使用
[toc]
- 安装:=pip install flask=
- Flask快速入门:http://docs.jinkan.org/docs/flask/quickstart.html#quickstart
快速构建
- 在项目根目录下构建:
- webapp包目录,存放flask代码,包内有=__init__.py=文件
- templates目录,存放模板文件
- static目录,存放js,css等静态文件。其下建立js目录,放入jquery、echarts的js文件
- app.py入口文件
- 基本组成
- 目录结构如下:
# /webapp/__init__.py文件内容 from flask import Flask,jsonify #创建应用 app = Flask("myweb") #路由和视图函数 @app.route("/") def index(): return "hello flask" @app.route("/json",methods=["GET"]) #列表中指定多个方法 def getjson(): d = {"a":1,"b":2,"c":3} return jsonify(d) #Mime是application/json #打印重要属性 print(*filter(lambda x: not x[0].startswith("__") and x[1],app.__dict__.items()),sep="\n") print("- "*30) print(app.url_map) print(app.template_folder) print(app.static_folder) print("- "*30)
- 应用:创建出来提供WEB服务的实例,也是wsgi的入口
- 视图函数:执行内部代码输出响应的内容
- 路由:通过route装饰器创建path到视图函数的映射关系
#/main.py文件 from webapp import app if __name__ == "__main__": app.run("127.0.0.1",port=8080,debug=True)
- 启动main.py文件
蓝图
Flask中,基本上都是route装饰器和视图函数的映射,如果函数很多,代码组织结构会非常乱。
蓝图Blueprint,就是Flask中*模块化*技术
- 新建=/web/app/books.py=蓝图文件
#/webapp/books.py文件 from flask import Blueprint,jsonify,render_template bpbooks = Blueprint("booksapp",__name__,url_prefix="/books") # bpbooks = Blueprint("booksapp",__name__) @bpbooks.route("/",methods=["GET","POST"]) def getall(): books = [ (1,"java",20), (2,"python",40), (3,"linux",50) ] return jsonify(books) # print("= "*30) # for x in bpbooks.__dict__.items(): # print(x)
- 修改=/webapp/__init__.py=文件如下:在app中注册新建的蓝图文件
#/webapp/__init__.py from flask import Flask,jsonify from .books import bpbooks #创建应用 app = Flask("myweb") #路由和视图函数 @app.route("/") def index(): return "hello flask" @app.route("/json",methods=["GET"]) #列表中指定多个方法 def getjson(): d = {"a":1,"b":2,"c":3} return jsonify(d) #Mime是application/json # 注册蓝图 # app.register_blueprint(bpbooks) #如果在app中注册蓝图时,给定了url_prefix,那么蓝图内自定义的url_prefix将失效 app.register_blueprint(bpbooks,url_prefix="/bookss") #打印重要属性 print(*filter(lambda x: not x[0].startswith("__") and x[1],app.__dict__.items()),sep="\n") print("- "*30) print(app.url_map) print(app.template_folder) print(app.static_folder) print("- "*30)
- 注册完成后,启动=/main.py=文件
- Blueprint构造参数
- name,蓝图名称,注册在app的蓝图字典中用的key
- import_name,用来计算蓝图模块所在路径,一般写=__name__=
- root_path,指定蓝图模块所在路径,如果None,使用import_name计算得到
- template_folder
- url_prefix,指定本蓝图模块的路径前缀,app.register_blueprint注册蓝图时,也可以对当前蓝图指定url_prefix,将覆盖蓝图中的定义。
- 特别注意,输出的root_path路径,说明蓝图有自己一套路径。
- 最后app.register_blueprint(bpbooks,url_prefix="/bookss"),url_prefix一定要以=/=开始,否则报错,最后路径以注册的url_prefix为准
模板
Flask使用jinja2模板。
对于应用app来说其模板是,根目录下的templates,其下新建index.html
/templates/index.html文件内容如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>xdd web</title> </head> <body> <h2>欢迎使用flask框架</h2> <hr> {% for x in userlist %} {{x}} {% endfor %} </body> </html>
- 修改=/webapp/__init__.py=文件中的index视图函数,使用模板渲染函数。
from flask import Flask,jsonify,render_template #创建应用 app = Flask("myweb") #路由和视图函数 @app.route("/index.html") def index(): return render_template("index.html", userlist=[ (1, "tom", 20), (1, "json", 30), ])
- jinja2和Django模板语法一致,这里不阐述
在app.jinja_loader属性中,有下面的语句
@locked_cached_property def jinja_loader(self): """The Jinja loader for this package bound object. .. versionadded:: 0.5 """ if self.template_folder is not None: return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
- 说明:不管是app,还是bluepoint,都是使用自己的root_path和模板路径拼接成模板路径。
- 可以通过app或者bluepoint查看=app.jinja_loader.searchpath=模板搜索路径。