- 项目放在github上,以下是项目地址
1.起步与红图
1.新建入口文件
-
在
ginger目录下新建入口文件ginger.py -
实例化
Flask对象:- 在
ginger目录下新建app的包,再在其包下新建app.py文件- 和
Flask对象相关的初始化或操作都放入app.py文件,使项目拥有良好拓展性 app=Flask(__name__)
- 和
- 在
-
导入项目配置文件到
Flask对象:-
在
app目录下新建config的包,再在其包下新建secury.py(敏感配置)文件setting.py(通用配置) -
把配置文件装载到
app.py中:app.config.from_object(\'app.config.setting\') app.config.from_object(\'app.config.secure\')
-
-
在入口文件
ginger.py中调用appapp=create_app() -
判断当前文件是入口文件,调用app的run方法启动web服务器
if __name__==\'__main__\': app.run(debug=True) -
使用postman进行测试:
- 在GET中输入localhost:5000按回车测试
- 因为还未编写视图函数,所以返回404
2.蓝图分离视图函数的缺陷
-
在入口文件
ginger.py中新建视图函数get_user-
利用
app.route()装饰器中传递视图函数get_user的URL:@app.route(\'/v1/user/get\') def get_user(): return \'i am kikyo\' -
使用postman进行测试:
- 在GET中输入localhost:5000//v1/user/get按回车测试
- 返回i am kikyo
-
-
为什么不在入口文件
ginger.py中新建视图函数- 视图函数较多,写在一个文件中不方便
- 不同的视图函数有不同的作用对象,大型项目中同一个作用对象可能有很多视图对象,应该分门别类放在不同文件中
-
所有视图函数都是对api的操作,在
app的包下新建api包,在api包下再新建v1包-
v1包下新建user.py和book.py -
把视图函数
get_user()和get_book()拆分到user.py和book.py中-
注册视图函数的路由时,需要Flask的核心对象app
-
把
ginger文件中的app直接导入,会导致循环导入 -
使用蓝图blueprint注册路由
- 实例化一个Blueprint(),第一个参数传递蓝图名称,第二个参数指定位置信息:
book=Blueprint(\'book\',__name__) - 使用蓝图下的route装饰器注册路由:
@book.route(\'/v1/book/get\')
- 实例化一个Blueprint(),第一个参数传递蓝图名称,第二个参数指定位置信息:
-
使蓝图生效
-
把蓝图注册到核心对象app上
-
在
app.py文件中定义register_blueprint( )函数注册def register_blueprint(app): from app.api.v1.user import user from app.api.v1.book import book app.register_blueprint(user) app.register_blueprint(book) -
在
app=create_app()中调用register_blueprint( )函数调用蓝图
-
-
使用使用postman进行测试看蓝图是否能拆分:
- 在GET中输入localhost:5000//v1/user/get按回车测试
- 返回i am kikyo
-
-
蓝图Blueprint不是用来拆分视图函数,而是一种模块级别的拆分
-
-
-
3.创建自己的红图
-
在
app下新建一个directory,命名为libs,用于存放自己定义的模块-
在
libs下新建redprint.py,定义redprint类-
在
book.py下实例化redprint:api=Redprint(\'book\') -
创建
v1蓝图被所有红图公用,在v1的___init__.py下定义蓝图:def create_blueprint(): bp_v1=Blueprint(\'v1\',__name__) pass
-
-
将红图注册到蓝图上:
- 在蓝图定义函数create_blueprint()下使用
user.api.register(bp_v1)
- 在蓝图定义函数create_blueprint()下使用
-
把蓝图注册到核心对象app上
-
在
app.py文件中定义register_blueprint( )函数注册def register_blueprint(app): from app.api.v1 import create_blueprint_v1 app.register_blueprint(create_blueprint_v1())
-
-
-
把视图函数中的
v1挂载到蓝图上,把book挂载到红图上 -
注册蓝图时给其附加一个前缀:
app.register_blueprint(create_blueprint_v1(),url_prefix=\'/v1\') -
把视图函数中的
book挂载到红图上- 注册红图时给其附加一个前缀:
user.api.register(bp_v1,url_prefix=\'/user\')
- 注册红图时给其附加一个前缀:
4.实现Redprint
-
传入红图的名字
-
定义构造函数
__init_方法传入名字def __init__(self,name): self.name=name -
实现
api.route()的装饰器-
参考blueprint的route函数,蓝图把视图函数注册到蓝图上,自定义的红图Redprint里也需要视图函数注册到蓝图上
-
原
blueprint.py中的self是蓝图,但自定义的redprint.py中的self是红图,route中拿不到蓝图,需要先把相关参数先保存起来#rule注册的URL,option其他可选选择 def route(self,rule,**options): #f装饰器作用的函数 def decorator(f): self.mound.append((f,rule,options)) return f return decorator
-
-
将红图注册到蓝图时使用了register方法,所以还需要定义register方法
-
register方法中传入了蓝图参数,在register方法中完成视图函数向蓝图的注册
def register(self,bp,url_prefix=None): for f,rule,options in self.mound: endpoint = options.pop("endpoint", f.__name__) #蓝图注册到视图函数上 bp.add_url_rule(rule,endpoint,f,**options)
-
-
5.优化Redprint
-
注册红图时url_prefix与Redprint中传入的名字一致,可以省去
user.api.register(bp_v1,url_prefix=\'/user\')中的url_prefix,在Redprint中定义好def register(self,bp,url_prefix=None): if url_prefix is None: url_prefix=\'/\'+self.name
2.自定义异常对象
1.构建client验证器
-
在
v1包下新建client.py构建客户端路由create_client()-
from app.libs.redprint import Redprint api=Redprint(\'client\') @api.route(\'/register\') def create_client(): #注册 登陆 #参数 校验 接受参数 #WTForms 校验表单 pass
-
-
在
libs包下新建enums.py定义客户端不同方式的各种枚举-
from enum import Enum #客户端类型 class ClientTypeEnum(Enum): USER_EMAIL=100 USER_MOBLE=101 #微信小程序 USER_MINA=200 #微信公众号 USER_WX=201 pass
-
-
在
app包下新建validators包进行客户端参数校验-
新建
forms.py使用WTForms 校验表单-
定义
ClientForm(Form)类对客户端表单验证-
验证时(
wtforms.validators)账号和登陆类型必须传入(DataRequired) -
客户登陆类型,WTForms 表单验证中没有,需要自定义传入的是枚举类型
enums.py中的一种#自定义客户端类型验证 def validate_tppe(self,value): try: client=ClientTypeEnum(value.data) except ValueError as e: raise e pass
-
-
-
2.处理不同客户端注册的方案
-
在
client.py的create_client()视图函数中使用client验证器进行参数的校验- 用json获取提交对象
- 表单的提交对象用于网页中,json对象用于移动端中
- 用实例化的表单验证类
ClientForm(Form)接收获取到的json数据对表单进行验证 - 定义一个字典
promise为不同的客户端编写不同的注册代码- 键:登陆的枚举对象
ClientTypeEnum.USER_EMAIL - 值:该登陆方式下用户注册的函数
- 键:登陆的枚举对象
#client.py from flask import request from app.libs.enums import ClientTypeEnum from app.libs.redprint import Redprint from app.validators.forms import ClientForm api=Redprint(\'client\') @api.route(\'/register\',methods=[\'POST\']) def create_client(): data=request.json#获得客户端参数 form=ClientForm(data=data)#实例化validators的forms客户端表单验证类 if form.validate(): promise={ ClientTypeEnum.USER_EMAIL:__register_user_by_email, ClientTypeEnum.USER_WX: __register_user_by_wx } #switch不同的客户端编写不同的注册代码 #request.args.to_dict() #表单 json #注册 登陆 #参数 校验 接受参数 #WTForms 校验表单 pass #用户用emil注册的相关代码 def __register_user_by_email(): pass def __register_user_by_wx(): pass - 用json获取提交对象
3.创建User模型
-
在
app下新建一个models包用来存放所有模型文件,新建用户模型文件user.py-
使用SQLALchemy定义
User模型类- 定义id emil,昵称nickname,是否管理员auth,密码_password
- 密码_password属性在
SQLALchemy中没有定义验证的方法,需要自定义
- 密码_password属性在
- 添加注册方法
register_by_email()
#models.user.py #User模型,继承自定义的base.py中的Base类 class User(Base): id=Column(Integer,primary_key=True) emil=Column(String(24),unique=True,nullable=False) nickname=Column(String(24),unique=True) auth=Column(SmallInteger,default=1) _password=Column(\'password\',String(100)) #把类中定义的实例方法变成类属性 @property def password(self): return self._password #@property对于新式类来说定义的属性是一个只读属性,如果需要可写,则需要一个@属性.setter装饰器装饰该函数 @password.setter def password(self): self._password=generate_password_hash(raw) #注册方法 @staticmethod#在对象下面再创建对象本身不合理,要用静态方法 def register_by_email(nickname,account,secret): #在数据库中使用auto_commit()方法新增用户 with db.auto_commit(): user=User() user.nickname=nickname user.emil=account user.password=secret db.session.add(user) pass - 定义id emil,昵称nickname,是否管理员auth,密码_password
-
-
使
SQLALchemy生效-
在
app.py定义register_plugin()方法- 导入
SQLALchemy的实例化对象db - 进行db注册
- 创建所有数据库的数据表
#使SQLALcjhemy生效 def register_plugin(app): from app.models.base import db#导入db db.init_app(app)#db注册 #create_all要在app的上下文环境中进行操作 with app.app_context(): db.create_all()#创建所有数据库的数据表 pass - 导入
-
4.完成客户端注册
-
用户注册方法
__register_user_by_email()-
调用
User.py表单模型类中register_by_email()方法完成注册 -
通过传入表单验证的实例化对象
form=ClientForm(data=data),传入注册所需的account和secret -
nickname无法直接从form中获取,json中有用户提交的所有数据,有nickname,但是拿到的数据没有通过校验,需要在form.py中新建一个User验证的类#用户注册类验证 class UserEmailForm(ClientForm): account = StringField(validators=[ Email(message=\'invalidate email\') ]) secret = StringField(validators=[ DataRequired(), # password can only include letters , numbers and "_" Regexp(r\'^[A-Za-z0-9_*&$#@]{6,22}$\') ]) nickname = StringField(validators=[DataRequired(), length(min=2, max=22)]) #验证账号是否已经被注册过 def validate_account(self, value): if User.query.filter_by(email=value.data).first(): raise ValidationError() -
User验证的类UserEmailForm接收用户提交的数据request.json进行验证,传入注册表单模型User.register_by_email中#用户用emil注册的相关代码 def __register_user_by_email():#从form的验证器中获取注册需要的参数 #request.json[\'nickname\'] form=UserEmailForm(data=request.json) #验证通过 if form.validate(): User.register_by_email(form.nickname.data,form.account.data,form.secret.data)
-
-
调用注册方法
__register_user_by_email()- 通过字典拿到
__register_user_by_email()-
form.py中将登陆类型的数字转换成枚举对象,用form.type.data可获取登陆类型的枚举对象,再通过字典[键]获取键值promise[form.type.data]()
-
- 通过字典拿到
5.生成用户数据
-
创建数据库
ginger:CREATE DATABASE ginger; -
在配置文件
secure.py中用SQLALCHEMY_DATABASE_URI连接数据库,并用SECRET_KEY设置密钥保证会话安全#连接数据库 SQLALCHEMY_DATABASE_URI = \'mysql+cymysql://root:123@127.0.0.1:3306/ginger\' # 设置密钥,保证会话安全 SECRET_KEY = \'\x8d\x7f\xaf\xc8"a\xa1]c\xba\xcb\x80x\xbc\x97s\' -
REST 细节特性:输入输出都要是
json格式 -
使用postman进行测试:
- 在POST中输入localhost:5000/v1/client/register
- 在Body中选择raw,选择格式为json
- 在下方输入json格式的注册数据
- 按send发送数据,在最底下显示success
-
查看数据添加
- 选择数据库use ginger;
- 显示所有表格show tables;
- 显示表格所有内容:select * from user;
- 数据未添加对时无法在数据库中显示
6.自定义异常对象
-
pycharm运行时显示
Adress already in use解决方法:- 用
sudo lsof -i:5000查看哪些端口被占用 - 再使用
sudo kill (PID)结束进程,释放端口
- 用
-
使用postman进行测试
-
在下方输入错误的json格式的注册数据
{"account":"777@qq.com","secret":"1234567","type":99,"nickname":"***"} -
按send发送数据,在最底下也能显示success
-
-
在pycharm代码左侧点击设置断点进行debug
- 断点1:
client.py的form=ClientForm(data=data) - 断点2:
client.py的return \'sucess\' - 断点3:
forms.py的客户端类型验证try - 点击
ginger.py的debug,点击run to cursor运行到下一个断点,可以看到枚举类型是99,点击step into my code运行到raise e查看异常显示’99 is not a vaild ClientTypeEnum’,再点击run to cursor运行到下一个断点可以直接运行到return ‘sucess’而并不会在异常处中断。 - form.validate()异常不会被wtform抛出,只会把异常信息记录在form的error属性中
- 断点1:
-
校验不通过时,手动抛出异常
-
from werkzeug.exceptions import HTTPException进入exceptions.py查看werkzeug自带的异常403 Not found -
继承
HTTPException自定义异常:在libs下新建error_code.py- 创建自定义的error类
ClientTypeError(HTTPException) - 添加状态码和描述
#客户端类型错误 class ClientTypeError(HTTPException): #401未授权 403禁止访问 404没有找到资源 #500服务器产生一个未知的错误 #200查询成功 201创建、更新成功 204删除成功 #301 302重定向 code=400#请求参数错误 description = ( "client is invalid" ) - 创建自定义的error类
-
验证form.validate()不通过时raise ClientTypeError()
-
-
使用postman进行测试:
- 在POST中输入localhost:5000/v1/client/register
- 在Body中选择raw,选择格式为json
- 在下方输入json格式的错误注册数据
- 按send发送数据,在最底下选择preview显示自定义的错误描述client is invalid,状态码为400
7.异常返回的标准性
- 返回信息分类
- 业务数据信息
- 操作成功提示信息
- 错误异常信息{‘msg’:xxx,’error_code’:xxx,’request’:url}
8.自定义json格式的APIException
-
API输入输出数据都必须是json格式,但继承
HTTPException自定义的错误只能输出HTML格式的错误信息,需要重写HTTPException -
在
libs下新建error.py自定义APIException(HTTPException)类继承HTTPException,对其重写-
APIException要有些默认的msg(错误信息),error_code(错误码),code(错误状态码) - 有机制改变默认值
- 重写构造函数,改变默认值
- 用if判断是否传了参数,用传的参数替代默认参数
- 用super继承父类
HTTPException的构造方法
- 重新get_body函数,改变
HTML内容为json格式- 字典存储:错误信息,错误码,访问哪个api接口产生的(请求的http动词+’ ‘+当前请求的URL路径(不包括主机名和端口号))
- 通过一个静态方法(和类本身没有交互)用
request.full_path拿到完整路径,再用split去除掉问号后的路径
- 通过一个静态方法(和类本身没有交互)用
- 通过
json.dumps(body)把字典格式改成文本信息
- 字典存储:错误信息,错误码,访问哪个api接口产生的(请求的http动词+’ ‘+当前请求的URL路径(不包括主机名和端口号))
- 重写get_headers函数,使输出的http头是json
- 把
text/html改为application/json
- 把
- 重写构造函数,改变默认值
from flask import request, json from werkzeug.exceptions import HTTPException class APIException(HTTPException): code=500#错误状态码500服务器产生一个未知的错误 msg=\'sorry,we have a mistake -