07-Profile-Page-And-Avatar
本章将致力于为应用添加个人主页。个人主页用来展示用户的相关信息,其个人信息由本人录入。 我将为你展示如何动态地生成每个用户的主页,并提供一个编辑页面给他们来更新个人信息。
本章的GitHub链接为: Source, Diff, Zip

Profile Page

作为创建个人主页的第一步,让我们为其URL /user/ 新建一个对应的视图函数。
我们还是老套路 一个 vm 加一个 page 另外再加一个 controllermodel 暂时不涉及新的model)
vm/profile.go
1
package vm
2
3
import "github.com/bonfy/go-mega-code/model"
4
5
// ProfileViewModel struct
6
type ProfileViewModel struct {
7
BaseViewModel
8
Posts []model.Post
9
ProfileUser model.User
10
}
11
12
// ProfileViewModelOp struct
13
type ProfileViewModelOp struct{}
14
15
// GetVM func
16
func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
17
v := ProfileViewModel{}
18
v.SetTitle("Profile")
19
u1, err := model.GetUserByUsername(pUser)
20
if err != nil {
21
return v, err
22
}
23
posts, _ := model.GetPostsByUserID(u1.ID)
24
v.ProfileUser = *u1
25
v.Posts = *posts
26
v.SetCurrentUser(sUser)
27
return v, nil
28
}
Copied!
_base.html 在登陆情况下 加入 Profile 的链接
templates/_base.html
1
...
2
<div>
3
Blog:
4
<a href="/">Home</a>
5
{{if .CurrentUser}}
6
<a href="/user/{{.CurrentUser}}">Profile</a>
7
<a href="/logout">Logout</a>
8
{{else}}
9
<a href="/login">Login</a>
10
{{end}}
11
</div>
12
...
Copied!
templates/content/profile.html
1
{{define "content"}}
2
<h1>User: {{.ProfileUser.Username}}</h1>
3
4
<hr/>
5
6
{{range .Posts}}
7
<p>
8
{{ .User.Username }} says: <b>{{ .Body }}</b>
9
</p>
10
{{end}}
11
{{end}}
Copied!
加入 profileController
controller/home.go
1
package controller
2
3
import (
4
"fmt"
5
"log"
6
"net/http"
7
8
"github.com/bonfy/go-mega-code/vm"
9
"github.com/gorilla/mux"
10
)
11
12
type home struct{}
13
14
func (h home) registerRoutes() {
15
r := mux.NewRouter()
16
r.HandleFunc("/logout", middleAuth(logoutHandler))
17
r.HandleFunc("/login", loginHandler)
18
r.HandleFunc("/register", registerHandler)
19
r.HandleFunc("/user/{username}", middleAuth(profileHandler))
20
r.HandleFunc("/", middleAuth(indexHandler))
21
22
http.Handle("/", r)
23
}
24
25
...
26
27
28
func profileHandler(w http.ResponseWriter, r *http.Request) {
29
tpName := "profile.html"
30
vars := mux.Vars(r)
31
pUser := vars["username"]
32
sUser, _ := getSessionUser(r)
33
vop := vm.ProfileViewModelOp{}
34
v, err := vop.GetVM(sUser, pUser)
35
if err != nil {
36
msg := fmt.Sprintf("user ( %s ) does not exist", pUser)
37
w.Write([]byte(msg))
38
return
39
}
40
templates[tpName].Execute(w, &v)
41
}
42
43
...
Copied!
这里面由于要实现 flask 那样的 /user/username 的效果,快速的方法是引入 gorilla/mux 的第三方package
1
$ go get -v github.com/gorilla/mux
Copied!
然后注意 registerRoutes 函数,里面原来的 http.handleFunc 全部替换成 r.handleFunc 交给第三方的 gorilla/mux来处理,这样我们就可以在 handler里面使用 mux.Vars 来解析URL里面的{username}
另外 严谨起见,这里在 profileHandler 前面加了 auth check,其实不加的话也是OK的,区别就是在登陆之前能不能查看特定 user 的 Profile
我们运行程序,登陆后点击 Profile的链接,就能查看到结果了。
当然你在地址栏里面直接输入 http://127.0.0.1/user/username如果存在,就显示 User 的 Profile,不存在就提示 user (username) does not exist
07-01
本小节 Diff

Avatar

