上章的结束,若在实际开发过程中,会发现一个问题,那就首页或关注分享,是一下子按时间顺序全部显示出来,这在实际项目中不可能出现的,想想实际中的产品是如何做的?
一般来说,无非是两种,一种是使用页码,来进行分页,还有一种是js到页底自动加载,而使用页底自动加载的话,上一章实现的通过tab来区分全部和关注就不可取了,因为无法保证两个tab加载的内容数量一致,导致页面布局就无法实现,所以,这里首页参考tumblr的实现方式,删除关注分享的部分,只保留全部分享,使用js页底动态加载分页方式,同时在导航栏新增两个导航,分别为博文,和关注,使用传统页码的方式显示全部博文和已关注博文,这样是为了有些人可能会查询比较久的历史信息,所以,一个页面,一个功能如何设计,主要取决于业务需求,而不是技术需求。首先修改导航(base.html):
用户登录后,在首页后面会新增两个item,分别是分享和关注,其中关注是一个下拉菜单,分别是“我”关注的用户发布的分享,和“我”关注的用户
下面完成这几个页面,首先是分享页,即所有用户发布的分享,页面与之前的首页很像,首先完成视图模型:
@main.route("/post")
@main.route("/post/")
def post(page=1):
pagination=Post.query.order_by(Post.createtime.desc()).paginate(
page,per_page=current_app.config["POSTS_PER_PAGE"],error_out=False
)
return render_template("posts.html",posts=pagination.items,pagination=pagination,endpoint=request.endpoint)
这个模型的route的意思是,既可以通过/post访问,也可以通过/post/1等类型访问,当/post访问的时候,默认访问第一页。
endpoint的意思为访问的端点,即方法的端点,针对于这个方法来说,endpoint的值为"main.post"
接下来的内容,就是本章的一个重点了,pagination对象,这个是flask-SQLAlchemy框架中的一个很重要的对象,它包含了一系列用于分页的属性,其中主要的属性如下:
has_next
是否还有下一页
has_prev
是否还有前一页
items
当前页的数据
iter_pages(left_edge=2,left_current=2,right_current=5,right_edge=2)
一个关于页面的迭代,可以有四个带有默认值的参数:
- left_edge 页码最左边显示的页数
- left_current 当前页左边显示的页数
- right_current 当前页右边显示的页数
- right_edge 页面最右边显示的页数
可能有些不好理解,举个例子,假设共100页,当前为50页,则显示如下:
1,2...48,49,50,51,52,53,54,55...99,100
next(error_out=False)
下一页的分页对象,当error_out为true时,超过页面返回404
prev(error_out=False)
上一页的分页对象
page
当前页码
prev_num
上页页码
next_num
下页页码
per_page
每页显示的记录数量
total
记录总数
还有更多属性,请查验文档
分享页的模板与首页几乎一样,同样是一个分享发布框,一个已分享列表(posts.html):
{% import "_index_post_macros.html" as macros %}
...
{% if current_user.is_authenticated %}
{{ wtf.quick_form(form) }}
{% endif %}
{{macros.rander_posts(posts,moment,pagination,endpoint)}}
{% if current_user.is_authenticated %}

