commit
dad0a70726
|
|
@ -147,7 +147,7 @@
|
|||
items = items[:]
|
||||
for i in range(len(items) - 1):
|
||||
swapped = False
|
||||
for j in range(i, len(items) - 1 - i):
|
||||
for j in range(len(items) - 1 - i):
|
||||
if comp(items[j], items[j + 1]):
|
||||
items[j], items[j + 1] = items[j + 1], items[j]
|
||||
swapped = True
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
items = items[:]
|
||||
for i in range(len(items) - 1):
|
||||
swapped = False
|
||||
for j in range(i, len(items) - 1 - i):
|
||||
for j in range(len(items) - 1 - i):
|
||||
if comp(items[j], items[j + 1]):
|
||||
items[j], items[j + 1] = items[j + 1], items[j]
|
||||
swapped = True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## Web前端概述
|
||||
|
||||
> 说明:本文使用的部分插图来自*Jon Duckett*先生的*[HTML and CSS: Design and Build Websites](https://www.amazon.cn/dp/1118008189/ref=sr_1_5?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&keywords=html+%26+css&qid=1554609325&s=gateway&sr=8-5)*一书,这是一本非常棒的前端入门书,有兴趣的读者可以在亚马逊或者其他网站上找到该书的购买链接。
|
||||
> **说明**:本文使用的部分插图来自*Jon Duckett*先生的*[HTML and CSS: Design and Build Websites](https://www.amazon.cn/dp/1118008189/ref=sr_1_5?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&keywords=html+%26+css&qid=1554609325&s=gateway&sr=8-5)*一书,这是一本非常棒的前端入门书,有兴趣的读者可以在亚马逊或者其他网站上找到该书的购买链接。
|
||||
|
||||
### HTML简史
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## 玩转Linux操作系统
|
||||
|
||||
> 说明:本文中对Linux命令的讲解都是基于名为CentOS的Linux发行版本,我自己使用的是阿里云服务器,系统版本为CentOS Linux release 7.6.1810。不同的Linux发行版本在Shell命令和工具程序上会有一些差别,但是这些差别是很小的。
|
||||
> **说明**:本文中对Linux命令的讲解都是基于名为CentOS的Linux发行版本,我自己使用的是阿里云服务器,系统版本为CentOS Linux release 7.6.1810。不同的Linux发行版本在Shell命令和工具程序上会有一些差别,但是这些差别是很小的。
|
||||
|
||||
### 操作系统发展史
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ NoSQL数据库按照其存储类型可以大致分为以下几类:
|
|||
| 图数据库 | Neo4J<br>FlockDB<br>JanusGraph | 使用图结构进行语义查询的数据库,它使用节点、边和属性来表示和存储数据。图数据库从设计上,就可以简单快速的检索难以在关系系统中建模的复杂层次结构。 |
|
||||
| 对象数据库 | db4o<br>Versant | 通过类似面向对象语言的语法操作数据库,通过对象的方式存取数据。 |
|
||||
|
||||
> 说明:想了解更多的NoSQL数据库,可以访问<http://nosql-database.org/>。
|
||||
> **说明**:想了解更多的NoSQL数据库,可以访问<http://nosql-database.org/>。
|
||||
|
||||
### Redis概述
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ Redis有着非常丰富的数据类型,也有很多的命令来操作这些数
|
|||
|
||||

|
||||
|
||||
> 说明:上面的插图来自付磊和张益军先生编著的《Redis开发与运维》一书。
|
||||
> **说明**:上面的插图来自付磊和张益军先生编著的《Redis开发与运维》一书。
|
||||
|
||||
```Shell
|
||||
127.0.0.1:6379> set username admin
|
||||
|
|
@ -297,7 +297,7 @@ MongoDB是2009年问世的一个面向文档的数据库管理系统,由C++语
|
|||
|
||||
MongoDB将数据存储为一个文档,一个文档由一系列的“键值对”组成,其文档类似于JSON对象,但是MongoDB对JSON进行了二进制处理(能够更快的定位key和value),因此其文档的存储格式称为BSON。关于JSON和BSON的差别大家可以看看MongoDB官方网站的文章[《JSON and BSON》](https://www.mongodb.com/json-and-bson)。
|
||||
|
||||
目前,MongoDB已经提供了对Windows、MacOS、Linux、Solaris等多个平台的支持,而且也提供了多种开发语言的驱动程序,Python当然是其中之一。
|
||||
目前,MongoDB已经提供了对Windows、macOS、Linux、Solaris等多个平台的支持,而且也提供了多种开发语言的驱动程序,Python当然是其中之一。
|
||||
|
||||
#### MongoDB的安装和配置
|
||||
|
||||
|
|
@ -320,7 +320,7 @@ mongod --bind_ip 172.18.61.250
|
|||
2018-06-03T18:03:28.945+0800 I NETWORK [initandlisten] waiting for connections on port 27017
|
||||
```
|
||||
|
||||
> 说明:上面的操作中,export命令是设置PATH环境变量,这样可以在任意路径下执行mongod来启动MongoDB服务器。MongoDB默认保存数据的路径是/data/db目录,为此要提前创建该目录。此外,在使用mongod启动MongoDB服务器时,--bind_ip参数用来将服务绑定到指定的IP地址,也可以用--port参数来指定端口,默认端口为27017。
|
||||
> **说明**:上面的操作中,export命令是设置PATH环境变量,这样可以在任意路径下执行mongod来启动MongoDB服务器。MongoDB默认保存数据的路径是/data/db目录,为此要提前创建该目录。此外,在使用mongod启动MongoDB服务器时,--bind_ip参数用来将服务绑定到指定的IP地址,也可以用--port参数来指定端口,默认端口为27017。
|
||||
|
||||
#### MongoDB基本概念
|
||||
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ Teacher.objects.filter(subject__name__contains='全栈')
|
|||
```Python
|
||||
from django.contrib import admin
|
||||
|
||||
from polls.models import Subject, Teacher
|
||||
from polls.models import Subject, Teacher
|
||||
|
||||
|
||||
class SubjectModelAdmin(admin.ModelAdmin):
|
||||
|
|
@ -566,7 +566,7 @@ from polls.models import Subject, Teacher
|
|||
13. 定义`__str__`方法。
|
||||
14. 不要将数据文件放在同一个目录中。
|
||||
|
||||
> 说明:以上内容来自于STEELKIWI网站的[*Best Practice working with Django models in Python*](https://steelkiwi.com/blog/best-practices-working-django-models-python/),有兴趣的小伙伴可以阅读原文。
|
||||
> **说明**:以上内容来自于STEELKIWI网站的[*Best Practice working with Django models in Python*](https://steelkiwi.com/blog/best-practices-working-django-models-python/),有兴趣的小伙伴可以阅读原文。
|
||||
|
||||
#### 模型定义参考
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@
|
|||
报表 = 多样的格式 + 动态的数据
|
||||
```
|
||||
|
||||
有很多的三方库支持在Python程序中写Excel文件,包括[xlwt](<https://xlwt.readthedocs.io/en/latest/>)、[xlwings](<https://docs.xlwings.org/en/latest/quickstart.html>)、[openpyxl](<https://openpyxl.readthedocs.io/en/latest/>)、[xlswriter](<https://xlsxwriter.readthedocs.io/>)、[pandas](<http://pandas.pydata.org/>)等,其中的xlwt虽然只支持写xls格式的Excel文件,但在性能方面的表现还是不错的。下面我们就以xlwt为例,来演示如何在Django项目中导出Excel报表,例如导出一个包含所有老师信息的Excel表格。
|
||||
有很多的三方库支持在Python程序中写Excel文件,包括[`xlwt`](<https://xlwt.readthedocs.io/en/latest/>)、[`xlwings`](<https://docs.xlwings.org/en/latest/quickstart.html>)、[`openpyxl`](<https://openpyxl.readthedocs.io/en/latest/>)、[`xlswriter`](<https://xlsxwriter.readthedocs.io/>)、[`pandas`](<http://pandas.pydata.org/>)等,其中的xlwt虽然只支持写xls格式的Excel文件,但在性能方面的表现还是不错的。下面我们就以`xlwt`为例,来演示如何在Django项目中导出Excel报表。
|
||||
|
||||
安装`xlwt`。
|
||||
|
||||
```Bash
|
||||
pip install xlwt
|
||||
```
|
||||
|
||||
导出包含所有老师信息的Excel表格的视图函数。
|
||||
|
||||
```Python
|
||||
def export_teachers_excel(request):
|
||||
|
|
@ -46,15 +54,15 @@ def export_teachers_excel(request):
|
|||
|
||||
```Python
|
||||
urlpatterns = [
|
||||
# 此处省略上面的代码
|
||||
|
||||
path('excel/', views.export_teachers_excel),
|
||||
# 此处省略下面的代码
|
||||
|
||||
]
|
||||
```
|
||||
|
||||
### 导出PDF报表
|
||||
|
||||
在Django项目中,如果需要导出PDF报表,可以借助三方库reportlab来生成PDF文件的内容,再将文件的二进制数据输出给浏览器并指定MIME类型为`application/pdf`,具体的代码如下所示。
|
||||
在Django项目中,如果需要导出PDF报表,可以借助三方库`reportlab`来生成PDF文件的内容,再将文件的二进制数据输出给浏览器并指定MIME类型为`application/pdf`,具体的代码如下所示。
|
||||
|
||||
```Python
|
||||
def export_pdf(request: HttpRequest) -> HttpResponse:
|
||||
|
|
@ -70,7 +78,7 @@ def export_pdf(request: HttpRequest) -> HttpResponse:
|
|||
return resp
|
||||
```
|
||||
|
||||
关于如何用reportlab定制PDF报表的内容,可以参考reportlab的[官方文档](https://www.reportlab.com/docs/reportlab-userguide.pdf)。
|
||||
关于如何用`reportlab`定制PDF报表的内容,可以参考reportlab的[官方文档](https://www.reportlab.com/docs/reportlab-userguide.pdf)。
|
||||
|
||||
### 生成前端统计图表
|
||||
|
||||
|
|
@ -89,7 +97,7 @@ def get_teachers_data(request):
|
|||
|
||||
```Python
|
||||
urlpatterns = [
|
||||
path('teachers_data/', views.export_teachers_excel),
|
||||
path('teachers_data/', views.get_teachers_data),
|
||||
]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -169,9 +169,9 @@ LOGGING = {
|
|||
|
||||
4. 在配置好Django-Debug-Toolbar之后,页面右侧会看到一个调试工具栏,如下图所示,上面包括了如前所述的各种调试信息,包括执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等,查看起来非常的方便。
|
||||
|
||||

|
||||

|
||||
|
||||
#### 优化ORM代码
|
||||
### 优化ORM代码
|
||||
|
||||
在配置了日志或Django-Debug-Toolbar之后,我们可以查看一下之前将老师数据导出成Excel报表的视图函数执行情况,这里我们关注的是ORM框架生成的SQL查询到底是什么样子的,相信这里的结果会让你感到有一些意外。执行`Teacher.objects.all()`之后我们可以注意到,在控制台看到的或者通过Django-Debug-Toolbar输出的SQL是下面这样的:
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject
|
|||
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
|
||||
```
|
||||
|
||||
这里的问题通常被称为“1+N查询”(或“N+1查询”),原本获取老师的数据只需要一条SQL,但是由于老师关联了学科,当我们查询到N条老师的数据时,Django的ORM框架又向数据库发出了N条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力,如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法,这一点也很容易做到,相信大家已经想到怎么做了。是的,我们可以使用连接查询,但是在使用Django的ORM框架时如何做到这一点呢?对于多对一关联(如投票应用中的老师和学科),我们可以使用`QuerySet`的用`select_related()`方法来加载关联对象;而对于多对多关联(如电商网站中的订单和商品),我们可以使用`prefetch_related()`方法来加载关联对象。
|
||||
这里的问题通常被称为“1+N查询”(有的地方也将其称之为“N+1查询”),原本获取老师的数据只需要一条SQL,但是由于老师关联了学科,当我们查询到`N`条老师的数据时,Django的ORM框架又向数据库发出了`N`条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力,如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法,这一点也很容易做到,相信大家已经想到怎么做了。是的,我们可以使用连接查询,但是在使用Django的ORM框架时如何做到这一点呢?对于多对一关联(如投票应用中的老师和学科),我们可以使用`QuerySet`的用`select_related()`方法来加载关联对象;而对于多对多关联(如电商网站中的订单和商品),我们可以使用`prefetch_related()`方法来加载关联对象。
|
||||
|
||||
在导出老师Excel报表的视图函数中,我们可以按照下面的方式优化代码。
|
||||
|
||||
|
|
@ -202,15 +202,13 @@ queryset = Teacher.objects.all().only('name', 'good_count', 'bad_count')
|
|||
当然,如果要统计出每个学科的老师好评和差评的平均数,利用Django的ORM框架也能够做到,代码如下所示:
|
||||
|
||||
```Python
|
||||
queryset = Teacher.objects.values('subject').annotate(
|
||||
good=Avg('good_count'), bad=Avg('bad_count'))
|
||||
queryset = Teacher.objects.values('subject').annotate(good=Avg('good_count'), bad=Avg('bad_count'))
|
||||
```
|
||||
|
||||
这里获得的`QuerySet`中的元素是字典对象,每个字典中有三组键值对,分别是代表学科编号的`subject`、代表好评数的`good`和代表差评数的`bad`。如果想要获得学科的名称而不是编号,可以按照如下所示的方式调整代码:
|
||||
|
||||
```Python
|
||||
queryset = Teacher.objects.values('subject__name').annotate(
|
||||
good=Avg('good_count'), bad=Avg('bad_count'))
|
||||
queryset = Teacher.objects.values('subject__name').annotate(good=Avg('good_count'), bad=Avg('bad_count'))
|
||||
```
|
||||
|
||||
可见,Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。
|
||||
|
|
|
|||
|
|
@ -51,9 +51,7 @@ from django.http import JsonResponse
|
|||
from django.shortcuts import redirect
|
||||
|
||||
# 需要登录才能访问的资源路径
|
||||
LOGIN_REQUIRED_URLS = {
|
||||
'/praise/', '/criticize/', '/excel/', '/teachers_data/',
|
||||
}
|
||||
LOGIN_REQUIRED_URLS = {'/praise/', '/criticize/', '/excel/', '/teachers_data/'}
|
||||
|
||||
|
||||
def check_login_middleware(get_resp):
|
||||
|
|
@ -76,7 +74,37 @@ def check_login_middleware(get_resp):
|
|||
return wrapper
|
||||
```
|
||||
|
||||
修改配置文件,激活中间件使其生效。
|
||||
当然,我们也可以定义一个类来充当装饰器,如果类中有`__call__`魔术方法,这个类的对象就像函数一样可调用,所以下面是另一种实现中间件的方式,道理跟上面的代码完全一样。
|
||||
|
||||
还有一种基于类实现中间件的方式,这种方式在较新版本的Django中已经不推荐使用了,但是大家接触到的代码中,仍然有可能遇到这种写法,大致的代码如下所示。
|
||||
|
||||
```Python
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
|
||||
class MyMiddleware(MiddlewareMixin):
|
||||
|
||||
def process_request(self, request):
|
||||
pass
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
pass
|
||||
|
||||
def process_template_response(self, request, response):
|
||||
pass
|
||||
|
||||
def process_response(self, request, response):
|
||||
pass
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
pass
|
||||
```
|
||||
|
||||
上面类中的五个方法都是中间件的钩子函数,分别在收到用户请求、进入视图函数之前、渲染模板、返回响应和出现异常的时候被回调。当然,写不写这些方法是根据中间件的需求来确定的,并不是所有的场景都需要重写五个方法,下面的图相信能够帮助大家理解这种写法。
|
||||
|
||||

|
||||
|
||||
写好中间件代码后,需要修改配置文件来激活中间件使其生效。
|
||||
|
||||
```Python
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -95,3 +123,4 @@ MIDDLEWARE = [
|
|||
注意上面这个中间件列表中元素的顺序,当收到来自用户的请求时,中间件按照从上到下的顺序依次执行,这行完这些中间件以后,请求才会最终到达视图函数。当然,在这个过程中,用户的请求可以被拦截,就像上面我们自定义的中间件那样,如果用户在没有登录的情况下访问了受保护的资源,中间件会将请求直接重定向到登录页,后面的中间件和视图函数将不再执行。在响应用户请求的过程中,上面的中间件会按照从下到上的顺序依次执行,这样的话我们还可以对响应做进一步的处理。
|
||||
|
||||
中间件执行的顺序是非常重要的,对于有依赖关系的中间件必须保证被依赖的中间件要置于依赖它的中间件的前面,就好比我们刚才自定义的中间件要放到`SessionMiddleware`的后面,因为我们要依赖这个中间件为请求绑定的`session`对象才能判定用户是否登录。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
## 前后端分离开发入门
|
||||
|
||||
在传统的Web应用开发中,大多数的程序员会将浏览器作为前后端的分界线。将浏览器中为用户进行页面展示的部分称之为前端,而将运行在服务器,为前端提供业务逻辑和数据准备的所有代码统称为后端。所谓前后端分离的开发,就是前后端工程师约定好数据交互接口,并行的进行开发和测试,后端只提供数据,不负责将数据渲染到页面上,前端通过HTTP请求获取数据并负责将数据渲染到页面上,这个工作是交给浏览器中的JavaScript代码来完成。
|
||||
在传统的Web应用开发中,大多数的程序员会将浏览器作为前后端的分界线。将浏览器中为用户进行页面展示的部分称之为前端,而将运行在服务器为前端提供业务逻辑和数据准备的所有代码统称为后端。所谓前后端分离的开发,就是前后端工程师约定好数据交互接口,并行的进行开发和测试,后端只提供数据,不负责将数据渲染到页面上,前端通过HTTP请求获取数据并负责将数据渲染到页面上,这个工作是交给浏览器中的JavaScript代码来完成。
|
||||
|
||||
使用前后端分离开发有诸多的好处,下面我们简要的说下这些好处:
|
||||
|
||||
|
|
@ -30,9 +30,9 @@ def show_subjects(request):
|
|||
|
||||
上面的代码中,我们通过循环遍历查询学科得到的`QuerySet`对象,将每个学科的数据处理成一个字典,在将字典保存在名为`subjects`的列表容器中,最后利用`JsonResponse`完成对列表的序列化,向浏览器返回JSON格式的数据。由于`JsonResponse`序列化的是一个列表而不是字典,所以需要指定`safe`参数的值为`False`才能完成对`subjects`的序列化,否则会产生`TypeError`异常。
|
||||
|
||||
可能大家已经发现了,自己写代码将一个对象转成字典是比较麻烦的,如果对象的属性很多而且某些属性又关联到一个比较复杂的对象时,情况会变得更加糟糕。为此我们可以使用一个名为bpmappers的三方库来简化将对象转成字典的操作,这个三方库本身也提供了对Django框架的支持。
|
||||
可能大家已经发现了,自己写代码将一个对象转成字典是比较麻烦的,如果对象的属性很多而且某些属性又关联到一个比较复杂的对象时,情况会变得更加糟糕。为此我们可以使用一个名为`bpmappers`的三方库来简化将对象转成字典的操作,这个三方库本身也提供了对Django框架的支持。
|
||||
|
||||
安装三方库bpmappers。
|
||||
安装三方库`bpmappers`。
|
||||
|
||||
```Shell
|
||||
pip install bpmappers
|
||||
|
|
@ -63,15 +63,24 @@ def show_subjects(request):
|
|||
return JsonResponse(subjects, safe=False)
|
||||
```
|
||||
|
||||
配置URL映射,然后访问该接口,可以得到如下所示的JSON格式数据。
|
||||
配置URL映射。
|
||||
|
||||
```Python
|
||||
urlpatterns = [
|
||||
|
||||
path('api/subjects/', show_subjects),
|
||||
|
||||
]
|
||||
```
|
||||
|
||||
然后访问该接口,可以得到如下所示的JSON格式数据。
|
||||
|
||||
```JSON
|
||||
[
|
||||
{
|
||||
"no": 101,
|
||||
"no": 1,
|
||||
"name": "Python全栈+人工智能",
|
||||
"intro": "Python是一种计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的、大型项目的开发。",
|
||||
"create_date": "2017-08-01",
|
||||
"is_hot": true
|
||||
},
|
||||
// 此处省略下面的内容
|
||||
|
|
@ -92,7 +101,7 @@ class SubjectMapper(ModelMapper):
|
|||
|
||||
class Meta:
|
||||
model = Subject
|
||||
exclude = ('create_date', 'is_hot')
|
||||
exclude = ('is_hot', )
|
||||
```
|
||||
|
||||
再次查看学科接口返回的JSON数据。
|
||||
|
|
@ -109,11 +118,11 @@ class SubjectMapper(ModelMapper):
|
|||
]
|
||||
```
|
||||
|
||||
关于bpmappers详细的使用指南,请参考它的[官方文档](<https://bpmappers.readthedocs.io/en/stable/>),这个官方文档是用日语书写的,可以使用浏览器的翻译功能将它翻译成你熟悉的语言即可。
|
||||
关于`bpmappers`详细的使用指南,请参考它的[官方文档](<https://bpmappers.readthedocs.io/en/stable/>),这个官方文档是用日语书写的,可以使用浏览器的翻译功能将它翻译成你熟悉的语言即可。
|
||||
|
||||
### 使用Vue.js渲染页面
|
||||
|
||||
关于Vue.js的知识,我们在第21天到第30天的内容中已经介绍过了,这里我们不再进行赘述。如果希望全面的了解和学习Vue.js,建议阅读它的[官方教程](<https://cn.vuejs.org/v2/guide/>)或者在[YouTube](<https://www.youtube.com/>)上搜索Vue.js的新手教程(Crash Course)进行学习。
|
||||
接下来我们通过前端框架Vue.js来实现页面的渲染。如果希望全面的了解和学习Vue.js,建议阅读它的[官方教程](<https://cn.vuejs.org/v2/guide/>)或者在[YouTube](<https://www.youtube.com/>)上搜索Vue.js的新手教程(Vue.js Crash Course)进行学习。
|
||||
|
||||
重新改写subjects.html页面,使用Vue.js来渲染页面。
|
||||
|
||||
|
|
@ -122,36 +131,52 @@ class SubjectMapper(ModelMapper):
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>学科</title>
|
||||
<title>学科信息</title>
|
||||
<style>
|
||||
#container {
|
||||
width: 80%;
|
||||
margin: 10px auto;
|
||||
}
|
||||
#main>dl>dt {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
#main>dl>dd {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: darkcyan;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>所有学科</h1>
|
||||
<div id="container">
|
||||
<h1>扣丁学堂所有学科</h1>
|
||||
<hr>
|
||||
<div id="app">
|
||||
<div v-for="subject in subjects">
|
||||
<h3>
|
||||
<a :href="getTeachersHref(subject.no)">{{ subject.name }}</a>
|
||||
<img v-if="subject.isHot" src="/static/images/hot.png" width="32">
|
||||
</h3>
|
||||
<p>{{ subject.intro }}</p>
|
||||
<div id="main">
|
||||
<dl v-for="subject in subjects">
|
||||
<dt>
|
||||
<a :href="'/static/html/teachers.html?sno=' + subject.no">{{ subject.name }}</a>
|
||||
<img v-if="subject.is_hot" src="/static/images/hot-icon-small.png">
|
||||
</dt>
|
||||
<dd>{{ subject.intro }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js"></script>
|
||||
<script>
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
let app = new Vue({
|
||||
el: '#main',
|
||||
data: {
|
||||
subjects: []
|
||||
},
|
||||
created() {
|
||||
fetch('/subjects/')
|
||||
fetch('/api/subjects/')
|
||||
.then(resp => resp.json())
|
||||
.then(json => this.subjects = json)
|
||||
},
|
||||
methods: {
|
||||
getTeachersHref(sno) {
|
||||
return `/static/teachers.html/?sno=${sno}`
|
||||
}
|
||||
.then(json => {
|
||||
this.subjects = json
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -161,4 +186,4 @@ class SubjectMapper(ModelMapper):
|
|||
|
||||
前后端分离的开发需要将前端页面作为静态资源进行部署,项目实际上线的时候,我们会对整个Web应用进行动静分离,静态资源通过Nginx或Apache服务器进行部署,生成动态内容的Python程序部署在uWSGI或者Gunicorn服务器上,对动态内容的请求由Nginx或Apache路由到uWSGI或Gunicorn服务器上。
|
||||
|
||||
在开发阶段,我们通常会使用Django自带的测试服务器,如果要尝试前后端分离,可以先将静态页面放在之前创建的放静态资源的目录下,具体的做法可以参考[项目完整代码]()。
|
||||
在开发阶段,我们通常会使用Django自带的测试服务器,如果要尝试前后端分离,可以先将静态页面放在之前创建的放静态资源的目录下,具体的做法可以参考[项目完整代码](https://gitee.com/jackfrued/django19062)。
|
||||
|
|
@ -1,4 +1,387 @@
|
|||
## RESTful架构和DRF入门
|
||||
|
||||
把软件(Software)、平台(Platform)、基础设施(Infrastructure)做成服务(Service)是很多IT企业都一直在做的事情,这就是大家经常听到的SasS(软件即服务)、PasS(平台即服务)和IasS(基础设置即服务)。实现面向服务的架构(SOA)有诸多的方式,包括RPC(远程过程调用)、Web Service、REST等,在技术层面上,SOA是一种**抽象的、松散耦合的粗粒度软件架构**;在业务层面上,SOA的核心概念是“**重用**”和“**互操作**”,它将系统资源整合成可操作的、标准的服务,使得这些资源能够被重新组合和应用。在实现SOA的诸多方案中,REST被认为是最适合互联网应用的架构,符合REST规范的架构也经常被称作RESTful架构。
|
||||
|
||||
### REST概述
|
||||
|
||||
REST这个词,是**Roy Thomas Fielding**在他2000年的博士论文中提出的,Roy是HTTP协议(1.0和1.1版)的主要设计者、Apache服务器软件主要作者、Apache基金会第一任主席。在他的博士论文中,Roy把他对互联网软件的架构原则定名为REST,即**RE**presentational **S**tate **T**ransfer的缩写,中文通常翻译为“**表现层状态转移**”或“**表述状态转移**”。
|
||||
|
||||
这里的“表现层”其实指的是“资源”的“表现层”。所谓资源,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲或一种服务。我们可以用一个URI(统一资源定位符)指向资源,要获取到这个资源,访问它的URI即可,URI就是资源在互联网上的唯一标识。资源可以有多种外在表现形式。我们把资源具体呈现出来的形式,叫做它的“表现层”。比如,文本可以用`text/plain`格式表现,也可以用`text/html`格式、`text/xml`格式、`application/json`格式表现,甚至可以采用二进制格式;图片可以用`image/jpeg`格式表现,也可以用`image/png`格式表现。URI只代表资源的实体,不代表它的表现形式。严格地说,有些网址最后的`.html`后缀名是不必要的,因为这个后缀名表示格式,属于“表现层”范畴,而URI应该只代表“资源”的位置,它的具体表现形式,应该在HTTP请求的头信息中用`Accept`和`Content-Type`字段指定,这两个字段才是对“表现层”的描述。
|
||||
|
||||
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。Web应用通常使用HTTP作为其通信协议,客户端想要操作服务器,必须通过HTTP请求,让服务器端发生“状态转移”,而这种转移是建立在表现层之上的,所以就是“表现层状态转移”。客户端通过HTTP的动词GET、POST、PUT(或PATCH)、DELETE,分别对应对资源的四种基本操作,其中GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT(或PATCH)用来更新资源,DELETE用来删除资源。
|
||||
|
||||
简单的说RESTful架构就是:“每一个URI代表一种资源,客户端通过四个HTTP动词,对服务器端资源进行操作,实现资源的表现层状态转移”。
|
||||
|
||||
我们在设计Web应用时,如果需要向客户端提供资源,就可以使用REST风格的URI,这是实现RESTful架构的第一步。当然,真正的RESTful架构并不只是URI符合REST风格,更为重要的是“无状态”和“幂等性”两个词,我们在后面的课程中会为大家阐述这两点。下面的例子给出了一些符合REST风格的URI,供大家在设计URI时参考。
|
||||
|
||||
| 请求方法(HTTP动词) | URI | 解释 |
|
||||
| -------------------- | -------------------------- | -------------------------------------------- |
|
||||
| **GET** | `/students/` | 获取所有学生 |
|
||||
| **POST** | `/students/` | 新建一个学生 |
|
||||
| **GET** | `/students/ID/` | 获取指定ID的学生信息 |
|
||||
| **PUT** | `/students/ID/` | 更新指定ID的学生信息(提供该学生的全部信息) |
|
||||
| **PATCH** | `/students/ID/` | 更新指定ID的学生信息(提供该学生的部分信息) |
|
||||
| **DELETE** | `/students/ID/` | 删除指定ID的学生信息 |
|
||||
| **GET** | `/students/ID/friends/` | 列出指定ID的学生的所有朋友 |
|
||||
| **DELETE** | `/students/ID/friends/ID/` | 删除指定ID的学生的指定ID的朋友 |
|
||||
|
||||
### DRF使用入门
|
||||
|
||||
在Django项目中,如果要实现REST架构,即将网站的资源发布成REST风格的API接口,可以使用著名的三方库`djangorestframework` ,我们通常将其简称为DRF。
|
||||
|
||||
#### 安装和配置DRF
|
||||
|
||||
安装DRF。
|
||||
|
||||
```Shell
|
||||
pip install djangorestframework
|
||||
```
|
||||
|
||||
配置DRF。
|
||||
|
||||
```Python
|
||||
INSTALLED_APPS = [
|
||||
|
||||
'rest_framework',
|
||||
|
||||
]
|
||||
|
||||
# 下面的配置根据项目需要进行设置
|
||||
REST_FRAMEWORK = {
|
||||
# 配置默认页面大小
|
||||
# 'PAGE_SIZE': 10,
|
||||
# 配置默认的分页类
|
||||
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
# 配置异常处理器
|
||||
# 'EXCEPTION_HANDLER': '...',
|
||||
# 配置默认解析器
|
||||
# 'DEFAULT_PARSER_CLASSES': (
|
||||
# 'rest_framework.parsers.JSONParser',
|
||||
# 'rest_framework.parsers.FormParser',
|
||||
# 'rest_framework.parsers.MultiPartParser',
|
||||
# ),
|
||||
# 配置默认限流类
|
||||
# 'DEFAULT_THROTTLE_CLASSES': (
|
||||
# '...'
|
||||
# ),
|
||||
# 配置默认授权类
|
||||
# 'DEFAULT_PERMISSION_CLASSES': (
|
||||
# '...',
|
||||
# ),
|
||||
# 配置默认认证类
|
||||
# 'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
# '...',
|
||||
# ),
|
||||
}
|
||||
```
|
||||
|
||||
#### 编写序列化器
|
||||
|
||||
前后端分离的开发需要后端为前端、移动端提供API数据接口,而API接口通常情况下都是返回JSON格式的数据,这就需要对模型对象进行序列化处理。DRF中封装了`Serializer`类和`ModelSerializer`类用于实现序列化操作,通过继承`Serializer`类或`ModelSerializer`类,我们可以自定义序列化器,用于将对象处理成字典,代码如下所示。
|
||||
|
||||
```Python
|
||||
class SubjectSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Subject
|
||||
fields = '__all__'
|
||||
```
|
||||
|
||||
上面的代码直接继承了`ModelSerializer`,通过`Meta`类的`model`属性指定要序列化的模型以及`fields`属性指定需要序列化的模型字段,稍后我们就可以在视图函数中使用该类来实现对`Subject`模型的序列化。
|
||||
|
||||
#### 编写视图函数
|
||||
|
||||
DRF框架支持两种实现数据接口的方式,一种是FBV(基于函数的视图),另一种是CBV(基于类的视图)。我们先看看FBV的方式如何实现数据接口,代码如下所示。
|
||||
|
||||
```Python
|
||||
@api_view(('GET', ))
|
||||
def show_subjects(request: HttpRequest) -> HttpResponse:
|
||||
subjects = Subject.objects.all().order_by('no')
|
||||
# 创建序列化器对象并指定要序列化的模型
|
||||
serializer = SubjectSerializer(subjects, many=True)
|
||||
# 通过序列化器的data属性获得模型对应的字典并通过创建Response对象返回JSON格式的数据
|
||||
return Response(serializer.data)
|
||||
```
|
||||
|
||||
对比上一个章节的使用`bpmapper`实现模型序列化的代码,使用DRF的代码更加简单明了,而且DRF本身自带了一套页面,可以方便我们查看我们使用DRF定制的数据接口,如下图所示。
|
||||
|
||||

|
||||
|
||||
直接使用上一节写好的页面,就可以通过Vue.js把上面接口提供的学科数据渲染并展示出来,此处不再进行赘述。
|
||||
|
||||
#### 实现老师信息数据接口
|
||||
|
||||
编写序列化器。
|
||||
|
||||
```Python
|
||||
class SubjectSimpleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Subject
|
||||
fields = ('no', 'name')
|
||||
|
||||
|
||||
class TeacherSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Teacher
|
||||
exclude = ('subject', )
|
||||
```
|
||||
|
||||
编写视图函数。
|
||||
|
||||
```Python
|
||||
@api_view(('GET', ))
|
||||
def show_teachers(request: HttpRequest) -> HttpResponse:
|
||||
try:
|
||||
sno = int(request.GET.get('sno'))
|
||||
subject = Subject.objects.only('name').get(no=sno)
|
||||
teachers = Teacher.objects.filter(subject=subject).defer('subject').order_by('no')
|
||||
subject_seri = SubjectSimpleSerializer(subject)
|
||||
teacher_seri = TeacherSerializer(teachers, many=True)
|
||||
return Response({'subject': subject_seri.data, 'teachers': teacher_seri.data})
|
||||
except (TypeError, ValueError, Subject.DoesNotExist):
|
||||
return Response(status=404)
|
||||
```
|
||||
|
||||
配置URL映射。
|
||||
|
||||
```Python
|
||||
urlpatterns = [
|
||||
|
||||
path('api/teachers/', show_teachers),
|
||||
|
||||
]
|
||||
```
|
||||
|
||||
通过Vue.js渲染页面。
|
||||
|
||||
```Python
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>老师信息</title>
|
||||
<style>
|
||||
#container {
|
||||
width: 80%;
|
||||
margin: 10px auto;
|
||||
}
|
||||
.teacher {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed gray;
|
||||
overflow: auto;
|
||||
}
|
||||
.teacher>div {
|
||||
float: left;
|
||||
}
|
||||
.photo {
|
||||
height: 140px;
|
||||
border-radius: 75px;
|
||||
overflow: hidden;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.info {
|
||||
width: 75%;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.info div {
|
||||
clear: both;
|
||||
margin: 5px 10px;
|
||||
}
|
||||
.info span {
|
||||
margin-right: 25px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: darkcyan;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<h1>{{ subject.name }}学科的老师信息</h1>
|
||||
<hr>
|
||||
<h2 v-if="teachers.length == 0">暂无该学科老师信息</h2>
|
||||
<div class="teacher" v-for="teacher in teachers">
|
||||
<div class="photo">
|
||||
<img :src="'/static/images/' + teacher.photo" height="140" alt="">
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>
|
||||
<span><strong>姓名:{{ teacher.name }}</strong></span>
|
||||
<span>性别:{{ teacher.sex | maleOrFemale }}</span>
|
||||
<span>出生日期:{{ teacher.birth }}</span>
|
||||
</div>
|
||||
<div class="intro">{{ teacher.intro }}</div>
|
||||
<div class="comment">
|
||||
<a href="" @click.prevent="praise(teacher)">好评</a> (<strong>{{ teacher.good_count }}</strong>)
|
||||
|
||||
<a href="" @click.prevent="criticize(teacher)">差评</a> (<strong>{{ teacher.bad_count }}</strong>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/">返回首页</a>
|
||||
</div>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js"></script>
|
||||
<script>
|
||||
let app = new Vue({
|
||||
el: '#container',
|
||||
data: {
|
||||
subject: null,
|
||||
teachers: []
|
||||
},
|
||||
created() {
|
||||
fetch('/api/teachers/' + location.search)
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
this.subject = json.subject
|
||||
this.teachers = json.teachers
|
||||
})
|
||||
},
|
||||
filters: {
|
||||
maleOrFemale(sex) {
|
||||
return sex? '男': '女'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
praise(teacher) {
|
||||
fetch('/praise/?tno=' + teacher.no)
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
if (json.code === 20000) {
|
||||
teacher.good_count = json.count
|
||||
} else {
|
||||
alert(json.mesg)
|
||||
}
|
||||
})
|
||||
},
|
||||
criticize(teacher) {
|
||||
fetch('/criticize/?tno=' + teacher.no)
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
if (json.code === 20000) {
|
||||
teacher.bad_count = json.count
|
||||
} else {
|
||||
alert(json.mesg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 前后端分离下的用户登录
|
||||
|
||||
之前我们提到过, HTTP是无状态的,一次请求结束连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。但是对于一个Web应用而言,它是需要有状态管理的,这样才能让服务器知道HTTP请求来自哪个用户,从而判断是否允许该用户请求以及为用户提供更好的服务,这个过程就是常说的**会话管理**。
|
||||
|
||||
之前我们做会话管理(用户跟踪)的方法是:用户登录成功后,在服务器端通过一个session对象保存用户相关数据,然后把session对象的ID写入浏览器的cookie中;下一次请求时,HTTP请求头中携带cookie的数据,服务器从HTTP请求头读取cookie中的sessionid,根据这个标识符找到对应的session对象,这样就能够获取到之前保存在session中的用户数据。我们刚才说过,REST架构是最适合互联网应用的架构,它强调了HTTP的无状态性,这样才能保证应用的水平扩展能力(当并发访问量增加时,可以通过增加新的服务器节点来为系统扩容)。显然,基于session实现用户跟踪的方式需要服务器保存session对象,在做水平扩展增加新的服务器节点时,需要复制和同步session对象,这显然是非常麻烦的。解决这个问题有两种方案,一种是架设缓存服务器(如Redis),让多个服务器节点共享缓存服务并将session对象直接置于缓存服务器中;另一种方式放弃基于session的用户跟踪,使用**基于token的用户跟踪**。
|
||||
|
||||
基于token的用户跟踪是在用户登录成功后,为用户生成身份标识并保存在浏览器本地存储(localStorage、sessionStorage、cookie等)中,这样的话服务器不需要保存用户状态,从而可以很容易的做到水平扩展。基于token的用户跟踪具体流程如下:
|
||||
|
||||
1. 用户登录时,如果登录成功就按照某种方式为用户生成一个令牌(token),该令牌中通常包含了用户标识、过期时间等信息而且需要加密并生成指纹(避免伪造或篡改令牌),服务器将令牌返回给前端;
|
||||
2. 前端获取到服务器返回的token,保存在浏览器本地存储中(可以保存在`localStorage`或`sessionStorage`中,对于使用Vue.js的前端项目来说,还可以通过Vuex进行状态管理);
|
||||
3. 对于使用了前端路由的项目来说,前端每次路由跳转,可以先判断`localStroage`中有无token,如果没有则跳转到登录页;
|
||||
4. 每次请求后端数据接口,在HTTP请求头里携带token;后端接口判断请求头有无token,如果没有token以及token是无效的或过期的,服务器统一返回401;
|
||||
5. 如果前端收到HTTP响应状态码401,则重定向到登录页面。
|
||||
|
||||
通过上面的描述,相信大家已经发现了,基于token的用户跟踪最为关键是在用户登录成功时,要为用户生成一个token作为用户的身份标识。生成token的方法很多,其中一种比较成熟的解决方案是使用JSON Web Token。
|
||||
|
||||
#### JWT概述
|
||||
|
||||
JSON Web Token通常简称为JWT,它是一种开放标准(RFC 7519)。随着RESTful架构的流行,越来越多的项目使用JWT作为用户身份认证的方式。JWT相当于是三个JSON对象经过编码后,用`.`分隔并组合到一起,这三个JSON对象分别是头部(header)、载荷(payload)和签名(signature),如下图所示。
|
||||
|
||||

|
||||
|
||||
1. 头部
|
||||
|
||||
```JSON
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
其中,`alg`属性表示签名的算法,默认是HMAC SHA256(简写成`HS256`);`typ`属性表示这个令牌的类型,JWT中都统一书写为`JWT`。
|
||||
|
||||
2. 载荷
|
||||
|
||||
载荷部分用来存放实际需要传递的数据。JWT官方文档中规定了7个可选的字段:
|
||||
|
||||
- iss :签发人
|
||||
- exp:过期时间
|
||||
- sub:主题
|
||||
- aud:受众
|
||||
- nbf:生效时间
|
||||
- iat:签发时间
|
||||
- jti:编号
|
||||
|
||||
除了官方定义的字典,我们可以根据应用的需要添加自定义的字段,如下所示。
|
||||
|
||||
```JSON
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"nickname": "jackfrued",
|
||||
"role": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
3. 签名
|
||||
|
||||
签名部分是对前面两部分生成一个指纹,防止数据伪造和篡改。实现签名首先需要指定一个密钥。这个密钥只有服务器才知道,不能泄露给用户。然后,使用头部指定的签名算法(默认是`HS256`),按照下面的公式产生签名。
|
||||
|
||||
```Python
|
||||
HS256(base64Encode(header) + '.' + base64Encode(payload), secret)
|
||||
```
|
||||
|
||||
算出签名以后,把头部、载荷、签名三个部分拼接成一个字符串,每个部分用`.`进行分隔,这样一个JWT就生成好了。
|
||||
|
||||
#### JWT的优缺点
|
||||
|
||||
使用JWT的优点非常明显,包括:
|
||||
|
||||
1. 更容易实现水平扩展,因为令牌保存在浏览器中,服务器不需要做状态管理。
|
||||
2. 更容易防范CSRF攻击,因为在请求头中添加`localStorage`或`sessionStorage`中的token必须靠JavaScript代码完成,而不是自动添加到请求头中的。
|
||||
3. 可以防伪造和篡改,因为JWT有签名,伪造和篡改的令牌无法通过签名验证,会被认定是无效的令牌。
|
||||
|
||||
当然,任何技术不可能只有优点没有缺点,JWT也有诸多缺点,大家需要在使用的时候引起注意,具体包括:
|
||||
|
||||
1. 可能会遭受到XSS攻击(跨站脚本攻击),通过注入恶意脚本执行JavaScript代码获取到用户令牌。
|
||||
2. 在令牌过期之前,无法作废已经颁发的令牌,要解决这个问题,还需要额外的中间层和代码来辅助。
|
||||
3. JWT是用户的身份令牌,一旦泄露,任何人都可以获得该用户的所有权限。为了降低令牌被盗用后产生的风险,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应通过其他方式再次对用户进行认证,例如短信验证码等。
|
||||
|
||||
#### 使用PyJWT生成和验证令牌
|
||||
|
||||
在Python代码中,可以使用三方库`PyJWT`生成和验证JWT,下面是安装`PyJWT`的命令。
|
||||
|
||||
```Bash
|
||||
pip install pyjwt
|
||||
```
|
||||
|
||||
生成令牌。
|
||||
|
||||
```Python
|
||||
payload = {
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1),
|
||||
'userid': 10001
|
||||
}
|
||||
token = jwt.encode(payload, settings.SECRET_KEY).decode()
|
||||
```
|
||||
|
||||
验证令牌。
|
||||
|
||||
```Python
|
||||
try:
|
||||
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTQ4NzIzOTEsInVzZXJpZCI6MTAwMDF9.FM-bNxemWLqQQBIsRVvc4gq71y42I9m2zt5nlFxNHUo'
|
||||
payload = jwt.decode(token, settings.SECRET_KEY)
|
||||
except InvalidTokenError:
|
||||
raise AuthenticationFailed('无效的令牌或令牌已经过期')
|
||||
```
|
||||
|
||||
相信通过上面的讲解,大家已经可以自行完成对投票项目用户登录功能的修改,如果有什么疑惑,可以参考我的代码,点击[地址一](https://github.com/jackfrued/vote)或[地址二](https://gitee.com/jackfrued/vote)可以打开项目仓库的页面。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
## RESTful架构和DRF进阶
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
## 使用Django开发商业项目
|
||||
|
||||
> 说明:本文的部分插图来自于《Python项目开发实战》和《精通Django》,这两本书中都包含了对Django框架精彩的讲解,有兴趣的读者可以自行购买阅读。
|
||||
> **说明**:本文的部分插图来自于《Python项目开发实战》和《精通Django》,这两本书中都包含了对Django框架精彩的讲解,有兴趣的读者可以自行购买阅读。
|
||||
|
||||
### Web应用
|
||||
|
||||
|
|
@ -8,9 +8,7 @@
|
|||
|
||||

|
||||
|
||||
问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群,如下图所示,架构并不是一开始就是这样,而是逐步演进的)
|
||||
|
||||

|
||||
问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群。当然,架构都是根据业务的需要一步步演进而不是一蹴而就的。)
|
||||
|
||||
问题3:描述Django项目的工作流程。(如下图所示)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Loading…
Reference in New Issue