一. 应用、蓝图与视图函数
我们将初始化app, 视图函数等内容,通过蓝图的方法进行重构:
目录结构:
/app/init.py
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object('config')
register_blueprint(app) # 完成蓝图注册
return app
def register_blueprint(app): # 注册蓝图
from app.web.book import web
app.register_blueprint(web)
/app/web/book.py
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, Blueprint # 蓝图
web = Blueprint('web', __name__) # 蓝图初始化
@web.route('/book/search/<q>/<page>')
def search(q, page):
"""
:param q:
:param page:
:return:
"""
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
else:
result = YuShuBook.search_by_keyword(q)
return jsonify(result)
/fisher.py
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=app.config['DEBUG'], port=81)
二. 单蓝图多模块拆分视图函数
修改文件目录:
app/web/blueprint.py:
from flask import Blueprint # 蓝图
web = Blueprint('web', __name__) # 蓝图初始化
app/web/book.py:
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify
from .blueprint import web
@web.route('/book/search/<q>/<page>')
def search(q, page):
"""
:param q:
:param page:
:return:
"""
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
else:
result = YuShuBook.search_by_keyword(q)
return jsonify(result)
app/web/user.py:
from .blueprint import web
@web.route('/user')
def user():
pass
app/web/init.py:
from app.web import book, user
app/init.py:
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object('config')
register_blueprint(app) # 完成蓝图注册
return app
def register_blueprint(app): # 注册蓝图
from app.web.blueprint import web
app.register_blueprint(web)
三. request对象
我们search视图函数的url,不够规范,
我们需要从book/search/<q>/<page>
改为/book/search?q=XXX&page=XXXX
的形式。
我们借助flask的request,修改app/web/book.py实现url规范化:
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
@web.route('/book/search')
def search():
q = request.args['q']
page = request.args['page']
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
else:
result = YuShuBook.search_by_keyword(q)
return jsonify(result)
补充说明: flask的request 必须在视图函数中才会有效, 即必须在触发http的情况下有效。
四. WTForms参数验证
在app/web/book.py中的视图函数search中,
我们希望能够对参数p和page 进行校验, 最的办法就是借助wtforms.
新增文件app/forms/book.py:
from wtforms import Form, StringField, IntegerField
from wtforms.validators import Length, NumberRange
class SearchForm(Form):
q = StringField(validators=[Length(min=1, max=30)])
page = IntegerField(validators=[NumberRange(min=1, max=99)], default=1)
修改app/web/book.py:
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
from app.forms.book import SearchForm
@web.route('/book/search')
def search():
# q = request.args['q'] # 至少要有一个字符串
# page = request.args['page'] # 至少是正整数, 且有最大值限制
form = SearchForm(request.args)
if form.validate():
q = form.q.data.strip()
page = form.page.data.strip()
# 参数最好从form中取, 如果从request.args来取的化, 没有默认值了
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
else:
result = YuShuBook.search_by_keyword(q)
return jsonify(result)
else:
return jsonify({'msg': '参数校验失败'})
优化报错
通过form.errors我们可以优化参数校验的报错
修改app/web/book.py:
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
from app.forms.book import SearchForm
@web.route('/book/search')
def search():
form = SearchForm(request.args)
if form.validate():
q = form.q.data.strip()
page = form.page.data.strip() # 最好从form中取, 如果从request.args来取的化, 没有默认值了
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
else:
result = YuShuBook.search_by_keyword(q)
return jsonify(result)
else:
return jsonify(form.errors) # 会报默认的form的错误
如果想自定义`form.errors`的报错,
我们需要在`app/forms/book.py`的`SearchForm`的每个元素的校验器validators中添加`message='报错信息'`
bug: 如果url为/book/search?q= &page=1
,我们发现 if form.validate():
为True。 而我们对于空格,想要的结果是不计算length或length=0.我们需要添加验证器Datarequired()
修改app/forms/book.py:
from wtforms import Form, StringField, IntegerField
from wtforms.validators import Length, NumberRange, DataRequired
class SearchForm(Form):
q = StringField(validators=[DataRequired(), Length(min=1, max=30)])
page = IntegerField(validators=[NumberRange(min=1, max=99)], default=1)
五. 拆分配置文件
在search视图函数中, 我们的page参数一直没有使用, 我们可以修改代码:
修改app/web/book.py:
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from flask import jsonify, request
from .blueprint import web
from app.forms.book import SearchForm
@web.route('/book/search')
def search():
form = SearchForm(request.args)
if form.validate():
q = form.q.data.strip()
page = form.page.data.strip() # 最好从form中取, 如果从request.args来取的化, 没有默认值了
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isbn(q)
else:
result = YuShuBook.search_by_keyword(q, page)
# 修改了传入参数 count和start改为page
return jsonify(result)
else:
return jsonify(form.errors)
修改/yushu_book.py:
from httper import HTTP
class YuShuBook:
per_page = 15 # 每页个数
isbn_url = 'http://t.yushu.im/v2/book/isbn/{}'
keyword_url = 'http://t.yushu.im/v2/book/search?q={}&count={}&start={}'
@classmethod
def search_by_isbn(cls, isbn):
url = cls.isbn_url.format(isbn)
result = HTTP.get(url)
return result
@classmethod
def search_by_keyword(cls, keyword, page=1): # count和start改为page
url = cls.keyword_url.format(keyword, cls.per_page, cls.per_page*(page-1))
result = HTTP.get(url)
return result
代码优化方向:per_page放入配置文件中,方便修改。url = cls.keyword_url.format(keyword, cls.per_page, cls.per_page*(page-1))
中cls.per_page*(page-1)用函数单独封装, 使代码更好阅读。
拆分配置文件
我们将/config.py删除, 在app下新建secure.py和setting两个配置文件
secure.py 存放开发环境和生产环境不一样的,较为私密的参数。 如DEBUG=True
setting.py 存放开发环境和生产环境通用的参数。 如PER_PAGE=15
DEBUG = False
per_page = 15
修改/yushu_book.py:
from httper import HTTP
from flask import current_app # 导入核心对象app
class YuShuBook:
isbn_url = 'http://t.yushu.im/v2/book/isbn/{}'
keyword_url = 'http://t.yushu.im/v2/book/search?q={}&count={}&start={}'
@classmethod
def search_by_isbn(cls, isbn):
url = cls.isbn_url.format(isbn)
result = HTTP.get(url)
return result
@classmethod
def search_by_keyword(cls, keyword, page=1):
url = cls.keyword_url.format(keyword, current_app.config['PER_PAGE'], cls.calculate_start(page))
result = HTTP.get(url)
return result
@staticmethod
def calculate_start(page):
return (page - 1) * current_app.config['PER_PAGE']
修改app/init.py:
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app) # 完成蓝图注册
return app
def register_blueprint(app): # 注册蓝图
from app.web.blueprint import web
app.register_blueprint(web)
六. 数据库模型
app下新建models/book.py:
from sqlalchemy import Column, Integer, String
class Book():
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(50), nullable=False)
author = Column(String(30), default='未名')
bingding = Column(String(20)) # 精装还是平装
publisher = Column(String(50))
price = Column(String(20))
pages = Column(Integer)
pubdata = Column(String(20))
isbn = Column(String(15), nullable=False, unique=True)
summary = Column(String(1000))
image = Column(String(50))
def sample(self):
pass
七. 将模型映射到数据库中
在app/secure.py配置文件中添加mysql的uri:
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://root:@localhost:3306/fisher'
更新models/book.py:
from sqlalchemy import Column, Integer, String
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Book(db.Model):
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(50), nullable=False)
author = Column(String(30), default='未名')
bingding = Column(String(20)) # 精装还是平装
publisher = Column(String(50))
price = Column(String(20))
pages = Column(Integer)
pubdata = Column(String(20))
isbn = Column(String(15), nullable=False, unique=True)
summary = Column(String(1000))
image = Column(String(50))
def sample(self):
pass
更新app/init.py
from flask import Flask
from app.models.book import db
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app) # 注册数据库
db.create_all() # 让模型进行映射
return app
def register_blueprint(app):
from app.web.blueprint import web
app.register_blueprint(web)
运行后报错:
'No application found. Either work inside a view function or push'
RuntimeError: No application found. Either work inside a view function or push an application context. See http://flask-sqlalchemy.pocoo.org/contexts/.
解决上面报错的方法1:
修改app/init.py:
from flask import Flask
from app.models.book import db
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
db.create_all(app=app) # create_all传入app实例
return app
def register_blueprint(app):
from app.web.blueprint import web
app.register_blueprint(web)
解决上面报错的方法2:
修改app/init.py:
from flask import Flask
from app.models.book import db
def create_app():
app = Flask(__name__)
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
with app.app_context():
db.create_all()
return app
def register_blueprint(app):
from app.web.blueprint import web
app.register_blueprint(web)
解决上面报错的方法3:
修改models/book.py一个地方: db = SQLAlchemy()
改为
from fisher import app
db = SQLAlchemy(app)
这样容易导致循环引用,不推荐。
我们为什么能够用这三种方法解决异常呢?
查看db.create_all
的源码:
def create_all(self, bind='__all__', app=None):
"""Creates all tables.
.. versionchanged:: 0.12
Parameters were added
"""
self._execute_for_all_tables(app, bind, 'create_all')
查看_execute_for_all_tables:
def _execute_for_all_tables(self, app, bind, operation, skip_tables=False):
app = self.get_app(app)
···
查看get_app:
def get_app(self, reference_app=None):
"""Helper method that implements the logic to look up an
application."""
if reference_app is not None: # 对应方法1
return reference_app
if current_app: # 对应方法2, 如果current_app为unbound,我们手动push
return current_app._get_current_object()
if self.app is not None: # 对应方法3, 给db的self.app赋值
return self.app
raise RuntimeError(
'No application found. Either work inside a view function or push'
' an application context. See'
' http://flask-sqlalchemy.pocoo.org/contexts/.'
)