翻译进度
8
分块数量
2
参与人数

15.6. 在 Web 应用中使用模板

这是一篇社区协同翻译的文章,你可以点击右边区块信息里的『改进』按钮向译者提交改进建议。

下面的程序是一个用来运行 wiki 的 web 应用,它用了不到 100 行代码实现了一组页面的显示、编辑、和保存。它是一个 Go 网站的 codelab 中的一个 wiki 教程,我认为它是最好的 Go 教程 之一;可以通过 wiki 查看完整的代码,可以更好的理解程序是如何构建的。在这里我们将从上到下对整个程序进行补充说明。这个程序是一个 web 服务器,所以它必须在命令行启动(译者注:不要在 IDE 中启动,否则会找不到路径,必须在命令行启动),比如在 8080 端口。浏览器可以通过像这样的 url 来访问 wiki 页面的内容: http://localhost:8080/view/page1

然后会到和这个名字(page1)相同的文本文件中读取文件的内容展示在页面中;页面中包含了一个可以编辑 wiki 页面的超链接( http://localhost:8080/edit/page1 )。编辑页面用一个文本框显示内容,用户可以修改文本并通过 Save 按钮保存到文件中;然后会在相同的页面(view/page1)中查看到被修改的内容。如果想要查看的页面不存在(例如: http://localhost:8080/edit/page999 ),程序会将其跳转到一个编辑页面,这样就可以创建并保存一个新的 wiki 页面。

BroQiang 翻译于 6个月前

这个 wiki 页面需要一个标题和文本内容;它在程序中是由下面的结构体组成,内容是一个叫 Body 的字节切片。

type Page struct {

    Title string

    Body []byte

}

为了在正在运行的程序之外保存我们的 wiki 页面,我们将使用简单的文本文件作为持久性存储。程序、模板和文本文件可以在示例代码的 code_examples/chapter_15/wiki 目录中找到。

示例 15.12—wiki.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(":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 {

        // 找不到页面

        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"

    // 创建一个只有当前用户拥有读写权限的文件

    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

}
BroQiang 翻译于 6个月前

让我们来通读代码:

  • 首先我们要导入需要的包,要构建一个 web 服务器就要有 http 包,io/ioutil 包可以很轻松的对文件进行读写,regexp 用来验证标题的输入,template 可以动态创建我们的 html 文件;我们使用操作系统的错误。

  • 我们希望阻止黑客输入,因为这样会破坏我们的服务器,所以我们将使用下面的正则表达式来检查用户在浏览器中的输入(wiki 页面的标题): var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")

    这个将在 makeHandler 函数中进行控制。

BroQiang 翻译于 6个月前
  • 我们必须有一个将我们的 Page 结构体中的数据插入到 web 页面中的标题和内容中的机制,是通过 template 包来完成的:

    • i )首先在编辑器中创建一个 html 模板文件,例如 view.html :
<h1>{{.Title |html}}</h1>

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

<div>{{printf "%s" .Body |html}}</div>

从数据结构中插入的字段被放在 {{ }} 之间,这里的 {{.Title |html}}{{printf “%s” .Body |html}} 中的数据都是来自 Page 结构体(为了展示原理,示例被尽可能的简化了,当然这里也可以是非常复杂的 html),|htmlprintf "%s 的用法看下面的章节。

BroQiang 翻译于 6个月前
    • ii )template.Must(template.ParseFiles(tmpl + ".html")) 函数将模板文件转换成一个 *template.Template (Template 结构体的指针),为了提高效率,我们只在我们的程序中转换一次,放在 init() 函数中就可以很方便的实现了。这个模板对象被保存在内存中的一个以 html 文件名称为索引的 map 中。
templates = make(map[string]*template.Template)

这种技术被称为 模板缓存 ,并且是非常好的值得推荐的方法。

    • 为了让模板和结构体输出到页面,我们必须使用 templates[tmpl].Execute(w, p) 函数。

    它会调用一个模板,将 Page 结构体 p 作为一个参数在模板中进行替换,并且写入到 ResponseWriter w 中。这个函数必须去检查是否有错误输出;如果出现错误,我们调用 http.Error 来发送信号。这个代码将会在我们的程序中出现多次,所以我们把它分解成一个单独的函数 renderTemplate 。

BroQiang 翻译于 6个月前
  • 在我们的 web 服务器的 main() 中启动一个使用 8080 端口的 ListenAndServe;像 15.2 章节 一样,我们先定义一些处理函数,它们的访问地址是在 localhost:8080/ 后面加上 view、edit 或者 save 作为开始部分(译者注:实际访问的时候还要加上充当持久化存储的文本文件的名称,如: localhost:8080/view/page999 )。在大多数的 web 服务器程序中,这一系列的访问路径的处理函数的形式,就类似于 Ruby and Rails、Django 或者 ASP.NET MVC 这种 MVC 框架的路由表。请求的网址与这些路径的匹配,会先去与最长的路径去匹配;如果没有与任何路径匹配,就会和 / 匹配,/ 对应的处理函数将会被调用(如果存在,不存在就是 404)。

    这里我们定义了 3 个处理函数,并且因为它们包含了重复的代码,我们拆分出了一个 makeHandler 函数。

    这是一个值得研究学习的相当特别的高阶函数;它用一个函数来作为它的第一个参数,并且将这个函数作为一个闭包返回:

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)

    }

}
BroQiang 翻译于 6个月前
  • 这个闭包为了构造它的返回值使用了一个闭合函数变量;但是在此之前,它使用了 titleValidator.MatchString(title) 来验证输入的标题。如果标题不是由字母与数字组成,会发出一个 NotFound 的错误信号。

    (例如用 localhost:8080/view/page++ 来测试一下);在 main() 中的 makeHandler 的参数 viewhandler、edithandler 和 savehandler 都必须与 fn 的参数是相同类型。

  • viewhandler 尝试去读取一个指定标题的文本文件; 这是通过一个 load() 函数来完成的,它重新组合了文件名并通过 ioutil.ReadFile 去读取文件。如果文件被找到,会将它的内容读取到一个本地的字符串类型的 body 变量中。将数据填入指向 Page 结构体的指针中:&Page{Title: title, Body: body}

    并且将这个和一个为 nil 的错误一起返回给调用者。然后这个结构体通过 renderTemplate 来和模板合并。

    如果出现错误,意味着磁盘中不存在 wiki 页面,将错误返回给 viewHandler() , 它对自动的重定向到这个标题对应的编辑页面。

BroQiang 翻译于 6个月前
  • edithandler 几乎是一样的: 尝试去读取文件,如果找到,用它去渲染编辑模板页面:如果出现错误,创建一个新的 Page 对象,然后用这个标题去渲染它(译者注:存在就修改,不存在就添加)。

  • 通过点击编辑页面的保存按钮将页面的内容保存;这个按钮在以 <form action="/save/{{.Title}}" method="POST" > 开头的 html 表单中。

    这意味着当从 http://localhost/save/{Title} (通过模板替换 Title)网址发送一个请求,会被发送到 web 服务器。对于这样的网址,我们定义了一个处理函数:saveHandler() 。通过 request 中的 FormValue() 方法,可以提取名字为 body 的文本域字段的内容,然后通过这个信息构造一个 Page 对象,并尝试通过 save() 函数保存。如果失败,会返回一个 http.Error 显示到浏览器中,如果它成功了,浏览器会重定向一个相同名称的展示页面。save() 函数非常简单: 使用 ioutil.WriteFile() 函数将 Page 结构体的 Body 字段写入一个叫 filename 的文件中。它使用 {{ printf “%s” .Body|html}} (译者注:不明白作者这句是什么意思,这个是模板中用来显示 Body 内容的输出方法)。

BroQiang 翻译于 6个月前

本文章首发在 GolangCaff
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

参与译者:2
讨论数量: 0
发起讨论


暂无话题~