yffxwyy


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中调用app

    app=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.pybook.py

    • 把视图函数get_user()get_book()拆分到user.pybook.py

      • 注册视图函数的路由时,需要Flask的核心对象app

        • ginger文件中的app直接导入,会导致循环导入

        • 使用蓝图blueprint注册路由

          • 实例化一个Blueprint(),第一个参数传递蓝图名称,第二个参数指定位置信息:book=Blueprint(\'book\',__name__)
          • 使用蓝图下的route装饰器注册路由:@book.route(\'/v1/book/get\')
        • 使蓝图生效

          • 把蓝图注册到核心对象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)
    • 把蓝图注册到核心对象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.pycreate_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
    

3.创建User模型

  • app下新建一个models包用来存放所有模型文件,新建用户模型文件user.py

    • 使用SQLALchemy定义User模型类

      • 定义id emil,昵称nickname,是否管理员auth,密码_password
        • 密码_password属性在SQLALchemy中没有定义验证的方法,需要自定义
      • 添加注册方法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
      
  • 使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),传入注册所需的accountsecret

    • 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.pyform=ClientForm(data=data)
    • 断点2:client.pyreturn \'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属性中
  • 校验不通过时,手动抛出异常

    • 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"
          )
      
    • 验证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)把字典格式改成文本信息
      • 重写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 

分类:

技术点:

相关文章: