HTTP 的常用请求方法如下:

HTTP 方法 行为 示例
GET 获取资源的信息 http://example.com/api/orders
GET 获取某个特定资源的信息 http://example.com/api/orders/123
POST 创建新资源 http://example.com/api/orders
PUT 更新资源 http://example.com/api/orders/123
PATCH 对 PUT 方法的补充,用来对已知资源进行局部更新 http://example.com/api/orders/123
DELETE 删除资源 http://example.com/api/orders/123

REST 设计不需要特定的数据格式。在请求中数据可以以 JSON 形式, 或者有时候作为 url 中查询参数项。

RESTful API规划

HTTP 方法 URL 动作
GET http://127.0.0.1:5000/api/v1/books 查询图书列表,支持分页查询,分页的参数为JSON格式数据:{"limit":10, "offset":0}
POST http://127.0.0.1:5000/api/v1/books 添加新图书
GET http://127.0.0.1:5000/api/v1/books/[book_id] 查询指定 id 的一本图书
PUT http://127.0.0.1:5000/api/v1/books/[book_id] 更新指定 id 的图书
PATCH http://127.0.0.1:5000/api/v1/books/[book_id] 更新指定 id 的图书, 与 PUT 方法相同
DELETE http://127.0.0.1:5000/api/v1/books/[book_id] 删除指定 id 的图书

添加修改 图书时,图书信息为 JSON 数据, 数据格式示例:{"title":"Python Web","author":"Tony John","summary":"This is a good book","published":"2020"}

实际部署时,将 URL 中的 127.0.0.1:5000 替换为相应的 主机名IP地址

创建项目

创建名为 flask-book-appflask 项目(步骤略)

项目中需要的包:

  1. flask: Python 编写的 Web 应用程序框架

  2. python-dotenv: 从文件中读取键值对,并将其设置为环境变量

  3. flask-sqlalchemy: 数据库 ORM

  4. flask-restful: 快速构建 REST APIs 的 Flask 扩展

  5. flask-marshmallow:序列化和反序列化模块, 用来将复杂的 orm 对象 与 python 原生数据类型之间相互转换的库

  6. flask-cors: 跨域请求配置模块

  7. marshmallow-sqlalchemy: 用于 flask-marshmallowflask-sqlalchemy 的集成

添加配置文件 .flaskenv, 内容如以下代码:

1
2
FLASK_ENV=development
FLASK_APP=bookapp

使用包组织项目代码

Flask 对项目结构没有固定要求,可以将所有代码放在 app.py 文件中, 但是这样会导致代码难以阅读和修改,因此这里使用包来组织整个项目的代码。

项目的最终结构如以下图所示:

1
2
3
4
5
6
7
8
.
|-- bookapp --- 包名
| |-- __init__.py --- 包构造文件,创建程序实例
| |-- command.py --- 命令函数
| |-- models.py --- 模型类
| `-- views.py --- 视图函数,编写 RESTful API 的方法
|-- venv --- 虚拟环境文件夹
`-- data.db --- Sqlite 数据库文件

添加文件夹 bookapp, 并在该文件夹中添加两个文件: __init__.py, views.py

编辑文件 bookapp\__init__.py, 内容如以下代码:

1
2
3
4
5
6
from flask import Flask

app = Flask(__name__)


from bookapp import views

编辑 bookapp\views.py, 内容如以下代码:

1
2
3
4
5
6
from bookapp import app


@app.route('/')
def index():
return 'Hello world'

运行项目, 效果如下图:

使用数据库

编辑 bookapp\__init__.py, 内容如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
os.path.join(os.path.dirname(app.root_path), 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# 确保这一行放在最后面
from bookapp import views, models

添加数据模型类

bookapp 文件夹中添加文件 models.py, 编辑代码如以下:

1
2
3
4
5
6
7
8
9
from bookapp import db


class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50)) # 标题
author = db.Column(db.String(50)) # 作者
summary = db.Column(db.String(250)) # 摘要
published = db.Column(db.String(4)) # 发行年份

添加的命令实现数据库初始化

bookapp 文件夹中添加文件 command.py, 编辑代码如以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import click
from bookapp import app, db
from bookapp.models import Book


@app.cli.command()
def forge():
'''
用于初始化数据库的指令, 在虚拟环境下运行: flask forge
'''
db.create_all()
books = [
{
'title': 'A Fire Upon the Deep',
'author': 'Vernor Vinge',
'summary': 'The coldsleep itself was dreamless.',
'published': '1992'
},
{
'title': 'The Ones Who Walk Away From Omelas',
'author': 'Ursula K. Le Guin',
'summary': 'With a clamor of bells that set the swallows soaring, the Festival of Summer came to the city Omelas, bright-towered by the sea.',
'published': '1973'
},
{
'title': 'Dhalgren',
'author': 'Samuel R. Delany',
'summary': 'to wound the autumnal city.',
'published': '1975'
},
{
'title': 'JavaScript Programming',
'author': 'Samuel R. Delany',
'summary': 'to wound the autumnal city.',
'published': '2015'
},
{
'title': 'Flask Web Development',
'author': 'Ursula K. Le Guin',
'summary': 'With a clamor of bells that set the swallows soaring, the Festival of Summer came to the city Omelas, bright-towered by the sea.',
'published': '2018'
},
]

for item in books:
book = Book(
title=item['title'],
author=item['author'],
summary=item['summary'],
published=item['published']
)
db.session.add(book)
db.session.commit()
click.echo('database initialized.')

编辑 bookapp\__init__.py, 在最后一行添加 command 模块的导入, 如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
os.path.join(os.path.dirname(app.root_path), 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# 确保这一行放在最后面,
from bookapp import views, models, command # 这里多了一个 command 模块

在项目的虚拟环境下执行 flask forge 命令初始化数据,并添加数据到数据库中:

1
2
(venv) PS D:\sources\python_repos\flask-book-app> flask forge
database initialized.

编写 API 方法 ( 组织 Flask-RESTful 资源对象)

编辑 bookapp\views.py, 内容如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
from bookapp import app, db, ma, api
from bookapp.models import Book
from flask import jsonify, redirect, request
from flask_restful import Resource


class BookSchema(ma.Schema):
class Meta:
# 需要序列化的字段, 只有在这里列出的字段名才会转为 json 数据
fields = ('id', 'title', 'author', 'summary', 'published')


# 单个模型数据的序列化
book_schema = BookSchema()
# 多个模型数据的序列化
books_schema = BookSchema(many=True)


class BookListResource(Resource):

def get(self):
'''
查询图书,分页参数的请求格式:http://127.0.0.1:5000/api/v1/books?limit=3&offset=3
使用默认参数的访问路径:http://127.0.0.1:5000/api/v1/books
'''

# 分页的默认值
limit = 10 # 每页大小(返回记录数)
offset = 0 # 跳过记录数

##### 接收 json 格式的参数 开始 , 这个方式不支持 url 地址传递的参数
# 如果要从URL中接收参数,请导入 from flask_restful.reqparse import RequestParser
try: # 加入异常处理,以防接收不到正确参数时程序崩溃
if(request.json):
limit = request.json['limit']
offset = request.json['offset']
except Exception as e:
pass
##### 接收 json 格式的参数 结束

query = Book.query.offset(offset).limit(limit) # 分页查询
total = Book.query.count() # 记录总数
books = books_schema.dump(query) # 将查询结果由对象类型转为 python 数据
return jsonify({ # 以JSON格式数据返回查询结果
"offset": offset,
"limit": limit,
"total_count": total,
"books": books
})

def post(self):
'''
添加一本图书, 每本图书四个属性,不能为缺少
访问路径:http://127.0.0.1:5000/api/v1/books
'''
try:
new_book = Book( # 创建新图书
title=request.json['title'],
author=request.json['author'],
summary=request.json['summary'],
published=request.json['published']
)
db.session.add(new_book) # 添加图书
db.session.commit() # 提交会话
return book_schema.dump(new_book) # 将查询结果由对象类型转为 python 数据
except Exception as e:
return None
# return request.json['title']


class BookResource(Resource):
def get(self, book_id):
'''
查询指定 id 的图书, 访问路径:http://127.0.0.1:5000/api/v1/books/2
'''
book = Book.query.get_or_404(book_id) # 查询指定id的图书
return book_schema.dump(book)

def patch(self, book_id):
'''
修改指定 id 的图书
patch 指令是 put 指令的升级,允许局部更新
访问路径:http://127.0.0.1:5000/api/v1/books/2 (同时带上要修改的属性值 )
'''
try:
book = Book.query.get_or_404(book_id) # 查询指定id的图书
if 'title' in request.json: # 修改数据
book.title = request.json['title']
if 'author' in request.json:
book.author = request.json['author']
if 'summary' in request.json:
book.summary = request.json['summary']
if 'published' in request.json:
book.published = request.json['published']
db.session.commit() # 提交会话
return book_schema.dump(book) # 返回修改成功后的图书信息
except Exception as e:
return None

def put(self, book_id):
'''
put 指令 和 patch 指令是一样的
'''
return self.patch(book_id)

def delete(self, book_id):
'''
删除指定 id 的图书
访问路径:http://127.0.0.1:5000/api/v1/books/2
'''
book = Book.query.get_or_404(book_id) # 查询指定id的图书
db.session.delete(book) # 删除图书
db.session.commit() # 提交会话
return '', 204


@app.route('/', methods=['GET'])
def index():
# 跳转到 api对应的地址
return redirect('/api/v1/books')

绑定资源与URI

编辑 bookapp\__init__.py, 加入跨域配置序列化模块, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import os

from flask import Flask
from flask_sqlalchemy import SQLAlchemy # 导入数据库 ORM 模块
from flask_cors import CORS # 导入跨域配置模块

# flask_marshmallow 序列化和反序列化模块,
# 用于将复杂的orm模型对象与python原生数据类型之间相互转换
from flask_marshmallow import Marshmallow

# flask_restful 快速构建 REST APIs 的Flask 扩展
# http://www.pythondoc.com/Flask-RESTful/
from flask_restful import Api

app = Flask(__name__) # 创建Flask实例

# 跨域允许
CORS(app, resources={r'/*': {'origins': '*'}})

# 配置数据库连接
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
os.path.join(os.path.dirname(app.root_path), 'data.db')

# 关闭数据库对象修改的跟踪
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app) # 初始化扩展 SQLAlchemy, 传入程序实例 app
ma = Marshmallow(app) # 初始化扩展 Marshmallow, 传入程序实例 app
api = Api(app) # 初始化扩展 Api, 传入程序实例 app


