01
发布用户动态
让我们从简单的事情开始吧。首页需要有一个表单,用户可以在其中键入新动态。我创建一个表单类:
代码语言:javascript复制class PostForm(FlaskForm): post = TextAreaField('Say something', validators=[ DataRequired(), Length(min=1, max=140)]) submit = SubmitField('Submit')
然后,我将该表单添加到网站首页的模板中:
代码语言:javascript复制{% extends "base.html" %}
{% block content %} <h1>Hi, {{ current_user.username }}!</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% for post in posts %} <p> {{ post.author.username }} says: <b>{{ post.body }}</b> </p> {% endfor %}{% endblock %}
模板中的变更和处理以前的表单类似。最后的部分是将表单处理逻辑添加到视图函数中:
代码语言:javascript复制from app.forms import PostFormfrom app.models import Post
@app.route('/', methods=['GET', 'POST'])@app.route('/index', methods=['GET', 'POST'])@login_requireddef index(): form = PostForm() if form.validate_on_submit(): post = Post(body=form.post.data, author=current_user) db.session.add(post) db.session.commit() flash('Your post is now live!') return redirect(url_for('index')) posts = [ { 'author': {'username': 'John'}, 'body': 'Beautiful day in Portland!' }, { 'author': {'username': 'Susan'}, 'body': 'The Avengers movie was so cool!' } ] return render_template("index.html", title='Home Page', form=form, posts=posts)
我们来一个个地解读该视图函数的变更:
- 导入
Post
和PostForm
类 - 关联到
index
视图函数的两个路由都新增接受POST
请求,以便视图函数处理接收的表单数据 - 处理表单的逻辑会为
post
表插入一条新的数据 - 模板新增接受
form
对象,以便渲染文本输入框
在继续之前,我想提一些与Web表单处理相关的重要内容。请注意,在处理表单数据后,我通过发送重定向到主页来结束请求。我可以轻松地跳过重定向,并允许函数继续向下进入模板渲染部分,因为这已经是主页视图函数了。
那么,为什么重定向呢?通过重定向来响应Web表单提交产生的POST请求是一种标准做法。这有助于缓解在Web浏览器中执行刷新命令的烦恼。当你点击刷新键时,所有的网页浏览器都会重新发出最后的请求。如果带有表单提交的POST请求返回一个常规的响应,那么刷新将重新提交表单。因为这不是预期的行为,所以浏览器会要求用户确认重复的提交,但是大多数用户却很难理解浏览器询问的内容。不过,如果一个POST
请求被重定向响应,浏览器现在被指示发送GET
请求来获取重定向中指定的页面,所以现在最后一个请求不再是'POST'请求了, 刷新命令就能以更可预测的方式工作。
这个简单的技巧叫做Post/Redirect/Get模式。它避免了用户在提交网页表单后无意中刷新页面时插入重复的动态。
02
展示用户动态
应用看起来更完善了,但是在主页显示所有用户动态迟早会出问题。如果一个用户有成千上万条关注的用户动态时,会发生什么?你可以想象得到,管理这么大的用户动态列表将会变得相当缓慢和低效。
为了解决这个问题,我会将用户动态进行分页。这意味着一开始显示的只是所有用户动态的一部分,并提供链接来访问其余的用户动态。Flask-SQLAlchemy的paginate()
方法原生就支持分页。例如,我想要获取用户关注的前20个动态,我可以将all()
结束调用替换成如下的查询:
>>> user.followed_posts().paginate(1, 20, False).items
Flask-SQLAlchemy的所有查询对象都支持paginate
方法,需要输入三个参数来调用它:
- 从1开始的页码
- 每页的数据量
- 错误处理布尔标记,如果是
True
,当请求范围超出已知范围时自动引发404错误。如果是False
,则会返回一个空列表。
paginate
方法返回一个Pagination
的实例。其items
属性是请求内容的数据列表。Pagination
实例还有一些其他用途,我会在之后讨论。
现在想想如何在index()
视图函数展现分页呢。我先来给应用添加一个配置项,以表示每页展示的数据列表长度吧。
class Config(object): # ... POSTS_PER_PAGE = 3
存储这些应用范围的“可控机关”到配置文件是一个好主意,因为这样我调整时只需去一个地方。 在最终的应用中,每页显示的数据将会大于三,但是对于测试而言,使用小数字很方便。
接下来,我需要决定如何将页码并入到应用URL中。 一个相当常见的方法是使用查询字符串参数来指定一个可选的页码,如果没有给出则默认为页面1。 以下是一些示例网址,显示了我将如何实现这一点:
- 第1页,隐含:http://localhost:5000/index
- 第1页,显式:http://localhost:5000/index?page=1
- 第3页:http://localhost:5000/index?page=3
要访问查询字符串中给出的参数,我可以使用Flask的request.args对象。你已经在第五章中看到了这种方法,我用Flask-Login实现了用户登录的可以包含一个next
查询字符串参数的URL。
给主页和发现页的视图函数添加分页的代码变更如下:
代码语言:javascript复制@app.route('/', methods=['GET', 'POST'])@app.route('/index', methods=['GET', 'POST'])@login_requireddef index(): # ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template('index.html', title='Home', form=form, posts=posts.items)
@app.route('/explore')@login_requireddef explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template("index.html", title='Explore', posts=posts.items)
通过这些更改,这两个路由决定了要显示的页码,可以从page
查询字符串参数获得或是默认值1。然后使用paginate()
方法来检索指定范围的结果。决定页面数据列表大小的POSTS_PER_PAGE
配置项是通过app.config
对象中获取的。
请注意,这些更改非常简单,每次更改都只会影响很少的代码。我试图在编写应用每个部分的时候,不做任何有关其他部分如何工作的假设,这使我可以编写更易于扩展和测试的且兼具模块化和健壮性的应用,并且不太可能失败或出现BUG。
来尝试下分页功能吧。首先确保你有三条以上的用户动态。在发现页面中更方便测试,因为该页面显示所有用户的动态。你现在只会看到最近的三条用户动态。如果你想看接下来的三条,请在浏览器的地址栏中输入*http://localhost:5000/explore?page=2*。
03
分页导航
接下来的改变是在用户动态列表的底部添加链接,允许用户导航到下一页或上一页。还记得我曾提到过paginate()
的返回是Pagination
类的实例吗?到目前为止,我已经使用了此对象的items
属性,其中包含为所选页面检索的用户动态列表。但是这个分页对象还有一些其他的属性在构建分页链接时很有用:
has_next
: 当前页之后存在后续页面时为真has_prev
: 当前页之前存在前置页面时为真next_num
: 下一页的页码prev_num
: 上一页的页码
有了这四个元素,我就可以生成上一页和下一页的链接并将其传入模板以渲染:
代码语言:javascript复制@app.route('/', methods=['GET', 'POST'])@app.route('/index', methods=['GET', 'POST'])@login_requireddef index(): # ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('index', page=posts.next_num) if posts.has_next else None prev_url = url_for('index', page=posts.prev_num) if posts.has_prev else None return render_template('index.html', title='Home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url)
@app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('explore', page=posts.next_num) if posts.has_next else None prev_url = url_for('explore', page=posts.prev_num) if posts.has_prev else None return render_template("index.html", title='Explore', posts=posts.items, next_url=next_url, prev_url=prev_url)
这两个视图函数中的next_url
和prev_url
只有在该方向上存在一个页面时,才会被设置为由url_for()
返回的URL。如果当前页面位于用户动态集合的末尾或者开头,那么Pagination
实例的has_next
或has_prev
属性将为'False',在这种情况下,将设置该方向的链接为None
。
url_for()
函数的一个有趣的地方是,你可以添加任何关键字参数,如果这些参数的名字没有直接在URL中匹配使用,那么Flask将它们设置为URL的查询字符串参数。
现在让我们把它们渲染在index.html模板上,就在用户动态列表的正下方:
代码语言:javascript复制 ... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %} ...
主页和发现页都添加了分页链接。第一个链接标记为“Newer posts”,并指向前一页(请记住,我显示的用户动态按时间的倒序来排序,所以第一页是最新的内容)。第二个链接标记为“Older posts”,并指向下一页的帖子。如果这两个链接中的任何一个都是None
,则通过条件过滤将其从页面中省略。
04
个人主页中的分页
主页分页已经完成,但是,个人主页中也有一个用户动态列表,其中只显示个人主页拥有者的动态。为了保持一致,个人主页也应该实现分页,以匹配主页的分页样式。
我开始更新个人主页视图函数,其中仍然有一个模拟用户动态的列表
代码语言:javascript复制@app.route('/user/<username>')@login_requireddef user(username): user = User.query.filter_by(username=username).first_or_404() page = request.args.get('page', 1, type=int) posts = user.posts.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('user', username=user.username, page=posts.next_num) if posts.has_next else None prev_url = url_for('user', username=user.username, page=posts.prev_num) if posts.has_prev else None return render_template('user.html', user=user, posts=posts.items, next_url=next_url, prev_url=prev_url)
为了得到用户的动态列表,我利用了User
模型中已经定义好的user.posts
一对多关系。 我执行该查询并添加一个order_by()
子句,以便我首先得到最新的用户动态,然后完全按照我对主页和发现页面中的用户动态所做的那样进行分页。 请注意,由url_for()
函数生成的分页链接需要额外的username
参数,因为它们指向个人主页,个人主页依赖用户名作为URL的动态组件。
最后,对user.html模板的更改与我在主页上所做的更改相同:
代码语言:javascript复制 ... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %}
完成对分页功能的实验后,可以将POSTS_PER_PAGE
配置项设置为更合理的值:
class Config(object): # ... POSTS_PER_PAGE = 25