Python: 前后端分离博客项目

博客系统

数据库模型设计

分析

多人使用博客系统。采用BS架构实现。市面上多数某某系统归根结底都是这种设计。

博客系统,核心模块有:

  1. 用户管理
    • 注册、登录
    • 删除查用户
  2. 博文管理
    • 增删改查博文

需要数据库,本次使用Mysql8+, InnoDB引擎。

需要支持多用户登录,各自可以管理自己的博文(增删改查),管理是不公开的,但是博文是不需要登录就可以公开预览的。

先不要思考过多的功能,先完成最小的核心需求代码。

数据库设计

创建数据库
CREATE DATABASE IF NOT EXISTS `blog2` DEFAULT CHARACTER SET utf8mb4;

需要用户表、文章表

用户表user
字段 说明
id 主键,唯一标识
username 登录名,唯一
name 用户姓名,描述性字段
email 电子邮箱,注册用信息,应该唯一。可用作登录名、可用于密码找回
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个字符的

一对多关系:一篇博文属于一个作者,一个作者有多篇博文。

  1. 博客内容选取什么字段类型?
  2. 多大合适?
  3. 博文图片如何处理?
  4. 适合和其它字段放在同一张表吗?

Content字段的设计

  1. 字段类型 :博文一般很长,不可能只有几百个字符,需要大文本字段。MySQL中,选择TEXT类型,而不是char或者varchar类型。
  2. 大小 :text类型是65535个字符,如果不够用,选择longtext,有\(2^{32}-1\)个字符长度。足够使用了。
  3. 图片存储 :博文就像HTML一样,图片是通过*路径信息*将图片是嵌入在内容中的,所以保存的内容还是字符串。图片来源有2中:
    • 外联:通过URL链接访问,本站不用存储该图片,但容易引起盗链问题。
    • 本站存储:需要提供博文的在线文本编辑器,提供图片上传到网站存储,并生成图片URL,这个URL嵌入博客正文中,不会有盗链问题,但要解决众多图片存储问题、水印问题、在线压缩问题、临时或垃圾图片清理等等难题。
    • 本博客项目不实现图片功能
  4. 字段考虑
    • 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快速开发框架。
  • 优点:
    1. 能够快速开发,自带ORM、Template、Form、Auth核心组件
    2. MVC设计模式
    3. 实用的管理后台Admin
    4. 简洁的url设计
    5. 周边插件丰富
  • 缺点:架构重、同步阻塞
  • 所有Django的设计目标就是一款大而全,便于企业快速开发项目的框架,因此企业应用较广。

安装Django

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

重要文件说明

  1. manage.py:本项目管理的命令行工具。应用创建、数据库迁移等都使用它完成
  2. blog/settings.py:本项目的核心配置文件。
    • 应用、数据库配置
    • 模板、静态文件
    • 中间件、日志
    • 第三方插件配置
  3. blog/urls.py:URL路径映射配置。项目初始,只配置了/admin的路由。
  4. 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文档
  1. 数据库引擎ENGINE
  2. 内建引擎有
    • 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目录,有如下文件:

  1. admin.py: 应用后台管理声明文件
  2. models.py: 模型层Model类定义
  3. views.py: 定义URL响应函数或类
  4. migrations包: 数据迁移文件生成目录
  5. apps.py: 应用的信息定义文件

user应用创建后应该完成以下功能:

  1. 用户注册
  2. 用户登录
注册应用

在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
  • 缺省主键
    1. 缺省情况下,Django的每一个Model都有一个名为id的AutoField字段,如下
      • id = models.AutoField(primary_key=True)
    2. 如果显示定义了主键,这种缺省主键就不会被创建了。Python之禅中说“显示优于隐试”,所以,尽量使用自己定义的主键,哪怕该字段名就是id,也是一种不错的选择。
  • 字段选项

官方文档

