8 个人博客
个人博客是一个典型的CMS(内容管理系统),通常包含前台和后台两部分。这一张将涉及更高级的项目组织方式,以及一些新的Python包:Flask-Login,Unidecode。
8.1 大型项目结构
本章将学习使用蓝本,和工厂函数,来进一步组织Flask程序。当一个模块中有太多代码时,常用的做法是将单一模块升级为包。新版本的目录结构如下:
blueblog/ blueprints/ - __init__.py - blog.py - auth.py - admin.py templates/ - admin/ - auth/ - blog/ - base.html - macros.html static/ __init__.py forms.py models.py ...
蓝本提供了更强大的代码组织能力,可以在程序功能层面模块化代码,而不仅仅是代码组织层面。
1、使用蓝本模块化程序
我们可以为蓝本实例注册路由、函数等等,使用上和程序实例很相似,但实际上是不同的。蓝本只是一个模子,把蓝本中的操作附加到程序上,这些操作才能够发挥作用。
- 创建蓝本:
auth_bp = BluePrint('auth', __name__)
使用errorhandler()装饰器可以把错误处理函数注册到蓝本上。你也可以在蓝本上注册视图函数、请求处理函数、模板上下文处理函数,此时他们都只在蓝本局部发生作用。(p223)
注册蓝本:
蓝本要注册到程序实例上,才能发挥作用。url_prefix会各所有注册在蓝本上的视图函数,添加一个url前缀。
app.register_blueprint(auth_bp, url_prefix='/auth')
查看当前程序注册的所有路由:flask route
- 端点:使用
蓝本.视图函数名
的方式访问。 - 资源:
将蓝本模块升级为包,则可以在其中创建蓝本独有的static/
文件夹和templates/
文件夹。在创建蓝本时需要指定他们:
auth_bp = Blueprint('auth', __name__, static_folder='static', templates_folder='templates')
可以将配置写作类属性,通过继承即可派生不同的配置组合。
class BaseConfig(object): SECRET_KEY = ... ... class DevelopmentConfig(BaseConfig): ... config = { 'base': BaseConfig, 'development': DevelopmentConfig }
然后从类导入配置:
config_name = os.getenv('FLASK_CONFIG', 'development') app.config.from_object(config[config_name])
3、使用工厂函数创建程序实例(p229)
这里的工厂函数,即函数的返回值是程序实例,使用它可以在任何地方创建程序实例。
def create_app(config_name=None): ... app = Flask('bluelog') app.config.from_object(config[config_name]) app.register_blueprint(blog_bp) return app
实例化扩展对象时需要传入app程序实例,但使用工厂函数并没有一个创建好的程序实例以传入(也不需要有)。扩展对象一般提供了init_app()方法,可以在其它模块中创建扩展对象,然后在工厂函数中完成初始化。
启动程序:在flask run命令运行时,会在FLASK_APP指定处寻找名为create_app()或者make_app()的工厂函数,自动调用工厂函数创建程序实例。
current_app:可以在工厂函数外调用程序实例独有的属性,如app.config。当实例被创建并运行时,它会自动指向程序实例,转发操作到实例。
8.2 编写程序骨架
这一章的笔记,我主要会记录一些需要注意的小点,会比较零散。
本书BlueBlog的功能分为三个部分:博客前台、用户认证、博客后台。
1、数据库
- 邻接列表关系
博客程序中的评论要支持回复,而回复本身可以算作是评论的一种,因此我们可以定义一种模型内部的一对多关系,每个评论对象可以包含多个子评论(回复)。
class Comment(db.Model): ... replied_id = db.Column(db.Integer, db.ForeignKey('comment.id')) replied = db.relationship('Comment', back_populates='replies', remote_side=[id]) replies = db.relatoinship('Comment', back_populates='replied', cascade='all')
由于关系的两侧都在同一个模型,SQLAlchemy无法分辨关系的两侧(应该指在调用relationship时)。remote_side=[id]将id字段设置为该relationship关系的远程侧,则replied_id字段作为关系的本地侧。即将replied字段作为关系中“多”的一方的属性(一对多关系总是在“多”的一方定义外键)。
这一节好像有点绕,但我感觉比较重要。
生成虚拟数据:(p236)
2、模板
主题:Bootstrap除了使用默认的样式,一些网站上还提供了许多免费的主题,如Bootswatch、StartBootstrap。(p242)
- 模板上下文:在基模板中使用的数据,为了避免在每个视图函数中都传入一次(麻烦),可以注册模板上下文处理函数。
@app.context_processor def make_template_context(): ... return dict(...)
而我之前采用的方法是将数据存储在session中,如用户名和用户id,以此来判断登录状态。
渲染导航链接:导航栏上的按钮应该在对应的页面显示激活状态。可以通过判断请求的端点来实现(p244),并可以包装成一个宏。不过,Bootstrap-Flask已经提供了一个宏:render_nav_item()。
render_nav_item()宏的常用参数:(p245)
Flash消息分类:在调用flask()函数时可以传入消息的类别,见(p245)
3、表单
- 下拉列表的选项(即
<option>
标签)通过参数choices指定。(p247) - “分类”的名称要求不能重复,可以定义一个行内验证器。(p248)
- 使用
Optional
验证器来使字段可以为空。
4、电子邮件支持
如何发送电子邮件,前面的章节已经介绍过。但如果使用异步的方式发送邮件,由于我们的程序实例采用工厂模式构建,而新建线程时要求真正的程序对象来创建上下文。
app = current_app._get_current_object() # 获取被代理的真实对象
8.3 编写博客前台
1、分页显示文章列表
- 截取正文开头:使用
truncate
过滤器。 - 分页
将查询执行函数从all()
换成paginate()
,可以对查询进行分页并获取其中一页的数据。(p254)
page = request.args.get('page', 1, type=1) # 获取哪一页的数据 per_page = 10 # 每一页的数量 pagination = Post.query.paginate(page, per_page=per_page) posts = pagination.items()
Pagination类的属性:(p255)
- 渲染分页部件
可以简单地设置上一页和下一页两个按钮,也可以使用bootstrap提供的render_page()
或render_pagination
宏。
2、显示文章正文
如果使用了富文本编辑器,则正文内容的样式是通过HTML标签来实现的,而jinja2会默认自动过滤掉文本中的html代码。需要使用safe
过滤器,让jinja2把这些文本当作html代码来渲染。
- 文章固定链接
文章的链接常常是如http://example.com/post/120的样子,在后台使用文章id=120来查询文章。你也可以使用一个可读性更强的链接,比如将id换成文章的标题。(p259)
如果想要方便地分享文章,可以提供一个单击复制文章链接的功能。或者,使用社交网站提供的分享API,或直接使用第三方社交分享服务。
3、显示评论列表
评论可以设置在文章页面的底部,可以给评论也添加一个分页导航,还可以使用fragment
关键字向分页按钮的链接中添加URL片段。这样在调整到另一页评论后,会自动跳转到文章下面的评论区域(而不用从文章标题手动滑到下面的评论区)。
{{ render_pagination(pagination, fragment='#comments') }}
url_for()与查询字符串:在使用url_for()
函数构建url时,任何多余的关键字参数都会自动转化为查询字符串。使用request.args
可以获取查询字符串。
4、网站主题切换
可以根据用户的选择加载不同的css文件,来实现主题的切换。这个主题选项可以存放在cookie中,因为每个用户会有不同的选择。