GoLang-web application-The net/http package
Jan 8, 2020
Using net/http to serve
承上篇文章,針對wiki 檔案進行http的操作
增加view 功能
增加一個function,將request URL中的/view/ 過濾,取其後面的值作為文章title
呼叫 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) }
接著,在main function 中去設定view 路由
並設定聽port 8080
1 2 3 4 func main () { http.HandleFunc("/view/" , viewHandler) log.Fatal(http.ListenAndServe(":8080" , nil )) }
Editing Pages
增加編輯和儲存的路由
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 )) }
增加編輯 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有點醜陋
所以可以使用html/template
package
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 >
將 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
也可以將 view 轉成使用 html 檔案
1 2 3 4 5 <h1>{{.Title}}</h1> <p>[<a href="/edit/{{.Title}}" >edit</a>]</p> <div>{{printf "%s" .Body}}</div>
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) }
將重複使用的程式共用 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 }
將 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包起來
將每個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 )
定義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) if m == nil { http.NotFound(w, r) return } fn(w, r, m[2 ]) } }
這個return function 是一個closure(閉包)
這個閉包會取得request 路徑上的title, 然後透過TitleValidator驗證他的格式
若title不合法,則error會寫入 ResponseWriter (http.NotFound)
若合法,則此function fn
會被呼叫
將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 )) }
從handler function裡移除getTitle
REFERENCES