说明
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

  1. 迁移Migration
    • 迁移:从模型定义生成数据库的表
  1. 生成迁移文件。执行 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,迁移文件的序号会增加。
    • 注意:
      1. 迁移的应用必须在settings.py的INSTALLED_APPS中注册。
      2. 不要谁便删除这些迁移文件,因为后面的改动都是要依据这些迁移文件的。
    • 生成的迁移文件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',
                },
            ),
        ]
    
  2. 执行迁移生成数据库的表 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) 进行模式匹配
      1. regex: 正则表达式,与之匹配的URL会执行对应的第二个参数view
      2. view: 用于执行与正则表达式匹配的URL请求
      3. kwargs: 视图使用的字典类型的参数
      4. name: 用来反向获取URL
  • path函数(2.x+)
    • path(route, view, kwargs=None, name=None)
      • 对URL进行模式匹配
      • 可以使用尖括号来捕获部分URL成分。一旦使用了尖括号提取成分,每一个成分都会变成一个 实参
      • 注入 到视图函数中,视图函数必须增加形参接收
      • 形参函数基本同url函数

还有一个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) #不同路径可以指向同一个函数执行
]
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目录,请构建这个目录。
    1. 项目根目录中构建templates文件夹。模板文件目录
  • APP_DIRS:是否运行在每个已经安装的应用中查找模板。应用自己目录下游templates目录,例如:=django/contrilb/admin/templates=。如果应用需要可分离、可重用,建议吧模板放到应用目录下
  • *BASE_DIR*是项目根目录,=os.path.join(BASE_DIR,'templates')=就是在manage.py这一层建立一个目录templates。这个路径就是以后默认找模板的地方。

模板渲染

  • 模板页

    1. 新建html文件,在=/templates/index.html=目录下
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Django web 模板技术</title>
    </head>
    <body>
    我是模板,数据是{{content}}
    </body>
    </html>
    
  • 模板处理

    1. *加载模板*:模板是一个文件,需要从磁盘读取并加载。要将模板放置在指定的模板文件夹中
    2. *渲染*:模板需要使用内容数据渲染
    3. 测试:修改=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))
    
    1. 运行server python manage.py runserver
    2. 访问=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快捷渲染函数

    1. 上面2个步骤代码编写繁琐,Django提供了对其的封装-------–—快捷函数render。
    2. 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,遵循以下顺序:
    1. 字典查找,例如=foo["bar"]=,把foo当做字典,bar当做key
    2. 属性或方法的查找,例如=foo.bar=,把foo当做对象,bar当做属性或方法
    3. 数字索引查找,例如=foo[1]=,把foo当做列表一样,使用索引访问
    4. 示例: 修改=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()}}=。
