Using net/http to serve


承上篇文章,針對wiki 檔案進行http的操作

增加view 功能

  1. 增加一個function,將request URL中的/view/ 過濾,取其後面的值作為文章title
  2. 呼叫 loadPage function
1
2
3
4
5
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
  1. 接著,在main function 中去設定view 路由
  2. 並設定聽port 8080
1
2
3
4
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Editing Pages

  1. 增加編輯和儲存的路由
1
2
3
4
5
6
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
  1. 增加編輯 function
1
2
3
4
5
6
7
8
9
10
11
12
13
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
fmt.Fprintf(w, "<h1>Editing %s</h1>"+
"<form action=\"/save/%s\" method=\"POST\">"+
"<textarea name=\"body\">%s</textarea><br>"+
"<input type=\"submit\" value=\"Save\">"+
"</form>",
p.Title, p.Title, p.Body)
}

但使用 html hardcode有點醜陋

  1. 所以可以使用html/template package
  • 建立一個edit.html
1
2
3
4
5
6
<h1>Editing {{.Title}}</h1>

<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>
  1. 將 edit function 改成使用 html
1
2
3
4
5
6
7
8
9
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
t, _ := template.ParseFiles("edit.html")
t.Execute(w, p)
}

template.ParseFiles會讀取 html 內的內容然後 return *template.Template

  1. 也可以將 view 轉成使用 html 檔案
  • view.html
1
2
3
4
5
<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>

<div>{{printf "%s" .Body}}</div>
  • viewHandler
1
2
3
4
5
6
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
t, _ := template.ParseFiles("view.html")
t.Execute(w, p)
}
  1. 將重複使用的程式共用 function
1
2
3
4
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}

然後將 viewHandler 和改成使用這個 function

1
2
3
4
5
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
1
2
3
4
5
6
7
8
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}

處理不存在的Pages

1
2
3
4
5
6
7
8
9
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}

Saving pages

1
2
3
4
5
6
7
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
p.save()
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

FormValue return type string. 需要轉換值為 []byte 去符合 Page struct

Error Handling

Handle the error in renderTemplate

1
2
3
4
5
6
7
8
9
10
11
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFiles(tmpl + ".html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

fix up saveHandler

1
2
3
4
5
6
7
8
9
10
11
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Template caching

建立一個全域變數
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

修改renderTemplate function

1
2
3
4
5
6
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

Validation

增加regex的格式驗證
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

增加function使用validPath去驗證title

1
2
3
4
5
6
7
8
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return "", errors.New("Invalid Page Title")
}
return m[2], nil // The title is the second subexpression.
}

getTitle function 加入 handlers裡

1
2
3
4
5
6
7
8
9
10
11
12
func viewHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
1
2
3
4
5
6
7
8
9
10
11
func editHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func saveHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err = p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

Literals and Closures

在每個handler中抓取error的情況會使用很多重複的程式碼,可以將每個handlers中的error 和 validation 用一個function包起來

  1. 將每個handler function 加上title string
1
2
3
func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)
  1. 定義function
1
2
3
4
5
6
7
8
9
10
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path) //extract the page title from the Request
if m == nil {
http.NotFound(w, r)
return
}
fn(w, r, m[2]) //call the provided handler 'fn'
}
}

這個return function 是一個closure(閉包)
這個閉包會取得request 路徑上的title, 然後透過TitleValidator驗證他的格式
若title不合法,則error會寫入 ResponseWriter (http.NotFound)
若合法,則此function fn 會被呼叫

  1. 將makeHandler加至 main function
1
2
3
4
5
6
7
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))

log.Fatal(http.ListenAndServe(":8080", nil))
}
  1. 從handler function裡移除getTitle

REFERENCES