from bookapp import views, models, command # 确保这一行放在最后面

from bookapp.views import BookListResource, BookResource # 导入资源类

# 从 BookListResource 添加 api 资源,
# 对应分页查询 和 添加图书 这两个不需要 id 的操作
# 使用默认参数分页查询数据的地址:http://127.0.0.1:5000/api/v1/books
# 指定参数的分页查询数据的地址和参数示例:http://127.0.0.1:5000/api/v1/books?limit=3&offset=3
# 添加数据的访问路径:http://127.0.0.1:5000/api/v1/books
api.add_resource(BookListResource, '/api/v1/books') # 添加资源及访问路径

# 从 BookResource 添加 api 资源,
# 对应 单个对图书查询, 删除、修改这三个 要指定 id 的操作
# 删除、修改和查询的访问路径:http://127.0.0.1:5000/api/v1/books/2
api.add_resource(BookResource, '/api/v1/books/<int:book_id>') # 添加资源及访问路径

测试 RESTful API

运行项目

在虚拟环境下执行命令: flask run 运行项目,运行结果如下所示:

1
2
3
4
5
6
7
8
(venv) PS D:\sources\python_repos\flask-book-app> flask run
* Serving Flask app 'bookapp' (lazy loading)
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 129-008-647
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

安装扩展

Visual Studio Code 中使用安装名为 PostCode 的扩展, 如下图所示:

无参数的默认分页查询图书测试

带参数的分页查询图书测试

查询指定 id 的图书测试

添加新图书测试

修改图书测试

删除图书测试