模板标签
  1. =if/else=标签
    • 基本语法如下:

      {% if condition %}
          ... display
      {% endif %}
      
      {% if condition %}
          ... display 1
      {% elif condition2 %}
          ... display 2
      {% else %}
          ... display 3
      {% endif %}
      
    • 条件也支持=and、or、not=
    • 注意,因为这些标签是断开的,所以不能像Python一样使用缩进就可以表示出来,必须有个结束标签,例如endif、endfor。
  2. =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 %}
    
  3. =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 %}
    
    • 其他标签
      1. *csrf_token*用于跨站请求伪造保护,防止跨站攻击的。={% csrf_token %}=
  4. 注释标签

    • 单行注释={# #}=
    • 多行注释={% comment %}… {% endcomment %}=
    {# 这是一个注释 #}
    {% comment %}
    这是多行注释
    {% endcomment %}
    
  5. 过滤器

    • 模板过滤器可以在遍历被显示前修改它。
    • 语法 {{ 变量|过滤器}}
      1. 过滤器使用管道字符=|=,例如={{ name|lower}}=,={{ name }}=变量被过滤器lower处理后,文档大写转换文本为小写。
      2. 过滤管道可以被*套接*,一个过滤器管道的输出又可以作为下一个管道的输入。
        • 例如={{ my_list|first|upper }}=,将列表第一个元素并将其转化为大写。
      3. 过滤器传参
        • 有些过滤器可以传递参数,过滤器的参数跟随冒号之后并且总是以双引号包含。
        • 例如:
          1. {{bio|truncatewords:"30"}},截取显示变量bio的前30个词。
          2. {{ 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日
模板实例
  1. 奇偶行列表输出

    • 使用下面字典my_dict的c的列表,在模板网页中列表ul输出多行数据
      1. 要求奇偶行颜色不同
      2. 每行有行号(从1开始)
      3. 列表中所有数据都增大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>
    
附加–Pycharm模板自定义
  1. 第一步:
  2. 第二步
  3. 第三步

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
    1. 指定过滤条件=GET /posts?tag=python=
  • 排序Sorting
    1. 指定排序条件。有很多种设计风格,例如使用=+=表示asc,=-表示desc。=GET /posts?sort=+title,-id=获取=GET /posts?sort=title_asc,id_desc
  • 分页Pagination
    1. 一般情况下,查询返回的记录数非常多,必须分页。=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种风格
    1. http://api.xdd.com/v1/posts/10 这种风格会跨域,适合较大的项目
    2. 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")

测试JSON数据

  • 使用POST方法,提交数据类型为application/json,json字符串要使用*双引号*。
  • 这个数据是注册用的,由客户端提交。

    1. 数据提交模板为:
    {
        "password":"abc",
        "name":"xdd",
        "email":"[email protected]"
    }
    
    1. 可以使用Postman软件测试。

CSRF处理

  • 在Post数据的时候,发现出现了下面的提示
    1. 原因:*默认Django CsrfViewMiddleware中间件会对所有POST方法提交的信息做CSRF校验*。
  • CSRF或XSRF(Cross-site Request Forgery),即跨站请求伪造。它也被称为:one click attack/session riding。是一种对网站的恶意利用。它伪造成来自受信任用户发起的请求,难以防范。
  • 原理:
    1. 用户登录某网站A完成登录认证,网站返回敏感信息的Cookie,即使是会话级的Cookie
    2. 用户没有关闭浏览器,或认证的Cookie一段时间内不过期还持久化了,用户就访问攻击网站B
    3. 攻击网站B看似一切正常,但是某些页面里面有一些隐藏运行的代码,或者诱骗用户操作的按钮等
    4. 这些代码一旦运行就是悄悄地向网站A发起特殊请求,由于网站A的Cookie还有效,且访问的是网站A,则其Cookie就可以一并发给网站A
    5. 网站A看到这些Cookie就只能认为是登录用户发起的合理合法请求,就会执行
  • CSRF解决
    1. 关闭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',
      ]
      
    2. csrftoken验证
      • 在表单POST提交时,需要发给服务器一个csrf_token
      • 模板中的表单Form中增加={% csrf_token %}=,它返回到了浏览器端就会为cookie增加csrftoken字段,还会在表单中增加一个名为csrfiddlewaretoken隐藏控件=<input type='hidden' name='csrfmiddlewaretoken' value='jZTxU0v5mPoLvugcfLbS1B6vT8COYrKuxMzodWv8oNAr3a4ouWlb5AaYG2tQi3dD' />=
      • POST提交表单数据时,需要将csrfmiddlewaretoken一并提交,Cookie中的csrf_token也一并会提交,最终在中间件中比较,相符通过,不相符就看到上面的403提示
    3. 双cookie验证
      • 访问本站先获得csrftoken的cookie
      • 如果使用AJAX进行POST,需要在每一次请求Header中增加自定义字段X-CSRFTOKEN,其值来自cookie中获取的csrftoken值
      • 在服务端比较cookie和X-CSRFTOKEN中的csrftoken,相符通过
  • 现在没有前端代码,为了测试方便,可以选择第一种方法先禁用中间件,测试完成后开启。

JSON数据处理

  • simplejson标准库方便好用,功能强大。
    1. 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)
  1. 邮箱检查
    • 邮箱检查需要查user表,需要使用filter方法。
    • email=email,前面是字段名email,后面是email变量。查询后返回结果,如果查询有结果,则说明该email已经存在,邮箱已经注册,返回400到前端
  2. 用户信息存储
    • 创建User类实例,属性存储数据,最后调用save方法。Django默认是在save()、delete()的时候事务*自动提交*。
    • 如果提交抛出任何错误,则捕获此异常做相应处理。
    • 如果没有异常,则返回201,不要返回任何用户信息。之后可能需要验证、用户登录等操作。
  3. 异常处理
    • 出现获取输入框提交信息异常,就返回400
    • 查询邮箱存在,返回400
    • save()方法保存数据,有异常,则返回400
    • 注意一点,Django的异常类继承自HttpResponse类,所以不能raise,只能return
    • 前端通过状态码判断是否成功
    • 由于采用Restful实战,所有异常全部返回JSON的错误信息,所以一律使用了JsonResponse

Django日志

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
    1. 数据的校验validation是在对象的Save、update方法上
    2. 对模型对象的CRUD,被Django ORM转换成相应的SQL语句操作不同的数据源。
查询
  • 查询集
    1. 查询会返回结果的集,它是django.db.models.query.QuerySet类型。
    2. 它是惰性求值,和sqlalchemy一样。结果就是查询的集。
    3. 它是可迭代对象。
  • 惰性求值
    • 创建查询集不会带来任何数据库的访问,直到调用方法使用数据时,才会访问数据库。在迭代、序列化、if语句中都会立即求值。
  • 缓存:
    • 每一个查询集都包含一个缓存,来最小化对数据库的访问。
    • 新建查询集,缓存为空。首次对查询集求值时,会发生数据库查询,Django会把查询的结果存在这个缓存中,并返回请求的结果,接下来对查询集求值将使用缓存的结果。
    • 观察下面的2个例子是要看真正生成的语句了
      1. 没有使用缓存,每次都要去查库,查了2次库

        [user.name for user in User.objects.all()]
        [user.name for user in User.objects.all()]
        
      2. 下面的语句使用缓存,因为使用同一个结果集

        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
    
  • 过滤器
    1. 返回*查询集*的方法,称为过滤器,如下:
    名称 说明
    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机制
    1. 浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为这是第一次请求,会返回一个新的session id给浏览器,浏览器只要不关闭,这个session id就会随着每一次请求重新发给服务端,服务器端查找这个session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。
    2. session是会话级的,服务端还可以在这个会话session中创建很多数据session键值对。
    3. 这个session id有过期的机制,一段时间如果没有发起请求,认为用户已经断开,服务端就清除本次会话所有session。浏览器端也会清除相应的cookie信息。
    4. 服务端保存着大量session信息,很消耗服务器内存,而且如果多服务器部署,可以考虑session复制集群,也可以考虑session共享的问题,比如redis、memcached等方案。
  • 无ssession方案
    1. 既然服务端就是需要一个ID来表示身份,那么不使用session也可以创建一个ID返回给客户端。但是,要保证客户端不可篡改该信息。
    2. 服务端生成一个标识,并使用某种算法对标识签名。
    3. 服务端收到客户端发来的标识,需要检查签名。
    4. 这种方案的缺点是,加密、解密需要消耗CPU计算资源,无法让浏览器自己主动检查过期的数据以清除。这种技术称作JWT(json WEB Token)。
  • JWT

    1. JWT(Json WEB Token)是一种采用Json方式安装传输信息的方式。本次使用PyJWT,它是Python对JWT的实现。
    2. 安装=pip install pyjwt=
    3. 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))
    
  1. 由此,可知jwt生成的token分为三部分
    1. header,由数据类型、加密算法构成
    2. payload,负载就是要传输的数据,一般来说放入python对象即可,会被json序列化的
    3. signature,签名部分。是签名2部分数据分别base64编码后使用点号链接后,加密算法使用key计算好一个结果,再被base64编码,得到签名
    4. 所有数据都是明文传输的,只是做了base64,如果是敏感信息,请不要使用jwt。
    5. 数据签名的目的不是为了隐藏数据,而是保证数据不被篡改。如果数据篡改了,发回到服务器端,服务器使用自己的key再计算一遍,然后进行签名比对,一定对不上签名。
  2. jwt使用场景
    • 认证:这是jwt最常用的场景,一旦用户登录成功,就会得到wt,然后请求中就可以带上这个jwt。服务器中jwt验证通过,就可以被允许访问资源。甚至可以在不同域名中传递,在单点登录(Single Sign On)中应用广泛。
    • 数据交换:jwt可以防止数据被篡改,它还可以使用公钥、私钥加密,确保请求的发送者是可信的
  • 密码
    1. 使用邮箱+密码方式登录。
    2. 邮箱要求唯一就行了,但是,密码存储需要加密。早期,都是用明文的密码存储。后来,使用MD5存储,但是,目前也不安全,网上有很多MD5的网站,使用反查方式找到密码。
    3. *加盐*,使用hash(password+salt)的结果存入数据库中,就算拿到数据库的密码反查,也没有用了。如果是固定加盐,还是容易被找到规律,或者从源码中泄露。随机加盐,每一次盐都变,就增加了破解的难度。
    4. *暴力破解*:什么密码都不能保证不被暴力破解,例如穷举。所以要使用慢hash算法,例如bcrypt,就会让每一次计算都很慢,都是秒即的,这样穷举的时间就会很长,为了一个密码破解的时间在当前CPU或者GPU的计算能力下可能需要几十年以上。
  • bcrypt

    1. 安装=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))
    
  1. 从耗时看出,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

  1. 全局变量

    • 项目的settings.py文件实际上就是全局变量的配置 文件。
    • SECRET_KEY一个强KEY
    from django.conf import settings
    print(settings.SECRET_KEY)
    
  2. 使用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信息?
    1. 使用Header中的Authorization
      • 通过这个header增加token信息。
      • 通过header发送数据,方法可以是Post、Get
    2. 自定义header
      • 在Http请求头中使用X-JWT字段来发送token
  • 本次选择第二种
