Flask先后端分离项目案例

简介

学习慕课课程,Flask先后端分离API后台接口的实现demo,前端能够接入小程序,暂时已经完成后台API基础架构,使用postman调试. githtml

重构部分:前端

  1. token校验模块
  2. auths认证模块
  3. scope权限模块,增长全局扫描器(参考flask HTTPExceptions模块)

收获

  1. 咱们能够接受定义时的复杂,但不能接受调用时的复杂
  2. 若是你以为写代码厌倦,无聊,那你只是停留在功能的实现上,功能的实现很简单,你要追求的是更好的写法,抽象的艺术,不是机械的劳动而是要创造,要有本身的思考
  3. Sqlalchemy中对类的建立都是用元类的方式,因此调用的时候都不用实例化,当咱们重写__init__方法是须要调用orm.reconstrcut装饰器,才会执行实例化对象的构造函数
  4. 权限等级模块的设计(api访问权限),如超级管理员,管理员,普通用户,访客,这四者之间的关系,有包含的关系,因此能够考虑合并也能够考虑排除的方式来构建权限控制模块. 参考本项目中的app.libs.scope
  5. 学的是解决问题的方法,首先要有深度,在去考虑广度,还要懂得迁移应用,造成本身的思惟模型。

推荐阅读:
工做中如何作好技术积累
没有技术深度的苦恼python

知识点复盘

初始化flask应用程序

app = Flask(__name__, static_folder='views/statics', static_url_path='/static', template_folder="templates")

建立Flask应用程序实例对象, 若是模块存在,会根据模块所在的目录去寻找静态文件和模块文件, 若是模块不存在,会默认使用app对象所在的项目目录git

  • __name__ 表示以此模块所在的目录做为工做目录,就是静态文等从这个目录下去找
  • static_folder 指定静态文件存放相对路径 flask默认会用/进行分割而后取最后一个做为访问url 相似Django中的STATICFILES_DIRS
  • static_url_path 指定访问静态文件的url地址前缀, 相似Django 中的 STATIC_URL
  • template_folder 指定模板文件的目录
@property
    def static_url_path(self):
        """The URL prefix that the static route will be accessible from.

        If it was not configured during init, it is derived from
        :attr:`static_folder`.
        """
        if self._static_url_path is not None:
            return self._static_url_path

        if self.static_folder is not None:
            basename = os.path.basename(self.static_folder)
            return ("/" + basename).rstrip("/")

    @static_url_path.setter
    def static_url_path(self, value):
        if value is not None:
            value = value.rstrip("/")

        self._static_url_path = value

Flask 中url相关底层类

  • BaseConverter子类:保存提取url参数匹配规则
  • Rule类:记录一个url和一个视图函数的对应关系
  • Map类:记录全部url地址和试图函数对应的关系 Map(Rule, Rule, ....)
  • MapAdapter类:执行url匹配的过程,其中有一个match方法,Rule.match(path, method)

自定义路由管理器

from flask import Flask

app = Flask(__name__)

from werkzeug.routing import BaseConverter

class RegexUrl(BaseConverter):
    # 指定匹配参数时的正则表达式
    # 如: # regex = '\d{6}'
    def __init__(self, url_map, regex):
        """
        :param url_map: flask会自动传递该参数
        :param regex: 自定义的匹配规则
        """
        super(RegexUrl, self).__init__(url_map)
        self.regex = regex
    
    # 在对应的试图函数以前调用
    # 从url中提取出参数以后,会先调用to_python
    # 会把提取出的值做为参数传递给to_pthon在返回给对应的试图
    def to_python(self, value):
        """能够在这里作一些参数的类型转换"""
        return value
    
    # 调用url_for时会被调用, 用来处理url反向解析时url参数处理
	# 返回值用来拼接url
    def to_url(self, value):
        """对接收到参数作一些过滤等"""
        return value
        
# 将自定义路由转换器类添加到转换器字典中
app.url_map.converters['re'] = RegexUrl


# 案例
@app.route('/user/<re("[a-z]{3}"):id>')
def hello(id):
    return f'hello {id}'


if __name__ == '__main__':
    app.run(debug=True)

全局异常捕获

AOP编程思想,面向切面编程,把事件统一在一个地方处理,在一个统一的出口作处理github

errorhandler 在flask 1.0版本以前只支持填写对应的错误码,好比 @app.errorhandler(404)正则表达式

在flask1.0版本以后就支持全局的异常捕获了@app.errorhandler(code_or_exception),有了这个以后,就能够在全局作一个异常捕获了,不用每一个视图函数都作异常捕获。编程

@app.errorhandler(Exception)
def framework_error(e):
    if isinstance(e, APIException):
        return e
    elif isinstance(e, HTTPException):
        code = e.code
        msg = e.description
        error_code = 1007
        return APIException(msg, code, error_code)

    else:
        if not current_app.config['DEBUG']:
            return ServerError()
        else:
            raise e

