# 15.6 用模板编写网页应用 以下程序是用 100 行以内代码实现可行的 wiki 网页应用,它由一组页面组成,用于阅读、编辑和保存。它是来自 Go 网站 codelab 的 wiki 制作教程,我所知的最好的 Go 教程之一,非常值得进行完整的实验,以见证并理解程序是如何被构建起来的([https://golang.org/doc/articles/wiki/](https://golang.org/doc/articles/wiki/))。这里,我们将以自顶向下的视角,从整体上给出程序的补充说明。程序是网页服务器,它必须从命令行启动,监听某个端口,例如 8080。浏览器可以通过请求 URL 阅读 wiki 页面的内容,例如:`http://localhost:8080/view/page1`。 接着,页面的文本内容从一个文件中读取,并显示在网页中。它包含一个超链接,指向编辑页面(`http://localhost:8080/edit/page1`)。编辑页面将内容显示在一个文本域中,用户可以更改文本,点击“保存”按钮保存到对应的文件中。然后回到阅读页面显示更改后的内容。如果某个被请求阅读的页面不存在(例如:`http://localhost:8080/edit/page999`),程序可以作出识别,立即重定向到编辑页面,如此新的 wiki 页面就可以被创建并保存。 wiki 页面需要一个标题和文本内容,它在程序中被建模为如下结构体,Body 字段存放内容,由字节切片组成。 ```go type Page struct { Title string Body []byte } ``` 为了在可执行程序之外维护 wiki 页面内容,我们简单地使用了文本文件作为持久化存储。程序、必要的模板和文本文件可以在 [wiki](examples/chapter_15/wiki) 中找到。 示例 15.12 [wiki.go](examples/chapter_15/wiki/wiki.go) ```go package main import ( "net/http" "io/ioutil" "log" "regexp" "text/template" ) const lenPath = len("/view/") var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$") var templates = make(map[string]*template.Template) var err error type Page struct { Title string Body []byte } func init() { for _, tmpl := range []string{"edit", "view"} { templates[tmpl] = template.Must(template.ParseFiles(tmpl + ".html")) } } func main() { http.HandleFunc("/view/", makeHandler(viewHandler)) http.HandleFunc("/edit/", makeHandler(editHandler)) http.HandleFunc("/save/", makeHandler(saveHandler)) err := http.ListenAndServe("localhost:8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err.Error()) } } func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[lenPath:] if !titleValidator.MatchString(title) { http.NotFound(w, r) return } fn(w, r, title) } } func viewHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := load(title) if err != nil { // page not found http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) } func editHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := load(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) } func saveHandler(w http.ResponseWriter, r *http.Request, title string) { 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) } func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { err := templates[tmpl].Execute(w, p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (p *Page) save() error { filename := p.Title + ".txt" // file created with read-write permissions for the current user only return ioutil.WriteFile(filename, p.Body, 0600) } func load(title string) (*Page, error) { filename := title + ".txt" body, err := ioutil.ReadFile(filename) if err != nil { return nil, err } return &Page{Title: title, Body: body}, nil } ``` 让我们来通读代码: - 首先导入必要的包。由于我们在构建网页服务器,`http` 当然是必须的。不过还导入了 `io/ioutil` 来方便地读写文件,`regexp` 用于验证输入标题,以及 `template` 来动态创建 html 文档。 - 为避免黑客构造特殊输入攻击服务器,我们用如下正则表达式检查用户在浏览器上输入的 URL(同时也是 wiki 页面标题): ```go var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$") ``` `makeHandler` 会用它对请求管控。 - 必须有一种机制把 `Page` 结构体数据插入到网页的标题和内容中,可以利用 `template` 包通过如下步骤完成: 1. 先在文本编辑器中创建 html 模板文件,例如 view.html: ```html
[edit]