认证流程
  • 基本上所有的业务都有需要认证用户的信息。
  • 在这里比较实际戳,如果过期,就直接抛未认证成功401,客户端收到后就改直接跳转到登录页。
  • 如果没有提交user id,就直接重新登录。如果用户查到了,填充user对象。
  • request->时间戳比较->user id比较->向后执行
Django的认证
  • django.contrilb。auth中提供了许多方法,这里主要介绍其中的三个:
    1. authenticate(**credentials)
      • 提供了用户认证,即验证用户名以及密码是否正确
      • user = authentica(username='someone',password='somepassword')
    2. login(HttpRequest,user,backend=None)
      • 该函数接受一个HttpRequest对象,以及一个认证了的User对象
      • 此函数使用django的session框架给某个已认证的用户附加上session id等信息。
    3. logout(request)
      • 注销用户
      • 该函数接受一个HttpRequest对象,无返回值。
      • 当调用该函数时,当前请求的session信息会全部清除
      • 该用户即使没有登录,使用该函数也不会报错
      • 还提供了一个装饰器来判断是否登录django.contrib.auth.decorators.login_required
      • 本项目使用了无session机制,且用户信息自己建表管理,所以,认证需要自己实现。
中间件技术Middleware
  1. 官方定义,在Django的request和response处理过程中,由框架提供的hook钩子
  2. 中间技术在1.10后发生了改变,我们当前使用1.11版本,可以使用新的方式定义。
  3. 参考https://docs.djangoproject.com/en/1.11/topics/http/middleware/#writing-your-own-middleware
  4. 原理
