06-Flask之REST&API设计
一、REST
-
问题
网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这致使API构架的流行。
-
基本概念
REST是"Representational State Transfer"缩写,即是"表现层状态转化"。而"表现层"其实指的是"资源(Resource)"的表现层。一种软件架构风格、设计风格、而不是标准,只是提供了一组设计原则和约束条件。
它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存机制等。
REST其实是一种组织Web服务的架构,而并不是我们想象的那样是实现Web服务的一种新的技术,更没有要求一定要使用HTTP。其目标是为了创建具有良好扩展性的分布式系统。
1. 资源(Resource)
就是网络上的一个实体,或是网络上的具体信息,可以是一段文本、一张图片、一首歌曲、一部电影。
每个资源都会对应的URL,且是唯一的标识符,想要获取资源只需要调用对应URL。
2. 表现层(Representation)
"资源"是一种信息实体,它可以有多种外在表现形式。
而"资源"具体呈现出来的形式,就叫它的"表现层"。
3. 状态转换(State Transfer)
访问一个网站,就是客户端和服务端的交互过程,这个过程中就会涉及到数据和状态的变化。
互联网通信协议HTPP,是无状态协议。即所有状态都保存在服务端。
客户端要操作服务端必须通过某种方式,让服务端发生"状态转换",而这转换是建立在表现层之上的,所以就是"表现层状态转换"。
客户端用到的手段只能是HTTP协议,在操作方式的动词: GET/POST/PUT/DELETE。
对应GET获取资源,POST新建资源(或更新资源),PUT更新资源,DELETE删除资源。
- 架构级约束
1.使用客户/服务器模型。客户和服务器之间通过一个统一的接口来互相通讯
2.层次化的系统。在一个REST系统中,客户端并不会固定地与一个服务器打交道
3.无状态。在一个REST系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都需要提供足够的信息
4.可缓存。REST系统需要能够恰当地缓存请求,以尽量减少服务端和客户端之间的信息传输,以提高性能
5.统一的接口。一个REST系统需要使用一个统一的接口来完成子系统之间以及服务与用户之间的交互。这使得REST系统中的各个子系统可以独自完成演化
一个系统满足了上面所列出的五条约束,那么该系统就被称为是RESTful。
- 什么是RESTful框架
1. 每一个URL代表一种资源
2. 客户端和服务器之间,传递这种资源的某种表现层
3. 客户端通过四个HTTP动词,对服务端进行操作,实现"表现层状态转换"
RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。
二、返回JSON格式数据
天气API: https://www.sojson.com/api/weather.html
- jsonify序列化操作
# 返回json格式数据
@blue.route('/getjson/')
def getjson():
data = {
'data': 'hello flask',
'status': 200
}
return jsonify(data)
实现前后端的分离,后台人员只需要提供API接口接口。
- 后台人员工作职责
- 定制接口
- 模型定制
- 面向接口编程
只关注请求地址
值关注请求结果格式
- 示例
- 在app/views.py中添加获取商品列表数据API 【后台人员】
# 学生成绩API
@blue.route('/getscore/')
def getscore():
data = {
'msg':'学生成绩列表',
'status': '200',
'content': [20,32,53,90,43,67,99,89]
}
return jsonify(data)
备注: 获取数据 http://127.0.0.1:8000/getscore/
- 在app/static/html/testlist.html 【前端人员】
# 发起Ajax请求,获取对应学生成绩列表json数据,解析json数据,并渲染到页面中
<script>
$(function () {
$.getJSON("http://127.0.0.1:8000/getscore/", function(json){
if (json.status == 200){
$('body').append($('<h3></h3>').html(json.msg))
for(var i=0; i<json.content.length; i++){
$('body').append($('<h3></h3>').html('成绩:'+json.content[i]))
}
}
else{
$('body').append($('<h3></h3>').html('哥们,你错了!'))
}
});
})
</script>
备注: 静态页面在浏览器中打开 http://127.0.0.1:8000/static/html/scorelist.html
书写静态页面时,要注意是web相关的内容,而不是模板,所以注释等方式是会不一样的!!!
三、RESTful API设计
- 协议
API与用户的通信协议,通常使用HTTP(S)协议。
- 域名
应该尽量将API部署在专用域名之下。
如: http://api.zyz.com
如果确定API很简单,不会有大规模扩从,可以考虑放在主域名之下。
如: http://www.zyz.com/api/
- 版本
应该将API的版本号放入URL。
如: http://api.zyz.com/v1/
也有将版本号放在HTTP的头信息中,但不如放在URL中方便直观,Github就是这么做的。
- 路径
路径又称"终点"(endpoint),表示API的具体网址。
在RESTful架构中,每个网址代表一种资源,所以网址不能有动词,只能有名词。
而所用名词往往与数据库表单名对应。
- HTTP动词
对于资源的具体操作类型,由HTTP动词表示。
HTTP常用动词:
- GET(SELECT) 从服务器取资源
- POST(CREATE or UPDATE) 服务器中创建资源或更新资源
- PUT(UPDATE) 在服务器更新资源(客户端提供改变后的完整资源)
- PATCH(UPDATE) 在服务器更新资源(客户端提供改变的属性)
- DELETE(DELETE) 从服务器删除资源
- HEAD 获取资源的元数据
- OPTHONS 获取信息,关于资源的那些属性是客户端可以改变的
例如:
- GET /students 获取所有学生
- POST /student 新建学生
- GET /students/id 获取某一个学生
- PUT /students/id 更新某个学生的信息(需要提供学生的全部信息)
- PATHC /students/id 更新某个学生的信息(需要提供学生变更信息)
- DELETE /students/id 删除某个学生
- 过滤信息
当记录数量过多,服务器不可能将它们返回给用户。APIT应该提供参数,过滤返回结果。
?limit=10
?offset=10
?page=2&per_page=10
?sortby=name&order=desc
?student_id=id
- 状态码
服务器向用户返回的状态码和提示信息。
200 OK - [GET]:服务器成功返回用户请求的数据
201 CREATED -[POST/PUT/PATCH]:用户新建或修改数据成功
202 Accepted - [*] :表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:表示数据删除成功
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误
401 Unauthorized - [*] :表示用户没有权限(令牌,用户名,密码错误)
403 Forbidden - [*]:表示用户得到授权,但是访问是被禁止的
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录
406 Not Acceptable - [*]:用户请求格式不可得
410 Gone - [GET] :用户请求的资源被永久移除,且不会再得到的
422 Unprocesable entity -[POST/PUT/PATCH]:当创建一个对象时,发生一个验证错误
500 INTERNAL SERVER EROR - [*] :服务器内部发生错误
2xx —— 正确的响应
3xx —— 重定向
4xx —— 客户端错误
5xx —— 服务端错误
- 错误处理
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error做为键名
- 返回结果
针对不同操作,服务器想用户返回的结果应该符合以下规范:
GET /collection:返回资源对象的列表(数组,集合)
GET /collection/id:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/id:返回完整的资源对象
PATCH /collection/id:返回完整的资源对象
DELETE /collection/id:返回一个空文档
- 核心点
RESTfull 的核心是 resource ,对一个 resource ,用不同的 HTTP verb 做增删查改。
- 其他
服务器返回的数据格式,应该尽量使用JSON
- 原生实现
@blue.route('/api/v1/users/',methods=['GET','POST','DELETE','PUT','PATCH'])
def users():
if request.method == 'GET': # 获取用户
pass
elif request.method == 'POST': # 更新或创建用户密码
# 获取数据
username = request.form.get('username')
password = request.form.get('password')
data = {
'msg':'ok',
'status': 201
}
# 不为空
if not username or not password:
data['msg'] = '参数不正确'
data['status'] = 422
return jsonify(data)
user = User()
user.u_name = username
user.u_passwd = generate_passwd(password)
try:
db.session.add(user)
db.session.commit()
except Exception as e:
data['msg'] = '用户已存在'
data['status'] = 423
return jsonify(data)
return jsonify(data)
elif request.method == 'DELETE': # 删除用户
pass
elif request.method == 'PUT': # 更新账和密码
pass
elif request.method == 'PATCH': # 修改密码
pass
else:
abort(405)
# 密码加密处理
def generate_passwd(passwd):
hash = hashlib.md5()
hash.update(passwd.encode('utf-8'))
return hash.hexdigest()
jsonify: json序列化
四、Flask-RESTful插件
Flask-RESTful添加快速构建REST API的支持,也是一个能够和现有ORM库协同工 作的轻量级的扩展。Flask-RESTful鼓励以最小的设置的最佳实践。
中文文档: http://www.pythondoc.com/Flask-RESTful/
- 基本使用
- 安装
pip install flask-restful
- 配置
# ext.py文件中
from flask_restful import Api
api = Api()
api.init_app(app)
- 使用(项目拆分)
# 定义一个资源
# views.py已经可以替换为apis.py,之前的路由功能换种写法
class HelloWorld(Resource):
def get(self): # get请求
return {'msg': 'hello world'}
def post(self): # post请求
return {'msg': '你好!'}
# 添加一个资源
# add_resource 注册路由到框架上
# 如果没有指定 endpoint,Flask-RESTful 会根据类名生成一个
# 但有时候如 url_for 需要 endpoint,因此最好明确给 endpoint 赋值
api.add_resource(HelloWorld, '/hello/',endpoint='hello')
Flask-Rest-JSONAPI插件、Flask-Restless插件
- 项目拆分
备注:
之前路由的操作都是在views.py中,现在只需要提供API接口,所以功能就会不一样;
在项目进行完基本拆分之后,可以将views.py改为apis.py;
资源不止一个,而不能都只是有一个,而是有多个,为了方便管理,可以将apis变为包的形式,方便管理;
# App/apis/__init__.py
api = Api()
def init_api(app):
api.init_app(api)
# App/__init_.py [之前init_blue 换为 init_api即可]
init_api(app)
# App/apis/HelloResource.py 定义资源
class HelloWorld(Resource):
def get(self): # get请求
return {'msg': 'hello world'}
# App/apis/__init__.py 添加资源
api.add_resource(HelloResource, '/hello/', endpoint='hello')
- 带参数操作
# App/apis/UserApi.py文件
class UserResource(Resource):
def get(self,id):
str = '(get)userid: %d' % id
return {'msg':str}
def post(self,id):
str = '(post)userid: %d' % id
return {'msg': str}
# App/apis/__init__.py文件
from App.apis import UserAPI
api.add_resource(UserResource, '/user/<int:id>/', endpoint='user')
Flask-RESTful 提供的最主要的基础就是资源(resources)。资源(Resources)是构建在 Flask视图 之上,只要在你的资源(resource)上定义方法就能够容易地访问多个 HTTP 方法。[无需原生操作,因为一个资源而进行不同的判断操作处理]
- 端点操作(Endpoints)
很多时候在一个 API 中,你的资源可以通过多个 URL 访问。
你可以把多个 URL 传给 Api 对象的 Api.add_resource() 方法。每一个 URL 都能访问到你的 Resource。
api.add_resource(HelloWorld,
'/hello/',
'/haha/',
'/hehe/')
- 输出格式定制
默认情况下,在你的返回迭代中所有字段将会原样呈现。
实际更多的需要一个字典类型数据,之后通过JSON序列化即可。
Flask-RESTful 提供了 fields 模块和 marshal_with() 装饰器来进行数据格式化。
# @marshal_with(需要返回的数据格式)
如果返回的数据,在预定义的结构中不存在,数据会被自动滤掉;
如果返回的数据,在预定的结构中存在,数据会正常返回;
如果返回的数据比预定的结构字段少,预定义的字段会显示默认值;
# 支持类型
- 常用基本类型
String
Integer
- 列表类型
List
- 级联类型
Nested
- 结构嵌套
fields.List(fields.Nested())
# 示例1
""" 获取一只猫的数据基本结构
{
'msg':'ok',
'status':200,
'data': {
'id': 1,
'name': 'TOM',
'color': '红色'
}
}
"""
catmodel_fields = {
'id': fields.Integer,
'name': fields.String,
'color': fields.String
}
onecat_fields = {
'msg': fields.String(default='ok'),
'status': fields.Integer(default=200),
'data': fields.Nested(catmodel_fields) # 嵌套
}
class OneCatResource(Resource):
@marshal_with(onecat_fields)
def get(self):
cat = Cat.query.first()
data = {
# msg 使用默认值,可以省略不写
# status 使用默认只,也可以不写
'data': cat
}
return data
# 示例2
""" 获取所有猫的数据结构
{
'msg':'ok',
'status': 200,
'data': [
{
'id': 1,
'name': 'TOM1',
'color': '红色'
},
{
'id': 2,
'name': 'TOM2',
'color': '红色'
},
{
'id': 3,
'name': 'TOM3',
'color': '红色'
},
...
]
}
"""
catmodel_fields = {
'id': fields.Integer,
'name': fields.String,
'color': fields.String
}
cats_fields = {
'msg': fields.String(default='ok'),
'status': fields.Integer(default=200),
'data': fields.List(fields.Nested(catmodel_fields))
}
class CatResource(Resource):
@marshal_with(cats_fields)
def get(self):
cats = Cat.query.all()
data = {
'msg': 'ok!',
'status': 200,
'data': cats
}
return data
- 请求参数解析
- 基本使用
# https://127.0.0.1/showview/?page=1
parser = reqparse.RequestParser()
# 接受参数page,类型是str,错误提示help
parser.add_argument('page', type=str, help='请输入页码')
class ShowView(Resource):
def get(self):
parser = parser.parse_args()
c_page = parser.get('page') or 1
def post(self):
parser = parser.parse_args()
c_page = parser.get('page') or 1
- 必须参数 required=True
# 该参数必须传入
parser.add_argument('page', type=int, help='请输入页码', required=True)
- 多参数(列表)
# 如果要接受一个键有多个值的话,可以传入 action='append'
# https://127.0.0.1/showview/?name='liming'&page=1&name='zhangsan'
parser.add_argument('name', type=str, action='append')
- 参数位置
参数位置: form、args、headers、cookies、files(上传文件)