09-Pagination
在本章,我将告诉你如何对数据列表进行分页。
第八章我们支持了社交网络非常流行的“粉丝”机制。 有了这个功能,接下来我准备好删除一开始就使用的模拟用户动态了。 在本章中,应用将开始接受来自用户的动态更新,并将其发布到网站首页和个人主页。
本章的GitHub链接为: Source, Diff, Zip

发布用户动态

简言之,就是发布Post,首页需要有一个表单,用户可以在其中键入新动态。
不过在这之前,我们先支持下 flash message
controller/g.go
1
...
2
var (
3
homeController home
4
templates map[string]*template.Template
5
sessionName string
6
flashName string
7
store *sessions.CookieStore
8
)
9
10
func init() {
11
templates = PopulateTemplates()
12
store = sessions.NewCookieStore([]byte("something-very-secret"))
13
sessionName = "go-mega"
14
flashName = "go-flash"
15
}
16
...
Copied!
controller/utils.go
1
...
2
3
func setFlash(w http.ResponseWriter, r *http.Request, message string) {
4
session, _ := store.Get(r, sessionName)
5
session.AddFlash(message, flashName)
6
session.Save(r, w)
7
}
8
9
func getFlash(w http.ResponseWriter, r *http.Request) string {
10
session, _ := store.Get(r, sessionName)
11
fm := session.Flashes(flashName)
12
if fm == nil {
13
return ""
14
}
15
session.Save(r, w)
16
return fmt.Sprintf("%v", fm[0])
17
}
Copied!
然后我们就可以使用 flash message 来提示 error message了
现在我们来完成发布Post功能
vm/index.go
1
package vm
2
3
import "github.com/bonfy/go-mega-code/model"
4
5
// IndexViewModel struct
6
type IndexViewModel struct {
7
BaseViewModel
8
Posts []model.Post
9
Flash string
10
}
11
12
// IndexViewModelOp struct
13
type IndexViewModelOp struct{}
14
15
// GetVM func
16
func (IndexViewModelOp) GetVM(username string, flash string) IndexViewModel {
17
u, _ := model.GetUserByUsername(username)
18
posts, _ := u.FollowingPosts()
19
v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *posts, flash}
20
v.SetCurrentUser(username)
21
return v
22
}
23
24
// CreatePost func
25
func CreatePost(username, post string) error {
26
u, _ := model.GetUserByUsername(username)
27
return u.CreatePost(post)
28
}
Copied!
Notice: 这里我们顺便将 IndexView 里的 Posts 改成 CurrentUser 的 FollowingPosts
templates/content/index.html
1
{{define "content"}}
2
<h1>Hello, {{.CurrentUser}}!</h1>
3
4
<form action="/" method="post">
5
6
<p><textarea name="body" rows="3" cols="80" value="" placeholder="say something..."></textarea></p>
7
<p><input type="submit" name="submit" value="Post"></p>
8
9
{{ if .Flash }}
10
<span style="color: red;">[{{.Flash}}]</span>
11
{{ end }}
12
</form>
13
14
15
{{range .Posts}}
16
<table>
17
<tr valign="top">
18
<td><img src="{{.User.Avatar}}&s=36"></td>
19
<td>{{ .User.Username }} says:<br>{{ .Body }}</td>
20
</tr>
21
</table>
22
{{end}}
23
{{end}}
Copied!
controller/home.go
1
...
2
func indexHandler(w http.ResponseWriter, r *http.Request) {
3
tpName := "index.html"
4
vop := vm.IndexViewModelOp{}
5
username, _ := getSessionUser(r)
6
if r.Method == http.MethodGet {
7
flash := getFlash(w, r)
8
v := vop.GetVM(username, flash)
9
templates[tpName].Execute(w, &v)
10
}
11
if r.Method == http.MethodPost {
12
r.ParseForm()
13
body := r.Form.Get("body")
14
errMessage := checkLen("Post", body, 1, 180)
15
if errMessage != "" {
16
setFlash(w, r, errMessage)
17
} else {
18
err := vm.CreatePost(username, body)
19
if err != nil {
20
log.Println("add Post error:", err)
21
w.Write([]byte("Error insert Post in database"))
22
return
23
}
24
}
25
http.Redirect(w, r, "/", http.StatusSeeOther)
26
}
27
}
28
...
Copied!
09-01
Notice: 这里由于我们上章初始化数据 bonfy follow了 rene,所以这里 bonfy 看到的 Index 页面中也有rene的Post,如果登陆rene的账户,是看不到bonfy的Post的,因为 rene 没有follow bonfy
我们现在在输入框中什么都不输入,直接点Post,就能看到 Flash 的 红色提示了
09-02
本小节 Diff

加入动态分页

应用看起来更完善了,但是在主页显示所有用户动态迟早会出问题。如果一个用户有成千上万条关注的用户动态时,会发生什么?你可以想象得到,管理这么大的用户动态列表将会变得相当缓慢和低效。
为了解决这个问题,我会将用户动态进行分页。这意味着一开始显示的只是所有用户动态的一部分,并提供链接来访问其余的用户动态。
我们先在 controller/g.go 中增加页数设置 pageLimit (其实更灵活点,也可以将它放入到配置文件中)
controller/g.go
1
...
2
var (
3
homeController home
4
templates map[string]*template.Template
5
sessionName string
6
flashName string
7
store *sessions.CookieStore
8
pageLimit int
9
)
10
11
func init() {
12
templates = PopulateTemplates()
13
store = sessions.NewCookieStore([]byte("something-very-secret"))
14
sessionName = "go-mega"
15
flashName = "go-flash"
16
pageLimit = 5
17
}
18
...
Copied!
接下来,我需要决定如何将页码并入到应用URL中。 一个相当常见的方法是使用查询字符串参数来指定一个可选的页码,如果没有给出则默认为页面1。 以下是一些示例网址,显示了我将如何实现这一点:
要访问查询字符串中给出的参数,我们在 utils 中创建一个函数,方便以后调用
1
...
2
3
func getPage(r *http.Request) int {
4
url := r.URL // net/url.URL
5
query := url.Query() // Values (map[string][]string)
6
7
q := query.Get("page")
8
if q == "" {
9
return 1
10
}
11
12
page, err := strconv.Atoi(q)
13
if err != nil {
14
return 1
15
}
16
return page
17
}
Copied!
在 vm 中建立分页的 BasePageViewModel
    PrevPage: 上一页的页码
    NextPage: 下一页的页码
    Total: 总页数
    CurrentPage: 当前页码
    Limit: 每页显示项目数
vm/g.go
1
...
2
3
// BasePageViewModel struct
4
type BasePageViewModel struct {
5
PrevPage int
6
NextPage int
7
Total int
8
CurrentPage int
9
Limit int
10
}
11
12
// SetPrevAndNextPage func
13
func (v *BasePageViewModel) SetPrevAndNextPage() {
14
if v.CurrentPage > 1 {
15
v.PrevPage = v.CurrentPage - 1
16
}
17
18
if (v.Total-1)/v.Limit >= v.CurrentPage {
19
v.NextPage = v.CurrentPage + 1
20
}
21
}
22
23
// SetBasePageViewModel func
24
func (v *BasePageViewModel) SetBasePageViewModel(total, page, limit int) {
25
v.Total = total
26
v.CurrentPage = page
27
v.Limit = limit
28
v.SetPrevAndNextPage()
29
}
Copied!

首页中的分页

model 中增加 FollowingPosts 的分页处理
model/user.go
1
...
2
// FollowingPostsByPageAndLimit func
3
func (u *User) FollowingPostsByPageAndLimit(page, limit int) (*[]Post, int, error) {
4
var total int
5
var posts []Post
6
offset := (page - 1) * limit
7
ids := u.FollowingIDs()
8
if err := db.Preload("User").Order("timestamp desc").Where("user_id in (?)", ids).Offset(offset).Limit(limit).Find(&posts).Error; err != nil {
9
return nil, total, err
10
}
11
db.Model(&Post{}).Where("user_id in (?)", ids).Count(&total)
12
return &posts, total, nil
13
}
14
...
Copied!
index 的 vm 中加入 BasePageViewModel
vm/index.go
1
...
2
type IndexViewModel struct {
3
BaseViewModel
4
Posts []model.Post
5
Flash string
6
7
BasePageViewModel
8
}
9
10
// IndexViewModelOp struct
11
type IndexViewModelOp struct{}
12
13
// GetVM func
14
func (IndexViewModelOp) GetVM(username, flash string, page, limit int) IndexViewModel {
15
u, _ := model.GetUserByUsername(username)
16
posts, total, _ := u.FollowingPostsByPageAndLimit(page, limit)
17
v := IndexViewModel{}
18
v.SetTitle("Homepage")
19
v.Posts = *posts
20
v.Flash = flash
21
v.SetBasePageViewModel(total, page, limit)
22
v.SetCurrentUser(username)
23
return v
24
}
25
...
Copied!
显示页中加入页码
templates/content/index.html
1
...
2
{{range .Posts}}
3
<table>
4
<tr valign="top">
5
<td><img src="{{.User.Avatar}}&s=36"></td>
6
<td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> says:<br>{{ .Body }}</td>
7
</tr>
8
</table>
9
{{end}}
10
11
{{ if gt .PrevPage 0 }}
12
<a href="/?page={{.PrevPage}}">Newer posts</a>
13
{{ end }}
14
{{ if gt .NextPage 0 }}
15
<a href="/?page={{.NextPage}}">Older posts</a>
16
{{ end }}
17
...
Copied!
Notice: 这里我们顺便在 Posts显示的时候将 Username 上加了 “/user/username” 的链接,方便我们快速访问用户profile
controller修改成新的 GetVM
1
...
2
func indexHandler(w http.ResponseWriter, r *http.Request) {
3
tpName := "index.html"
4
vop := vm.IndexViewModelOp{}
5
page := getPage(r)
6
username, _ := getSessionUser(r)
7
if r.Method == http.MethodGet {
8
flash := getFlash(w, r)
9
v := vop.GetVM(username, flash, page, pageLimit)
10
templates[tpName].Execute(w, &v)
11
}
12
if r.Method == http.MethodPost {
13
r.ParseForm()
14
body := r.Form.Get("body")
15
errMessage := checkLen("Post", body, 1, 180)
16
if errMessage != "" {
17
setFlash(w, r, errMessage)
18
} else {
19
err := vm.CreatePost(username, body)
20
if err != nil {
21
log.Println("add Post error:", err)
22
w.Write([]byte("Error insert Post in database"))
23
return
24
}
25
}
26
http.Redirect(w, r, "/", http.StatusSeeOther)
27
}
28
}
29
...
Copied!
09-03