# 测试代码添加在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)
  • 流程图
  • 结论
    1. Django中间件使用的洋葱式,但有特殊的地方
    2. 新版本中间件现在=__call__=中get_response(request)之前代码(相当于老版本中的process_request)
    3. settings中的顺序先后执行所有中间件的get_response(request)之前代码
    4. 全部执行完解析路径映射得到view_func
    5. settings中顺序先后执行process_view部分
      • return None 继续向后执行
      • return HttpResponse() 就不在执行其他函数的preview函数了,此函数返回值作为浏览器端的响应
    6. 执行view函数,前提是签名的所有中间件process_view都返回None
    7. 逆序执行所有中间件的get_response(request)之后代码
    8. 特别注意,如果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中注册
    
    1. 中间件拦截所有视图函数,但是只有一部分请求需要提供认证,所以考虑其他方法。
    2. 如果绝大多数都需要拦截,个别例外,采用中间件比较合适。
    3. 中间件有很多用户,适合拦截所有请求和响应。例如浏览器端的IP是否禁用、UserAgent分析、异常响应的统一处理。
    4. 用户验证装饰器
      1. 在需要认证的view函数上增强认证功能,写一个装饰器函数。谁需要认证,就在这个view函数上应用这个装饰器。
      2. 定义常量,可以在当前模块中,也可以定义在settings.py中。本次在=djweb/setting.py=中添加

        # 在`djweb/setting.py`中添加
        #自定义常量
        AUTH_EXPIRE = 8* 60 * 60 #8小时过期
        AUTH_HEADER = "HTTP_JWT" #浏览器端是jwt,服务器端被改写为全大写并加HTTP_前缀
        
      3. 用户验证装饰器代码

        # 本次写在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
        ]
        
        • 测试方法(先登录后测试)
    5. *JWT过期问题*(pyjwt过期)
      1. 在decode的时候,默认开启过期验证,如果过期,则抛出异常
      2. 需要在payload中增加claim exp,也就是exp的键值对,记录过期的时间点
      3. exp要求是一个整数int的时间戳,或时间
      4. exp键值对存在,才会进行过期校验
      5. 测试

        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"
    ]
    
  • 添加对应路由
    1. 修改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"))
      ]
      
    2. 新建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),
      ]
      
    3. 修改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)
      
  • 构建数据库模型

    1. 修改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__
    
    1. 注意:on_delete在Django2.0开始,on_delete必须提供,参考https://docs.djangoproject.com/en/1.11/ref/models/fields/#django.db.models.ForeignKey.on_delete
      1. models.CASCADE 级联删除。Django在DELETE级联上模拟SOL约束的行为,并删除包含外键的对象。
      2. models.PROTECT 通过引发ProtectedError (django.db.IntegrityError的子类)防止删除引用的对象。
      3. models.SET_NULL 设置外键为空;这只有在null为真时才有可能。
      4. models.SET_DEFAULT 将外键设置为其默认值;必须为外键设置默认值。
      5. models.DO_NOTHING 不采取任何行动。如果数据库后端强制执行引用完整性,这将导致IntegrityError,除非手动向数据库字段添加一个SOL ON DELETE约束。
  • 视图分类
    1. function-based view 视图函数:视图功能有函数实现
    2. 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映射配置中
      1. 本质上,as_view()方法还是把一个类伪装成了一个视图函数。
      2. 这个视图函数,内部使用了一个分发函数,使用请求方法名称吧请求分发给存在的同名函数处理。
    # 修改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
  • 事务的使用方法
    1. 装饰器用法

      @transaction.atomic #装饰器用法
      def viewfunc(request):
          # This code executes inside a transaction
          do_stuff()
      
    2. 上下文用法

      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)
    
  • 启动后测试
    1. 带jwt访问=http://127.0.0.1:8000/posts/=%E9%9C%80%E8%A6%81%E5%85%88%E7%99%BB%E5%BD%95

