上章的結束,若在實際開發過程中,會發現一個問題,那就首頁或關注分享,是一下子按時間順序全部顯示出來,這在實際項目中不可能出現的,想想實際中的產品是如何做的?
一般來說,無非是兩種,一種是使用頁碼,來進行分頁,還有一種是js到頁底自動加載,而使用頁底自動加載的話,上一章實現的通過tab來區分全部和關注就不可取了,因為無法保證兩個tab加載的內容數量一致,導致頁面布局就無法實現,所以,這里首頁參考tumblr的實現方式,刪除關注分享的部分,只保留全部分享,使用js頁底動態加載分頁方式,同時在導航欄新增兩個導航,分別為博文,和關注,使用傳統頁碼的方式顯示全部博文和已關注博文,這樣是為了有些人可能會查詢比較久的歷史信息,所以,一個頁面,一個功能如何設計,主要取決于業務需求,而不是技術需求。首先修改導航(base.html):
<ul class="nav navbar-nav">
<li><a href="/">首頁</a></li>
<li><a href="#">分享</a></li>
{% if current_user.is_authenticated %}
<li><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false">關注 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">分享</a></li>
<li><a href="#">用戶</a></li>
</ul>
</li>
{% endif %}
</ul>
用戶登錄后,在首頁后面會新增兩個item,分別是分享和關注,其中關注是一個下拉菜單,分別是“我”關注的用戶發布的分享,和“我”關注的用戶
下面完成這幾個頁面,首先是分享頁,即所有用戶發布的分享,頁面與之前的首頁很像,首先完成視圖模型:
@main.route("/post")
@main.route("/post/<int:page>")
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 %}
...
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-8 col-md-8 col-lg-8">
<div>
{% if current_user.is_authenticated %}
{{ wtf.quick_form(form) }}
{% endif %}
</div>
<br>
<div class="tab-content">
<!--全部-->
<div id="all" role="tabpanel" class="tab-pane fade in active">
{{macros.rander_posts(posts,moment,pagination,endpoint)}}
</div>
</div>
</div>
<div class="col-md-4 col-md-4 col-lg-4">
<!--這里 當沒有用戶登錄的時候 顯示熱門分享列表 稍后實現-->
{% if current_user.is_authenticated %}

<br><br>
<p class="text-muted">我已經分享<span class="text-danger">{{ current_user.posts.count() }}</span>條心情</p>
<p class="text-muted">我已經關注了<span class="text-danger">{{ current_user.followed.count() }}</span>名好友</p>
<p class="text-muted">我已經被<span class="text-danger">{{ current_user.followers.count() }}</span>名好友關注</p>
{%endif%}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
<script type="text/javascript">
$('.nav-tabs a').click(function (e) {
e.preventDefault()
$(this).tab('show')
})
</script>
{% 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 %}
<div class="bs-callout
{% if loop.index % 2 ==0 %}
bs-callout-d
{% endif %}
{% if loop.last %}
bs-callout-last
{% endif %}" >
<div class="row">
<div class="col-sm-2 col-md-2">
<!--使用測試域名-->
<a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">

</a>
</div>
<div class="col-sm-10 col-md-10">
<div>
<p>
{% if post.body_html%}
{{post.body_html|safe}}
{% else %}
{{post.body}}
{% endif %}
</p>
</div>
<div>
<a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">{{post.author.nickname}}</a>
<span class="text-right">發表于 {{ moment( post.createtime).fromNow(refresh=True)}}</span>
</div>
</div>
</div>
</div>
{% 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) %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm">
{% if pagination.has_prev %}
<li>
<a href="{{url_for(endpoint,page=pagination.page-1)}}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{% for p in pagination.iter_pages() %}
{% if p%}
{% if p ==pagination.page%}
<li class="active"><a href="#">{{p}}</a></li>
{% else %}
<li><a href="{{url_for(endpoint,page=p)}}">{{p}}</a></li>
{% endif %}
{% else %}
<li class="disabled"><a href="#">...</a></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li>
<a href="{{url_for(endpoint,page=pagination.page+1)}}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% 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/<int:page>",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的導航部分代碼為:
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="/">首頁</a></li>
<li><a href="{{url_for('main.post')}}">分享</a></li>
{% if current_user.is_authenticated %}
<li><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false">關注 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{url_for('main.follow_post')}}">分享</a></li>
<li><a href="{{url_for('main.follow_list',type='followed',userid=current_user.id)}}">用戶</a></li>
</ul>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><p class="navbar-text"><a href="#" class="navbar-link">{{current_user.username}}</a> 您好</p></li>
<li><a href="{{url_for('auth.logout')}}">登出</a></li>
{% else %}
<li><a href="{{url_for('auth.login')}}">登錄</a></li>
<li><a href="{{url_for('auth.register')}}">注冊</a></li>
{% endif %}
</ul>
<form class="navbar-form navbar-right">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">搜索</button>
</form>
</div><!-- /.navbar-collapse -->
然后刪除首頁中tab的部分,最終代碼為:
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-8 col-md-8 col-lg-8">
<div>
{% if current_user.is_authenticated %}
{{ wtf.quick_form(form) }}
{% endif %}
</div>
<br>
<ul class="nav nav-tabs">
<li role="presentation" class="active"><a href="#all">最新分享</a></li>
</ul>
<div class="tab-content">
<!--全部-->
<div id="all" role="tabpanel" class="tab-pane fade in active">
{{macros.rander_posts(posts,moment)}}
</div>
</div>
</div>
<div class="col-md-4 col-md-4 col-lg-4">
<!--這里 當沒有用戶登錄的時候 顯示熱門分享列表 稍后實現-->
{% if current_user.is_authenticated %}

<br><br>
<p class="text-muted">我已經分享<span class="text-danger">{{ current_user.posts.count() }}</span>條心情</p>
<p class="text-muted">我已經關注了<span class="text-danger">{{ current_user.followed.count() }}</span>名好友</p>
<p class="text-muted">我已經被<span class="text-danger">{{ current_user.followers.count() }}</span>名好友關注</p>
{%endif%}
</div>
</div>
</div>
這時候,你可能已經發現了,首頁的分享還沒有進行分頁,在本章的開始部分就已經解釋道,首頁使用動態加載的分頁方式,而動態加載顯然需要js的配合,使用json的方式向html中注入。這時候服務端就會面臨一個問題,如何與客戶端的js進行交互呢,這是下一章將要說明的問題。