我相信你也觉得我刚刚建立的个人主页非常枯燥乏味。为了使它们更加有趣,我将添加用户头像。与其在服务器上处理大量的上传图片,我将使用Gravatar为所有用户提供图片服务。
Gravatar服务使用起来非常简单。 要请求给定用户的图片,使用格式为https://www.gravatar.com/avatar/ 的URL即可,其中 hash 是用户的电子邮件地址的MD5哈希值。 在下面,你可以看到如何生成电子邮件为[email protected]的用户的Gravatar URL:
如果你想看一个实际的例子,我自己的Gravatar URL是https://www.gravatar.com/avatar/c60f4fa4bf54012b80bc140aab0fc2bc。Gravatar返回的图片如下:
avatar
默认情况下 是 80*80 , 但可以通过向URL的查询字符串添加s参数来请求不同大小的图片。如: https://www.gravatar.com/avatar/c60f4fa4bf54012b80bc140aab0fc2bc?s=128
另一个可传递给Gravatar的有趣参数是d,它让Gravatar为没有向服务注册头像的用户提供的随机头像。 我最喜欢的随机头像类型是“identicon”,它为每个邮箱都返回一个漂亮且不重复的几何设计图片。 如下:
请注意,一些Web浏览器插件(如Ghostery)会屏蔽Gravatar图像,因为它们认为Automattic(Gravatar服务的所有者)可以根据你发送的获取头像的请求来判断你正在访问的网站。 如果在浏览器中看不到头像,你在排查问题的时候可以考虑以下是否在浏览器中安装了此类插件。
由于头像与用户相关联,所以将生成头像URL的逻辑添加到用户模型是有道理的。
如果你对Gravatar服务很有兴趣,可以学习他们的文档
理论说好了,我们来写代码,由于 Go 原生的Template 不像 Jinja2 那么好支持函数( Go Template 是支持自定义函数的,只是要在 template.New().Funcs() 中预先传入,与我们已有的 PopulateTemplates函数集成上有点难度)
Notice: Go Template 支持类的 Func,不用预先传入,这里 Avatar 字段不是特别的必要了,特此说明,可以参见 12-Dates-And-Times的用法
所以这里发挥主观能动性,直接将 Avatar 作为字段放入数据库中,等于冗余了Avatar数据,但是减少了我们coding的难度(我们也乘此机会,fix下上次提到的Gorm format问题)
虽然我们 GeneratePasswordHash 就是 MD5 方法,不过为了逻辑的清晰,我们可以像下面代码这样处理。
model/utils.go
1
package model
2
3
import (
4
"crypto/md5"
5
"encoding/hex"
6
)
7
8
// GeneratePasswordHash : Use MD5
9
func GeneratePasswordHash(pwd string) string {
10
return Md5(pwd)
11
}
12
13
// Md5 func
14
func Md5(origin string) string {
15
hasher := md5.New()
16
hasher.Write([]byte(origin))
17
return hex.EncodeToString(hasher.Sum(nil))
18
}
Copied!
model/user.go
1
...
2
// 说明: User 加入 Avatar、AboutMe、LastSeen 字段
3
type User struct {
4
ID int `gorm:"primary_key"`
5
Username string `gorm:"type:varchar(64)"`
6
Email string `gorm:"type:varchar(120)"`
7
PasswordHash string `gorm:"type:varchar(128)"`
8
LastSeen *time.Time
9
AboutMe string `gorm:"type:varchar(140)"`
10
Avatar string `gorm:"type:varchar(200)"`
11
Posts []Post
12
Followers []*User `gorm:"many2many:follower;association_jointable_foreignkey:follower_id"`
13
}
14
...
15
16
// 说明: 在增加User 的时候,直接设置Avatar
17
18
// SetAvatar func
19
func (u *User) SetAvatar(email string) {
20
u.Avatar = fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", Md5(email))
21
}
22
23
// AddUser func
24
func AddUser(username, password, email string) error {
25
user := User{Username: username, Email: email}
26
user.SetPassword(password)
27
user.SetAvatar(email)
28
return db.Create(&user).Error
29
}
30
...
Copied!
cmd/db_init/main.go
1
package main
2
3
import (
4
"fmt"
5
"log"
6
7
"github.com/bonfy/go-mega-code/model"
8
_ "github.com/jinzhu/gorm/dialects/mysql"
9
)
10
11
func main() {
12
log.Println("DB Init ...")
13
db := model.ConnectToDB()
14
defer db.Close()
15
model.SetDB(db)
16
17
db.DropTableIfExists(model.User{}, model.Post{})
18
db.CreateTable(model.User{}, model.Post{})
19
20
users := []model.User{
21
{
22
Username: "bonfy",
23
PasswordHash: model.GeneratePasswordHash("abc123"),
24
25
Avatar: fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", model.Md5("[email protected]")),
26
Posts: []model.Post{
27
{Body: "Beautiful day in Portland!"},
28
},
29
},
30
{
31
Username: "rene",
32
PasswordHash: model.GeneratePasswordHash("abc123"),
33
34
Avatar: fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", model.Md5("[email protected]")),
35
Posts: []model.Post{
36
{Body: "The Avengers movie was so cool!"},
37
{Body: "Sun shine is beautiful"},
38
},
39
},
40
}
41
42
for _, u := range users {
43
db.Debug().Create(&u)
44
}
45
}
Copied!
这样再运行
1
$ go run cmd/db_init/main.go
Copied!
数据库中的数据就有了 Avatar 这个字段
07-02
现在我们再在 profile.html加入Avatar
这样我的个人主页的顶部有一个不错的大头像,不止如此,底下的所有用户动态都会有一个小头像。
templates/content/profile.html
1
{{define "content"}}
2
3
<table>
4
<tr valign="top">
5
<td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
6
<td><h1>User: {{.ProfileUser.Username}}</h1></td>
7
</tr>
8
</table>
9
10
<hr/>
11
12
{{range .Posts}}
13
<table>
14
<tr valign="top">
15
<td><img src="{{.User.Avatar}}&s=36"></td>
16
<td>{{ .User.Username }} says:<br>{{ .Body }}</td>
17
</tr>
18
</table>
19
{{end}}
20
21
{{end}}
Copied!
运行程序
1
$ go run main.go
Copied!
07-03
本小节 Diff

