GO语言(二十一):编写 Web 应用程序(下)

模板缓存

现在代码中有一处效率低下的地方:每次渲染页面时都调用renderTemplate 。ParseFiles更好的方法是在程序初始化时调用一次,将所有模板解析为单个*Template. 然后我们可以使用该 ExecuteTemplate 方法来渲染特定的模板。

首先,我们创建一个名为templates的全局变量,并用ParseFiles对其进行初始化。

var templates = template.Must(template.ParseFiles("edit.html", "view.html")) 

template.Must函数是一个方便的包装器,当传递一个非零error值时会发生panic,否则返回 *Template未更改的值。panic在这里是合适的;如果无法加载模板,唯一明智的做法是退出程序。

ParseFiles函数采用任意数量的字符串参数来标识我们的模板文件,并将这些文件解析为以基本文件名命名的模板。如果我们要在程序中添加更多模板,我们会将它们的名称添加到ParseFiles调用的参数中。

然后我们修改renderTemplate函数以使用适当模板的名称 调用templates.ExecuteTemplate方法:

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

请注意,模板名称是模板文件名,因此我们必须附加".html"到tmpl参数中。

验证

正如您可能已经观察到的,该程序有一个严重的安全漏洞:用户可以提供任意路径以在服务器上读取/写入。为了缓解这种情况,我们可以编写一个函数来使用正则表达式验证标题。

首先,添加"regexp"到import列表中。然后我们可以创建一个全局变量来存储我们的验证表达式:

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)#34;) 

regexp.MustCompile函数将解析和编译正则表达式,并返回一个regexp.Regexp。 MustCompile与Compile不同之处在于,如果表达式编译失败,它将panic,而Compile返回一个error作为第二个参数。

现在,让我们编写一个函数,使用validPath 表达式来验证路径并提取页面标题:

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

如果标题有效,它将与nil 错误值一起返回。如果标题无效,该函数将向 HTTP 连接写入“404 Not Found”错误,并向处理程序返回错误。要创建新错误,我们必须导入errors 包。

让我们在每个处理程序中调用getTitle:

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

介绍函数文字和闭包

在每个处理程序中捕获错误条件会引入大量重复代码。如果我们可以将每个处理程序包装在一个执行此验证和错误检查的函数中怎么样?Go 的 函数字面量提供了一种强大的抽象功能的方法,可以在这里为我们提供帮助。

首先,我们重写每个处理程序的函数定义以接受标题字符串:

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) 

现在让我们定义一个包装函数,它接受上述类型的函数,并返回一个类型的函数http.HandlerFunc (适合传递给函数http.HandleFunc):

func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Here we will extract the page title from the Request, // and call the provided handler 'fn' } }

返回的函数称为闭包,因为它包含在其外部定义的值。在这种情况下,变量fn( 的单个参数makeHandler)由闭包包围。该变量 fn将是我们的保存、编辑或查看处理程序之一。

现在我们可以从getTitle中获取代码并在此处使用它(稍作修改):

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]) } }

返回的闭包makeHandler是一个接受http.ResponseWriter和 http.Request(换句话说,一个http.HandlerFunc)的函数。闭包title从请求路径中提取 ,并使用正则validPath表达式对其进行验证。如果 title无效,将向 ResponseWriter使用http.NotFound函数写入错误。如果title有效, fn则将使用ResponseWriter、 Request和title作为参数调用封闭的处理程序函数。

makeHandler现在我们可以用in 包装处理函数main,然后再将它们注册到http 包中:

func main() { http.HandleFunc("/view/", makeHandler(viewHandler)) http.HandleFunc("/edit/", makeHandler(editHandler)) http.HandleFunc("/save/", makeHandler(saveHandler)) log.Fatal(http.ListenAndServe(":8080", nil)) }

最后,我们从处理函数中删除对getTitle的调用,使它们更简单:

func viewHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := loadPage(title) if err != nil { 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 := loadPage(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) }

试试看!

重新编译代码,然后运行应用程序:

$ go build wiki.go $ ./wiki

访问http://localhost:8080/view/ANewPage 应该会显示页面编辑表单。然后,您应该能够输入一些文本,单击“保存”,然后被重定向到新创建的页面。

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注