文章接口实现

  • 根据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 /posts/?page=3&size=20 文章列表,视图类PostView

响应
200 成功返回文章列表
  • 完善分页
    1. 分页信息,一般有:当前页/总页数、每页条数,记录总数。
      • *当前页*: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)
改写校验函数
  • 修改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
  1. 修改项目信息=package.json=文件

    {
        "name":"blog",
        "description":"blog project",
        "author":"xdd"
    }
    
  2. 修改=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
                }
            }
        }
    
  3. 安装依赖 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 #
    
  4. 相关命令

    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] 卸载

开发

前端路由
/* 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'));
  • 注意:
    1. Link组件相当于a标签
    2. *Route*组件不可见,用来做Router的路由定义。当网页中的地址栏中地址发生改变,会从Route中匹配对应的路径加载对应的组件。
  • *启动项目*=yarn run start=

    1. 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"
        },
    }
    
  • Route指令
    • 它负责静态路由,只能和Route指定的path匹配,组件就可以显示。URL变化,将重新匹配路径
    • component属性设置目标组件
    • path是匹配路径,如果匹配则显示组件
      1. exact:布尔值
      2. 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'));
    
    1. 路由配置

      • 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"=    
    2. 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没有匹配,才轮到它。
登录组件
<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模板来构建组件
  • 特别注意
    1. 搬到React组件中的时候,要*将class属性改为className*.
    2. 所有标签,需要闭合。
  • login.js

    1. 在component目录下新建login.js的登录组件。
    2. 使用上面的模板的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>
        )
    }
}
  1. 问题:
    • 页面提交
      1. 这次发现有一些问题,按钮点击会提交,导致页面刷新了。要阻止页面刷新,其实就是阻止提交。使用event.preventDefault()。
    • 如何拿到邮箱和密码?
      1. =event.target.form=返回暗流所在表单,可以看做一个数组。
      2. =fm[0].value=和=fm[1].value=就是文本框的值。
    • 如何在Login组件中使用UserService实例呢?
      1. 使用全局变量,虽然可以,但不好。
      2. 可以在Login的构造器中通过属性注入。
      3. 也可以在外部使用props注入。使用这种方式。
  2. 修改,保证在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方法实现
    1. 代理配置

      • 修改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
              }
          }
      }
      
    2. 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);
                    }
                )
            }
        }
        
      • 问题:
        1. 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=。
        2. 解决:
          1. 修改blog server的代码的路由匹配规则(不建议这么做,影响比较大)
          2. rewrite,类似httpd,nginx等的rewrite功能。本次测试使用的是dev server,去官方看看。https://webpack.js.org/configuration/dev-server/#devserver-proxy可以看到pathRewrite可以完成路由重写。
        3. 修改=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
    1. 一个域一个datatable
    2. key-value检索方式
    3. 建立在关系型的数据模型之上,具有索引表、游标、事务等概念
  • store.js
    1. store.js是一个兼容所有浏览器的LocalStorage包装器,不需要借助Cookie或者Flash。
    2. store.js会根据浏览器自动选择使用localStorage、globalStorage或者userData来实现本地存储功能。
  • *安装*=npm i store=或=yarn add store=
  • 测试代码
    1. 编写一个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
      
  1. 安装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);
    
  1. 下面是准备写在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
    1. 社区提供的状态管理库,有Redux和Mobx。
    2. Redux代码优秀,使用严格的函数式编程思想,学习曲线陡峭,小项目使用的优劣不明显。
    3. Mobx,非常优秀稳定的库,简单方便,适合中小项目使用。使用面向对象的方式,容易学习和接受。现在在中小项目中使用也非常广泛。Mobx和React也是一对强力组合。
  • 观察者模式
    1. 观察者模式,也称为*发布订阅模式*。观察者观察某个目标,目标对象(Obserable)状态发生了变化,会通知自己内部注册了的观察者Observer。
  • 状态管理
    1. 需求:
      • 一个组件的onClick触发事件响应函数,此函数会调用后台服务。但是后台服务比较耗时,等处理完,需要引起组件的渲染操作。
      • 要组件渲染,就需要改变组件的props或state。
    2. 同步调用
      • 同步调用中,实际上就是等着耗时的函数返回
    3. 异步调用
      • 思路一,使用setTimeout问题
        1. 无法向内部的等待执行函数传入参数,比如Root实例。
        2. 延时执行的函数的返回值无法取到,所以无法通知Root
      • 思路二、Promise异步执行

        1. Promise异步执行,如果成功,将调用回调。
        2. 不管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'));
        
  1. 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函数。
    • 被观察者变化不引起渲染的情况:
      1. 将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登录功能代码实现

  1. =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);
                }
            )
        }
    }
    
  2. =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]

注册功能实现

  1. 在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);
                }
            )
        }
    }
    
  2. 修改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实例即可),其他模块直接导入并使用这个实例即可。
  3. 修改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))
            }
        }
    
    }
    

进阶装饰器

  1. 新建src/utils.js放入以下内容

    import React from 'react';
    
    const inject = obj => Comp => props => <Comp {...obj} {...props} />
    
    export {inject}
    
  2. 将登陆、注册组件装饰一下
    • 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]

导航菜单

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'));

页面布局

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};

发布组件

// 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';
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=,使用下面链接测试
    1. http://www.mumuxi.online:8000/
    2. http://www.mumuxi.online:8000/posts/?page=2&size=2
    3. 运行正常。
  • =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配置
    1. =^~ /api/=左前缀匹配
    2. 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部署
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 {
    }
}

部署图

  1. 浏览器通过互联网HTTP协议访问NGINX
  2. 静态内容(图片、JS、CSS、文件)都是Nginx负者提供WEB服务
  3. Nginx配置代理。可以死Http和Socket通信。本次使用uwsgi协议
  4. uWSGI服务程序提供uwsgi协议的支持,将从Nginx发来的请求封装后调用WSGI的Application。这个Application可能很复杂,有可能是基于Django框架编写。这个程序将获得请求信息。
  5. 通过Django的路由,将请求交给视图函数(类)处理,可能需要访问数据库数据,也可能使用了模板。最终数据返回给浏览器。

MVC设计模式

  • Controller控制器:负者接收用户请求,调用Model完成数据,调用view完成对用户的响应
  • Model模型:负责业务数据的处理
  • View视图:负责用户的交互界面
  • Model层
    1. ORM建立对象关系映射,提供数据库操作
  • Template层
    1. 负责数据的可视化,使用HTML、CSS等构成模板,将数据应用到模板中,并返回给浏览器。
  • View层
    1. Django完成URL映射后,把请求交给View层的视图函数处理,调用Model层完成数据,如有必要调用Template层响应客户端,如果不需要,直接返回数据。

Session

Session-Cookie机制

  • 网景公司发明了Cookie技术,为了解决浏览器端数据存储问题。
    1. 每一次request请求时,会把此域名相关的Cookie发往服务器端。服务器端也可以使用response中的set-cookie来 设置cookie值。
  • 动态网页技术,也需要知道用户身份,但是HTTP是无状态协议,无法知道。必须提出一种技术,让客户端提交的信息可以表明身份,而且不能更改。这就是Session技术。
  • Session开启后,会为浏览器端设置一个Coolie值,即SessionID.
  • 这个放置SessionID的Cookie是会话级的,浏览器不做持久化存储只放在内存中,并且浏览器关闭自动清除。浏览器端发起一个HTTP请求后,这个SessionID会通过Cookie发到服务器端,服务端就可以通过这个ID查到对应的一个字典结构。如果查无此ID,就为此浏览器重新生成一个SessionID,为它建立一个SessionID和空字典的映射关系。
  • 可以在这个ID对应的Session字典中,存入键值对来保持与当前会话相关的信息。
    1. Session会定期过期清除
    2. Session占用服务端内存
    3. Session如果没有持久化,如果服务程序崩溃,那么所有Session信息丢失
    4. Session可以持久化到数据库中,如果服务程序崩溃,那么可以从数据库中恢复

开启session支持

  • Django可以使用Session
    1. 在settings中,MIDDLEWARE设置中,启用=django.contrib.sessions.middleware.SessionMiddleware=。
    2. 在INSTALLED_APPS设置中,启用=django.contrib.sessions=。它是基于数据库存储的Session。
    3. Session不使用,可以关闭上述配置,以减少开销
    4. 在数据库的表中的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过期时间
    1. 300秒过期
    2. None表示使用全局session过期策略
    3. 0表示会话级session,即浏览器关闭过期
    4. 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/
  • 优点:
    1. 简单:调用接口简单易用
    2. 高可用:客户端、Worker之间连接自动重试,Broker自身支持HA
    3. 快速:单个Celery进程每分钟可用数以百万计的任务
    4. 灵活:Celery的每一个部分都能扩展,或直接使用,或用户自己定义。

常用应用

  • Celery可用支持实时异步任务处理,也支持任务的定时调度。
    1. 异步发邮件
      • cleery执行队列
    2. 间隔半小时同步天气信息等
      • 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中。
  • 添加任务的常用方法
    1. T.delay(arg,kwarg=value) #快捷添加任务的方法
      • T:被app.task装饰的函数
      • args :位置参数
      • kwargs:关键字参数
    2. T.apply_async((args,),{'kwarg':value},countdown=?,expires=?)
      • args 位置参数,是个元组
      • kwargs 关键字参数,是个字典
      • contdown 延迟多久执行,默认秒
      • expires 多久后过期,默认秒
  • 执行任务:如果在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
    1. -A APP, –app App指定app名称,App是模块名
    2. –loglevel 指定日志级别
    3. -n 名称,%n指主机名
    4. –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)
  1. 安装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所在目录)
    1. 定义一个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
      
    2. 修改=__init__.py=文件

      from __future__ import absolute_import, unicode_literals
      from .celery import app as celery_app
      
      __all__ = ('celery_app',)
      
  • 在user应用下

    1. 新建文件=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("发送完成")
      
    2. 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)
      
    3. 以后放在注册函数中发送邮件。
    4. 注意:目前在Python3.7部署时,发邮件可能会报wrap_socket()错。可以使用python3.6版本
    5. 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),
      ]
      
    6. 访问=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
    7. 执行任务,下面命令会从redis中拿任务执行

    celery -A blog -P eventlet worker –loglevel=INFO –concurrency=4 -n worker@%n

Flask框架简单使用

[toc]

快速构建

  1. 在项目根目录下构建:
    • webapp包目录,存放flask代码,包内有=__init__.py=文件
    • templates目录,存放模板文件
    • static目录,存放js,css等静态文件。其下建立js目录,放入jquery、echarts的js文件
    • app.py入口文件
  2. 基本组成
    1. 目录结构如下:
# /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)
  1. 应用:创建出来提供WEB服务的实例,也是wsgi的入口
  2. 视图函数:执行内部代码输出响应的内容
  3. 路由:通过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))
    
    1. 说明:不管是app,还是bluepoint,都是使用自己的root_path和模板路径拼接成模板路径。
    2. 可以通过app或者bluepoint查看=app.jinja_loader.searchpath=模板搜索路径。
emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike