• 09-Pagination
    • 发布用户动态
    • 加入动态分页
      • 首页中的分页
      • 个人主页中的分页
    • 更容易地发现和关注用户
    • Links

    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. func init() {
    10. templates = PopulateTemplates()
    11. store = sessions.NewCookieStore([]byte("something-very-secret"))
    12. sessionName = "go-mega"
    13. flashName = "go-flash"
    14. }
    15. ...

    controller/utils.go

    1. ...
    2. func setFlash(w http.ResponseWriter, r *http.Request, message string) {
    3. session, _ := store.Get(r, sessionName)
    4. session.AddFlash(message, flashName)
    5. session.Save(r, w)
    6. }
    7. func getFlash(w http.ResponseWriter, r *http.Request) string {
    8. session, _ := store.Get(r, sessionName)
    9. fm := session.Flashes(flashName)
    10. if fm == nil {
    11. return ""
    12. }
    13. session.Save(r, w)
    14. return fmt.Sprintf("%v", fm[0])
    15. }

    然后我们就可以使用 flash message 来提示 error message了

    现在我们来完成发布Post功能

    vm/index.go

    1. package vm
    2. import "github.com/bonfy/go-mega-code/model"
    3. // IndexViewModel struct
    4. type IndexViewModel struct {
    5. BaseViewModel
    6. Posts []model.Post
    7. Flash string
    8. }
    9. // IndexViewModelOp struct
    10. type IndexViewModelOp struct{}
    11. // GetVM func
    12. func (IndexViewModelOp) GetVM(username string, flash string) IndexViewModel {
    13. u, _ := model.GetUserByUsername(username)
    14. posts, _ := u.FollowingPosts()
    15. v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *posts, flash}
    16. v.SetCurrentUser(username)
    17. return v
    18. }
    19. // CreatePost func
    20. func CreatePost(username, post string) error {
    21. u, _ := model.GetUserByUsername(username)
    22. return u.CreatePost(post)
    23. }

    Notice: 这里我们顺便将 IndexView 里的 Posts 改成 CurrentUser 的 FollowingPosts

    templates/content/index.html

    1. {{define "content"}}
    2. <h1>Hello, {{.CurrentUser}}!</h1>
    3. <form action="/" method="post">
    4. <p><textarea name="body" rows="3" cols="80" value="" placeholder="say something..."></textarea></p>
    5. <p><input type="submit" name="submit" value="Post"></p>
    6. {{ if .Flash }}
    7. <span style="color: red;">[{{.Flash}}]</span>
    8. {{ end }}
    9. </form>
    10. {{range .Posts}}
    11. <table>
    12. <tr valign="top">
    13. <td><img src="{{.User.Avatar}}&s=36"></td>
    14. <td>{{ .User.Username }} says:<br>{{ .Body }}</td>
    15. </tr>
    16. </table>
    17. {{end}}
    18. {{end}}

    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. ...

    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. func init() {
    11. templates = PopulateTemplates()
    12. store = sessions.NewCookieStore([]byte("something-very-secret"))
    13. sessionName = "go-mega"
    14. flashName = "go-flash"
    15. pageLimit = 5
    16. }
    17. ...

    接下来,我需要决定如何将页码并入到应用URL中。 一个相当常见的方法是使用查询字符串参数来指定一个可选的页码,如果没有给出则默认为页面1。 以下是一些示例网址,显示了我将如何实现这一点:

    • 第1页,隐含:http://localhost:8888/
    • 第1页,显式:http://localhost:8888/?page=1
    • 第3页:http://localhost:8888/?page=3

    要访问查询字符串中给出的参数,我们在 utils 中创建一个函数,方便以后调用

    1. ...
    2. func getPage(r *http.Request) int {
    3. url := r.URL // net/url.URL
    4. query := url.Query() // Values (map[string][]string)
    5. q := query.Get("page")
    6. if q == "" {
    7. return 1
    8. }
    9. page, err := strconv.Atoi(q)
    10. if err != nil {
    11. return 1
    12. }
    13. return page
    14. }

    在 vm 中建立分页的 BasePageViewModel

    • PrevPage: 上一页的页码
    • NextPage: 下一页的页码
    • Total: 总页数
    • CurrentPage: 当前页码
    • Limit: 每页显示项目数

    vm/g.go

    1. ...
    2. // BasePageViewModel struct
    3. type BasePageViewModel struct {
    4. PrevPage int
    5. NextPage int
    6. Total int
    7. CurrentPage int
    8. Limit int
    9. }
    10. // SetPrevAndNextPage func
    11. func (v *BasePageViewModel) SetPrevAndNextPage() {
    12. if v.CurrentPage > 1 {
    13. v.PrevPage = v.CurrentPage - 1
    14. }
    15. if (v.Total-1)/v.Limit >= v.CurrentPage {
    16. v.NextPage = v.CurrentPage + 1
    17. }
    18. }
    19. // SetBasePageViewModel func
    20. func (v *BasePageViewModel) SetBasePageViewModel(total, page, limit int) {
    21. v.Total = total
    22. v.CurrentPage = page
    23. v.Limit = limit
    24. v.SetPrevAndNextPage()
    25. }

    首页中的分页

    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. ...

    index 的 vm 中加入 BasePageViewModel

    vm/index.go

    1. ...
    2. type IndexViewModel struct {
    3. BaseViewModel
    4. Posts []model.Post
    5. Flash string
    6. BasePageViewModel
    7. }
    8. // IndexViewModelOp struct
    9. type IndexViewModelOp struct{}
    10. // GetVM func
    11. func (IndexViewModelOp) GetVM(username, flash string, page, limit int) IndexViewModel {
    12. u, _ := model.GetUserByUsername(username)
    13. posts, total, _ := u.FollowingPostsByPageAndLimit(page, limit)
    14. v := IndexViewModel{}
    15. v.SetTitle("Homepage")
    16. v.Posts = *posts
    17. v.Flash = flash
    18. v.SetBasePageViewModel(total, page, limit)
    19. v.SetCurrentUser(username)
    20. return v
    21. }
    22. ...

    显示页中加入页码

    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. {{ if gt .PrevPage 0 }}
    11. <a href="/?page={{.PrevPage}}">Newer posts</a>
    12. {{ end }}
    13. {{ if gt .NextPage 0 }}
    14. <a href="/?page={{.NextPage}}">Older posts</a>
    15. {{ end }}
    16. ...

    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. ...

    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. }

    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. // ProfileViewModelOp struct
    14. type ProfileViewModelOp struct{}
    15. // GetVM func
    16. func (ProfileViewModelOp) GetVM(sUser, pUser string, page, limit int) (ProfileViewModel, error) {
    17. v := ProfileViewModel{}
    18. v.SetTitle("Profile")
    19. u, err := model.GetUserByUsername(pUser)
    20. if err != nil {
    21. return v, err
    22. }
    23. posts, total, _ := model.GetPostsByUserIDPageAndLimit(u.ID, page, limit)
    24. v.ProfileUser = *u
    25. v.Editable = (sUser == pUser)
    26. v.SetBasePageViewModel(total, page, limit)
    27. if !v.Editable {
    28. v.IsFollow = u.IsFollowedByUser(sUser)
    29. }
    30. v.FollowersCount = u.FollowersCount()
    31. v.FollowingCount = u.FollowingCount()
    32. v.Posts = *posts
    33. v.SetCurrentUser(sUser)
    34. return v, nil
    35. }
    36. ...

    templates/content/profile.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. {{ if gt .PrevPage 0 }}
    11. <a href="/user/{{.ProfileUser.Username}}?page={{.PrevPage}}">Newer posts</a>
    12. {{ end }}
    13. {{ if gt .NextPage 0 }}
    14. <a href="/user/{{.ProfileUser.Username}}?page={{.NextPage}}">Older posts</a>
    15. {{ end }}
    16. ...

    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. ...

    09-04

    本小节 Diff

    更容易地发现和关注用户

    相信你已经留意到了,应用没有一个很好的途径来让用户可以找到其他用户进行关注。实际上,现在根本没有办法在页面上查看到底有哪些用户存在。我将会使用少量简单的变更来解决这个问题。

    我将会创建一个新的Explore页面。该页面看起来像是主页,但是却不是只显示已关注用户的动态,而是展示所有用户的全部动态。

    我们现在导航中加入Explore

    templates/_base.html

    1. ...
    2. <a href="/">Home</a>
    3. <a href="/explore">Explore</a>
    4. ...

    然后增加 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. offset := (page - 1) * limit
    7. if err := db.Preload("User").Offset(offset).Limit(limit).Order("timestamp desc").Find(&posts).Error; err != nil {
    8. return nil, total, err
    9. }
    10. db.Model(&Post{}).Count(&total)
    11. return &posts, total, nil
    12. }

    vm/explore.go

    1. package vm
    2. import "github.com/bonfy/go-mega-code/model"
    3. // ExploreViewModel struct
    4. type ExploreViewModel struct {
    5. BaseViewModel
    6. Posts []model.Post
    7. BasePageViewModel
    8. }
    9. // ExploreViewModelOp struct
    10. type ExploreViewModelOp struct{}
    11. // GetVM func
    12. func (ExploreViewModelOp) GetVM(username string, page, limit int) ExploreViewModel {
    13. // posts, _ := model.GetAllPosts()
    14. posts, total, _ := model.GetPostsByPageAndLimit(page, limit)
    15. v := ExploreViewModel{}
    16. v.SetTitle("Explore")
    17. v.Posts = *posts
    18. v.SetBasePageViewModel(total, page, limit)
    19. v.SetCurrentUser(username)
    20. return v
    21. }

    templates/content/explore.html

    1. {{define "content"}}
    2. <h1>Hello, {{.CurrentUser}}!</h1>
    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. {{ if gt .PrevPage 0 }}
    12. <a href="/explore?page={{.PrevPage}}">Newer posts</a>
    13. {{ end }}
    14. {{ if gt .NextPage 0 }}
    15. <a href="/explore?page={{.NextPage}}">Older posts</a>
    16. {{ end }}
    17. {{end}}

    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. http.Handle("/", r)
    14. }
    15. ...
    16. func exploreHandler(w http.ResponseWriter, r *http.Request) {
    17. tpName := "explore.html"
    18. vop := vm.ExploreViewModelOp{}
    19. username, _ := getSessionUser(r)
    20. page := getPage(r)
    21. v := vop.GetVM(username, page, pageLimit)
    22. templates[tpName].Execute(w, &v)
    23. }

    通过这些细小的变更,应用的用户体验得到了大大的提升。现在,用户可以访问发现页来查看陌生用户的动态,并通过这些用户动态来关注用户,而需要的操作仅仅是点击用户名跳转到其个人主页并点击关注链接。令人叹为观止!对吧?

    此时,我建议你在应用上再次尝试一下这个功能,以便体验最后的用户接口的完善。

    09-05

    本小节 Diff

    • 目录
    • 上一节: 08-Follower
    • 下一节: 10-Email-Support