More Info

新增的个人主页存在的一个问题是,真正显示的内容不够丰富。 用户喜欢在个人主页上展示他们的相关信息,所以我会让他们写一些自我介绍并在这里展示。 我也将跟踪每个用户最后一次访问该网站的时间,并显示在他们的个人主页上。
新字段 last_seenabout_me 已经在前面一起创建过了,我们只要在页面中加入就行了。
现在 model/user.go 中加入 UpdateLastSeen 函数
model/user.go
1
...
2
3
// UpdateUserByUsername func
4
func UpdateUserByUsername(username string, contents map[string]interface{}) error {
5
item, err := GetUserByUsername(username)
6
if err != nil {
7
return err
8
}
9
return db.Model(item).Updates(contents).Error
10
}
11
12
// UpdateLastSeen func
13
func UpdateLastSeen(username string) error {
14
contents := map[string]interface{}{"last_seen": time.Now()}
15
return UpdateUserByUsername(username, contents)
16
}
Copied!
在 middleAuth 中,如果判断用户登陆了,就更新他的 LastSeen 时间 (middleAuth的用法类似于Python 里面的装饰器用法)
Tip: 我们一般是不会在 controller 层直接和 model 层打交道,一般会通过 vm 层去处理,但是由于 middle 层不具有具体的view,我们这里破例直接调用 model 中的方法。 当然考究点,你可以建立一个 middle 的vm,在里面新建一个 UpdateLastSeen 再在 controller/middle 中调用,也是可以的
controller/middle.go
1
package controller
2
3
import (
4
"log"
5
"net/http"
6
7
"github.com/bonfy/go-mega-code/model"
8
)
9
10
func middleAuth(next http.HandlerFunc) http.HandlerFunc {
11
return func(w http.ResponseWriter, r *http.Request) {
12
username, err := getSessionUser(r)
13
log.Println("middle:", username)
14
if username != "" {
15
log.Println("Last seen:", username)
16
model.UpdateLastSeen(username)
17
}
18
if err != nil {
19
log.Println("middle get session err and redirect to login")
20
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
21
} else {
22
next.ServeHTTP(w, r)
23
}
24
}
25
}
26
27
...
Copied!
profile.html 中加入 AboutMe 以及 LastSeen
templates/content/profile.html
1
{{define "content"}}
2
3
<table>
4
<tr valign="top">
5
<td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
6
<td>
7
<h1>User: {{.ProfileUser.Username}}</h1>
8
{{if .ProfileUser.AboutMe}}
9
<p>{{ .ProfileUser.AboutMe }}</p>
10
{{end}}
11
12
{{if .ProfileUser.LastSeen}}
13
<p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
14
{{end}}
15
</td>
16
</tr>
17
</table>
18
19
<hr/>
20
21
{{range .Posts}}
22
<table>
23
<tr valign="top">
24
<td><img src="{{.User.Avatar}}&s=36"></td>
25
<td>{{ .User.Username }} says:<br>{{ .Body }}</td>
26
</tr>
27
</table>
28
{{end}}
29
30
{{end}}
Copied!
07-04
本小节 Diff

Edit Profile

我还需要给用户一个表单,让他们输入一些个人资料。 表单将允许用户更改他们的用户名,并且写一些个人介绍,以存储在新的about_me字段中。
其实细分的话分两步:
    Profile页面加入 Edit 的链接
    增加一个 profile_edit 的页面
加入 Edit 链接: vm 增加 Editable 字段, 然后在 profile.html 中加入链接
model/profile.go
1
package vm
2
3
import "github.com/bonfy/go-mega-code/model"
4
5
// ProfileViewModel struct
6
type ProfileViewModel struct {
7
BaseViewModel
8
Posts []model.Post
9
Editable bool
10
ProfileUser model.User
11
}
12
13
// ProfileViewModelOp struct
14
type ProfileViewModelOp struct{}
15
16
// GetVM func
17
func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
18
v := ProfileViewModel{}
19
v.SetTitle("Profile")
20
u1, err := model.GetUserByUsername(pUser)
21
if err != nil {
22
return v, err
23
}
24
posts, _ := model.GetPostsByUserID(u1.ID)
25
v.ProfileUser = *u1
26
v.Editable = (sUser == pUser)
27
v.Posts = *posts
28
v.SetCurrentUser(sUser)
29
return v, nil
30
}
Copied!
templates/content/profile.html
1
{{define "content"}}
2
3
<table>
4
<tr valign="top">
5
<td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
6
<td>
7
<h1>User: {{.ProfileUser.Username}}</h1>
8
{{if .ProfileUser.AboutMe}}
9
<p><pre>{{ .ProfileUser.AboutMe }}</pre></p>
10
{{end}}
11
12
{{if .ProfileUser.LastSeen}}
13
<p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
14
{{end}}
15
16
{{if .Editable}}
17
<p><a href="/profile_edit">Edit your profile</a></p>
18
{{end}}
19
</td>
20
</tr>
21
</table>
22
23
<hr/>
24
...
Copied!
增加 profile_edit ,这个老套路
model/user.go
1
...
2
3
// UpdateAboutMe func
4
func UpdateAboutMe(username, text string) error {
5
contents := map[string]interface{}{"about_me": text}
6
return UpdateUserByUsername(username, contents)
7
}
Copied!
vm/profile_edit.go
1
package vm
2
3
import "github.com/bonfy/go-mega-code/model"
4
5
// ProfileEditViewModel struct
6
type ProfileEditViewModel struct {
7
LoginViewModel
8
ProfileUser model.User
9
}
10
11
// ProfileEditViewModelOp struct
12
type ProfileEditViewModelOp struct{}
13
14
// GetVM func
15
func (ProfileEditViewModelOp) GetVM(username string) ProfileEditViewModel {
16
17
v := ProfileEditViewModel{}
18
u, _ := model.GetUserByUsername(username)
19
v.SetTitle("Profile Edit")
20
v.SetCurrentUser(username)
21
v.ProfileUser = *u
22
return v
23
}
24
25
// UpdateAboutMe func
26
func UpdateAboutMe(username, text string) error {
27
return model.UpdateAboutMe(username, text)
28
}
Copied!
templates/content/profile_edit.html
1
{{define "content"}}
2
<h1>Profile Edit</h1>
3
<p>Username: {{.ProfileUser.Username}}</p>
4
<form action="/profile_edit" method="post" name="profile_edit">
5
<p>About Me</p>
6
<p><textarea name="aboutme" rows="5" cols="80" value="" placeholder="about me">{{.ProfileUser.AboutMe}}</textarea></p>
7
<p><input type="submit" name="submit" value="Save"></p>
8
</form>
9
10
{{if .Errs}}
11
<ul>
12
{{range .Errs}}
13
<li>{{.}}</li>
14
{{end}}
15
</ul>
16
{{end}}
17
{{end}}
Copied!
最后在 controller 里面加入 profileEditHandler
controller/home.go
1
...
2
3
func (h home) registerRoutes() {
4
r := mux.NewRouter()
5
r.HandleFunc("/logout", middleAuth(logoutHandler))
6
r.HandleFunc("/login", loginHandler)
7
r.HandleFunc("/register", registerHandler)
8
r.HandleFunc("/user/{username}", middleAuth(profileHandler))
9
r.HandleFunc("/profile_edit", middleAuth(profileEditHandler))
10
r.HandleFunc("/", middleAuth(indexHandler))
11
12
http.Handle("/", r)
13
}
14
15
...
16
17
func profileEditHandler(w http.ResponseWriter, r *http.Request) {
18
tpName := "profile_edit.html"
19
username, _ := getSessionUser(r)
20
vop := vm.ProfileEditViewModelOp{}
21
v := vop.GetVM(username)
22
if r.Method == http.MethodGet {
23
err := templates[tpName].Execute(w, &v)
24
if err != nil {
25
log.Println(err)
26
}
27
}
28
29
if r.Method == http.MethodPost {
30
r.ParseForm()
31
aboutme := r.Form.Get("aboutme")
32
log.Println(aboutme)
33
if err := vm.UpdateAboutMe(username, aboutme); err != nil {
34
log.Println("update Aboutme error:", err)
35
w.Write([]byte("Error update aboutme"))
36
return
37
}
38
http.Redirect(w, r, fmt.Sprintf("/user/%s", username), http.StatusSeeOther)
39
}
40
}
Copied!
07-05
07-06
本小节 Diff

Links