我已经分享{{ current_user.posts.count() }}条心情
我已经关注了{{ current_user.followed.count() }}名好友
我已经被{{ current_user.followers.count() }}名好友关注
{%endif%}
{% endblock %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock%}
并没有做过多的封装,其实完全可以把右侧在封装成为一个macro
接下来是_index_post_macros.html
{% macro rander_posts(posts,moment,pagination=None,endpoint=None) %}
{% import "_posts_page_macros.html" as macros %}
{% for post in posts %}
{% if post.body_html%}
{{post.body_html|safe}}
{% else %}
{{post.body}}
{% endif %}
{{post.author.nickname}}
发表于 {{ moment( post.createtime).fromNow(refresh=True)}}
{% endfor %}
{% if pagination and endpoint %}
{{macros.rander_page(pagination,endpoint)}}
{% endif %}
{%endmacro%}
这里需要注意的一点也就是最下边新增的代码,意味着macro也可以嵌套,如果pagination和endpoint不为None,则显示页码,而_posts_page_macros.html的代码如下:
{% macro rander_page(pagination,endpoint) %}
{% endmacro %}
这是一个比较典型的pagination的使用方式,完全使用了bootstrap的样式,最终的显示效果如下:
貌似内容有点少,分页我发测试,并且之后关注分页,首页动态分页都要用,所以首先要扩充一些分享内容,扩充的方式多种多样,比如实际数据,手动修改数据库,但对于python来说,它提供了一个不错的生成虚拟数据的轮子,即ForgeryPy,首先当然还是安装:
pip3.6 install ForgeryPy
然后修改Post类,添加一个静态方法(Post.py)
@staticmethod
def generate_fake():
from random import seed, randint;
from .User import User
import forgery_py;
seed()
user_count = User.query.count()
for i in range(100):
u = User.query.offset(randint(0, user_count - 1)).first()
p = Post(body=forgery_py.lorem_ipsum.sentences(randint(1, 3)),
createtime=forgery_py.date.date(True), author=u)
db.session.add(p)
db.session.commit()
几个参数说明一下:
- lorem_ipsum比较有趣,原意为一些排版时的占位用的无意义字符,具体解释可参考阮博的博客,sentences方法为生成普通句子,参数代表句子的数量。
- date的参数表示生成时间的区间,True表示生成的区间都为过去的时间
这个静态方法的使用方式为:
python manage.py shell
Post.generate_fake()
这样就会生成100条分享。
下面在看一下分享页的效果(尾页):
不考虑美工的话,效果还是可以的,不过内容都是英文的,不知道能不能有一个中文的虚拟数据生成器:)
下面是关注分享页,和这个页类似,视图模型为:
@main.route("/follow_post",methods=["GET","POST"])
@main.route("/follow_post/",methods=["GET","POST"])
@login_required
def follow_post(page=1):
form = PostForm()
if form_util(form):
return redirect(url_for(request.endpoint)) # 跳回首页
print(form.body.data)
pagination=Post.query.select_from(Follow).filter_by(follower_id=current_user.id)\
.join(Post,Follow.followed_id == Post.author_id).paginate(
page,per_page=current_app.config["POSTS_PER_PAGE"],error_out=False
)
return render_template("posts.html",posts=pagination.items,form=form,
pagination=pagination,endpoint=request.endpoint)
注意,由于关注分享,分享和首页都有PostForm,所以把这个功能独立出来:
def form_util(form):
if form.validate_on_submit():
post = Post(body=form.body.data, author_id=current_user.id)
db.session.add(post);
return True
return False
其实我想flask应该有整个页面的某一个功能独立为一个视图模型的方式,但我没有找到,如果读者找到了别忘了留言回复
最后,关注的用户就太简单了,可以直接使用某用户关注的页面,将userid参数赋予当前用户的参数即可,最终base.html的导航部分代码为:
然后删除首页中tab的部分,最终代码为:
{% if current_user.is_authenticated %}
{{ wtf.quick_form(form) }}
{% endif %}
{{macros.rander_posts(posts,moment)}}
{% if current_user.is_authenticated %}

我已经分享{{ current_user.posts.count() }}条心情
我已经关注了{{ current_user.followed.count() }}名好友
我已经被{{ current_user.followers.count() }}名好友关注
{%endif%}
这时候,你可能已经发现了,首页的分享还没有进行分页,在本章的开始部分就已经解释道,首页使用动态加载的分页方式,而动态加载显然需要js的配合,使用json的方式向html中注入。这时候服务端就会面临一个问题,如何与客户端的js进行交互呢,这是下一章将要说明的问题。