个人主页中的分页

与首页分页类似,建立 profile 中的分页
model/post.go
1
...
2
// GetPostsByUserIDPageAndLimit func
3
func GetPostsByUserIDPageAndLimit(id, page, limit int) (*[]Post, int, error) {
4
var total int
5
var posts []Post
6
offset := (page - 1) * limit
7
if err := db.Preload("User").Order("timestamp desc").Where("user_id=?", id).Offset(offset).Limit(limit).Find(&posts).Error; err != nil {
8
return nil, total, err
9
}
10
db.Model(&Post{}).Where("user_id=?", id).Count(&total)
11
return &posts, total, nil
12
}
Copied!
vm/profile.go
1
...
2
// ProfileViewModel struct
3
type ProfileViewModel struct {
4
BaseViewModel
5
Posts []model.Post
6
Editable bool
7
IsFollow bool
8
FollowersCount int
9
FollowingCount int
10
ProfileUser model.User
11
BasePageViewModel
12
}
13
14
// ProfileViewModelOp struct
15
type ProfileViewModelOp struct{}
16
17
// GetVM func
18
func (ProfileViewModelOp) GetVM(sUser, pUser string, page, limit int) (ProfileViewModel, error) {
19
v := ProfileViewModel{}
20
v.SetTitle("Profile")
21
u, err := model.GetUserByUsername(pUser)
22
if err != nil {
23
return v, err
24
}
25
posts, total, _ := model.GetPostsByUserIDPageAndLimit(u.ID, page, limit)
26
v.ProfileUser = *u
27
v.Editable = (sUser == pUser)
28
v.SetBasePageViewModel(total, page, limit)
29
if !v.Editable {
30
v.IsFollow = u.IsFollowedByUser(sUser)
31
}
32
v.FollowersCount = u.FollowersCount()
33
v.FollowingCount = u.FollowingCount()
34
35
v.Posts = *posts
36
v.SetCurrentUser(sUser)
37
return v, nil
38
}
39
...
Copied!
templates/content/profile.html
1
...
2
3
{{range .Posts}}
4
<table>
5
<tr valign="top">
6
<td><img src="{{.User.Avatar}}&s=36"></td>
7
<td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> says:<br>{{ .Body }}</td>
8
</tr>
9
</table>
10
{{end}}
11
12
{{ if gt .PrevPage 0 }}
13
<a href="/user/{{.ProfileUser.Username}}?page={{.PrevPage}}">Newer posts</a>
14
{{ end }}
15
{{ if gt .NextPage 0 }}
16
<a href="/user/{{.ProfileUser.Username}}?page={{.NextPage}}">Older posts</a>
17
{{ end }}
18
...
Copied!
controller/home.go
1
...
2
func profileHandler(w http.ResponseWriter, r *http.Request) {
3
tpName := "profile.html"
4
vars := mux.Vars(r)
5
pUser := vars["username"]
6
sUser, _ := getSessionUser(r)
7
page := getPage(r)
8
vop := vm.ProfileViewModelOp{}
9
v, err := vop.GetVM(sUser, pUser, page, pageLimit)
10
if err != nil {
11
msg := fmt.Sprintf("user ( %s ) does not exist", pUser)
12
w.Write([]byte(msg))
13
return
14
}
15
templates[tpName].Execute(w, &v)
16
}
17
...
Copied!
09-04
本小节 Diff

更容易地发现和关注用户

相信你已经留意到了,应用没有一个很好的途径来让用户可以找到其他用户进行关注。实际上,现在根本没有办法在页面上查看到底有哪些用户存在。我将会使用少量简单的变更来解决这个问题。
我将会创建一个新的Explore页面。该页面看起来像是主页,但是却不是只显示已关注用户的动态,而是展示所有用户的全部动态。
我们现在导航中加入Explore
templates/_base.html
1
...
2
<a href="/">Home</a>
3
<a href="/explore">Explore</a>
4
...
Copied!
然后增加 Explore 页面, 不多说了,老套路
model/post.go
1
...
2
// GetPostsByPageAndLimit func
3
func GetPostsByPageAndLimit(page, limit int) (*[]Post, int, error) {
4
var total int
5
var posts []Post
6
7
offset := (page - 1) * limit
8
if err := db.Preload("User").Offset(offset).Limit(limit).Order("timestamp desc").Find(&posts).Error; err != nil {
9
return nil, total, err
10
}
11
12
db.Model(&Post{}).Count(&total)
13
14
return &posts, total, nil
15
}
Copied!
vm/explore.go
1
package vm
2
3
import "github.com/bonfy/go-mega-code/model"
4
5
// ExploreViewModel struct
6
type ExploreViewModel struct {
7
BaseViewModel
8
Posts []model.Post
9
BasePageViewModel
10
}
11
12
// ExploreViewModelOp struct
13
type ExploreViewModelOp struct{}
14
15
// GetVM func
16
func (ExploreViewModelOp) GetVM(username string, page, limit int) ExploreViewModel {
17
// posts, _ := model.GetAllPosts()
18
posts, total, _ := model.GetPostsByPageAndLimit(page, limit)
19
v := ExploreViewModel{}
20
v.SetTitle("Explore")
21
v.Posts = *posts
22
v.SetBasePageViewModel(total, page, limit)
23
v.SetCurrentUser(username)
24
return v
25
}
Copied!
templates/content/explore.html
1
{{define "content"}}
2
<h1>Hello, {{.CurrentUser}}!</h1>
3
4
{{range .Posts}}
5
<table>
6
<tr valign="top">
7
<td><img src="{{.User.Avatar}}&s=36"></td>
8
<td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> says:<br>{{ .Body }}</td>
9
</tr>
10
</table>
11
{{end}}
12
13
{{ if gt .PrevPage 0 }}
14
<a href="/explore?page={{.PrevPage}}">Newer posts</a>
15
{{ end }}
16
{{ if gt .NextPage 0 }}
17
<a href="/explore?page={{.NextPage}}">Older posts</a>
18
{{ end }}
19
{{end}}
Copied!
controller/home.go
1
...
2
func (h home) registerRoutes() {
3
r := mux.NewRouter()
4
r.HandleFunc("/logout", middleAuth(logoutHandler))
5
r.HandleFunc("/login", loginHandler)
6
r.HandleFunc("/register", registerHandler)
7
r.HandleFunc("/user/{username}", middleAuth(profileHandler))
8
r.HandleFunc("/follow/{username}", middleAuth(followHandler))
9
r.HandleFunc("/unfollow/{username}", middleAuth(unFollowHandler))
10
r.HandleFunc("/profile_edit", middleAuth(profileEditHandler))
11
r.HandleFunc("/explore", middleAuth(exploreHandler))
12
r.HandleFunc("/", middleAuth(indexHandler))
13
14
http.Handle("/", r)
15
}
16
17
...
18
19
func exploreHandler(w http.ResponseWriter, r *http.Request) {
20
tpName := "explore.html"
21
vop := vm.ExploreViewModelOp{}
22
username, _ := getSessionUser(r)
23
page := getPage(r)
24
v := vop.GetVM(username, page, pageLimit)
25
templates[tpName].Execute(w, &v)
26
}
Copied!
通过这些细小的变更,应用的用户体验得到了大大的提升。现在,用户可以访问发现页来查看陌生用户的动态,并通过这些用户动态来关注用户,而需要的操作仅仅是点击用户名跳转到其个人主页并点击关注链接。令人叹为观止!对吧?
此时,我建议你在应用上再次尝试一下这个功能,以便体验最后的用户接口的完善。
09-05
本小节 Diff

Links