异常类型

  • 可预知的异常(已知异常)
  • 彻底没有意识的异常(未知异常)

abort函数

  • abort(状态码) 是一个默认的抛出异常的方法
  • 调用abort函数能够抛出一个指定状态码对应的异常信息
  • abort函数会当即终止当前视图函数的运行**

模型对象的序列化

场景:咱们有时候可能须要返回模型对象中的某些字段,或者所有字段,平时的作法就是将对象中的各个字段转为字典在返回jsonnify(data), 可是这样的写法可能在每一个须要返回数据的试图函数中都写一个对应的字典。。对象转字典在返回。json默认是不能序列化对象的,通常咱们的作法是 json.dumps(obj, default=lambda o: o.__dict__)可是 __dict__中只保存实例属性,咱们的模型类基本定义的类属性。解决这个问题就要看jsonify中是如何作序列化的,而后怎么重写。json

  1. 重写JSONEncoder
from datetime import date
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder

class JSONEncoder(_JSONEncoder):
    """
    重写json序列化,使得模型类的可序列化
    """
    def default(self, o):
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)
        if isinstance(o, date):
            return o.strftime('%Y-%m-%d')
        
   		super(JSONEncoder, self).default(o)
        

# 须要将重写的类绑定到应用程序中
class Flask(_Flask):
    json_encoder = JSONEncoder
  1. 模型类的定义
class User(Base):
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)
    auth = Column(SmallInteger, default=1)
    _password = Column('password', String(100))
    
    def keys(self):
        return ['id', 'email', 'nickname', 'auth']
    
    def __getitem__(self, item):
        return getattr(self, item)

注意: 修改了json_encode方法后,只要调用到flask.json 模块的都会走这个方法flask

为何要写keys__getitem__方法小程序

当咱们使用dict(object) 操做一个对象的时候,dict首先会到实例中找keys的方法,将其返回列表的值做为key, 而后会根据object[key] 获取对应的值,因此实例要实现__getitem__方法才可使用中括号的方式调用属性

进阶写法 - 控制返回的字段

场景:当咱们有一个Book的模型类,咱们的api接口可能须要返回book的详情页因此就要返回全部字典,但另一个接口可能只须要返回某几个字段。

class Book(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(30), default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdate = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(String(1000))
    image = Column(String(50))
	
    # orm实例化对象, 字段须要写在构造函数中,这样每一个实例对象都会有本身的一份,删除增长都不会互相影响
    @orm.reconstructor
    def __init__(self):
        self.fields = ['id', 'title', 'author', 'binding',
                       'publisher', 'price', 'pages', 'pubdate',
                       'isbn', 'summary', 'image']
        
   	def keys(self):
        return self.fields if hasattr(self, 'fields') else []
    
    def hide(self, *keys):
        for key in keys:
            self.fields.remove(key)
        return self
    
    def append(self, *keys):
        for key in keys:
            self.fields.append(key)
        return self


@api.route('/search')
def search():
    books = Book.query.filter().all()  # 根据某些条件搜索的
   	books = [book.hide('summary') for book in books]
    return jsonify(books)
    
    
@api,route('/<isbn>/detail')
def detail(isbn):
    book = Book.query.filter_by(isbn=isbn).first_or_404()
    return jsonify(book)

请求钩子函数

  • before_first_request:在处理第一个请求前运行。
  • before_request:在每次请求前运行。
  • after_request:若是没有未处理的异常抛出,在每次请求后运行。
  • teardown_request:在每次请求后运行,即便有未处理的异常抛出。

全局扫描器

模仿flask exceptions 预加载各个异常类的方式,将用户组自动加载进内存中,这样获取的话就更方便

str2obj = {}
level2str = {}


def iteritems(d, *args, **kwargs):
    return iter(d.items(*args, **kwargs))


def _find_scope_group():
    for _name, obj in iteritems(globals()):
        try:
            is_scope_obj = issubclass(obj, BaseScope)
        except TypeError:
            is_scope_obj = False
        if not is_scope_obj or obj.level < 1:
            continue

        old_obj = str2obj.get(_name, None)
        if old_obj is not None and issubclass(obj, old_obj):
            continue
        str2obj[_name] = obj
        level2str[obj.level] = _name


# 模仿flask exceptions 预加载各个异常类的方式,将用户组自动加载进内存
_find_scope_group()
del _find_scope_group

常见bug

  1. form正则校验注意事项

r'[1]{6, 25}$'

带空格和不带空格是两码事, 正则里面{,} 连续不带空格

r'[2]{6,25}$'

参考

Python Flask高级编程之RESTFul API先后端分离精讲
七月老师的课程挺好的,不是纯写代码,而是从问题入手,怎么把复杂问题简单化,从0到1。


  1. A-Za-z0-9_ ↩︎

  2. A-Za-z0-9_ ↩︎