mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-12 01:55:35 +08:00
add chapter 15.6 (#682)
This commit is contained in:
181
eBook/15.6.md
Normal file
181
eBook/15.6.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 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` 用于验证输入标题,以及 `tremplate` 来动态创建 html 文档。
|
||||
- 为避免黑客构造特殊输入攻击服务器,我们用如下正则表达式检查用户在浏览器上输入的 URL(同时也是 wiki 页面标题):
|
||||
```go
|
||||
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
|
||||
```
|
||||
`makeHandler` 会用它对请求管控。
|
||||
- 必须有一种机制把 `Page` 结构体数据插入到网页的标题和内容中,可以利用 `template` 包通过如下步骤完成:
|
||||
1. 先在文本编辑器中创建 html 模板文件,例如 view.html:
|
||||
```html
|
||||
<h1>{{.Title |html}}</h1>
|
||||
<p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>
|
||||
<div>{{printf "%s" .Body |html}}</div>
|
||||
```
|
||||
把要插入的数据结构字段放在 `{{` 和 `}}` 之间,这里是把 `Page` 结构体数据 `{{.Title |html}}` 和 `{{printf "%s" .Body |html}}` 插入页面(当然可以是非常复杂的 html,但这里尽可能地简化了,以突出模板的原理。)(`{{.Title |html}}` 和 `{{printf "%s" .Body |html}}` 语法说明详见后续章节)。
|
||||
|
||||
2. `template.Must(template.ParseFiles(tmpl + ".html"))` 把模板文件转换为 `*template.Template` 类型的对象,为了高效,在程序运行时仅做一次解析,在 `init()` 函数中处理可以方便地达到目的。所有模板对象都被保持在内存中,存放在以 html 文件名作为索引的 map 中:
|
||||
```go
|
||||
templates = make(map[string]*template.Template)
|
||||
```
|
||||
此种技术被称为*模板缓存*,是推荐的最佳实践。
|
||||
|
||||
3. 为了真正从模板和结构体构建出页面,必须使用:
|
||||
```go
|
||||
templates[tmpl].Execute(w, p)
|
||||
```
|
||||
它基于模板执行,用 `Page` 结构体对象 p 作为参数对模板进行替换,并写入 `ResponseWriter` 对象 w。必须检查该方法的 error 返回值,万一有一个或多个错误,我们可以调用 `http.Error` 来明示。在我们的应用程序中,这段代码会被多次调用,所以把它提取为单独的函数 `renderTemplate`。
|
||||
- 在 `main()` 中网页服务器用 `ListenAndServe` 启动并监听 8080 端口。但正如 [15.2节](15.2.md) 那样,需要先为紧接在 URL `localhost:8080/` 之后, 以`view`, `edit` 或 `save` 开头的 url 路径定义一些处理函数。在大多数网页服务器应用程序中,这形成了一系列 URL 路径到处理函数的映射,类似于 Ruby 和 Rails,Django 或 ASP.NET MVC 这样的 MVC 框架中的路由表。请求的 URL 与这些路径尝试匹配,较长的路径被优先匹配。如不与任何路径匹配,则调用 / 的处理程序。
|
||||
|
||||
在此定义了 3 个处理函数,由于包含重复的启动代码,我们将其提取到单独的 `markHandler` 函数中。这是一个值得研究的特殊高阶函数:其参数是一个函数,返回一个新的闭包函数:
|
||||
```go
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
- 闭包封闭了函数变量 `fn` 来构造其返回值。但在此之前,它先用 `titleValidator.MatchString(title)` 验证输入标题 `title` 的有效性。如果标题包含了字母和数字以外的字符,就触发 NotFound 错误(例如:尝试 `localhost:8080/view/page++`)。`viewHandler`,`editHandler` 和 `saveHandler` 都是传入 `main()` 中 `makeHandler` 的参数,类型必须都与 `fn` 相同。
|
||||
- `viewHandler` 尝试按标题读取文本文件,这是通过调用 `load()` 函数完成的,它会构建文件名并用 `ioutil.ReadFile` 读取内容。如果文件存在,其内容会存入字符串中。一个指向 `Page` 结构体的指针按字面量被创建:`&Page{Title: title, Body: body}`。
|
||||
|
||||
另外,该值和表示没有 error 的 nil 值一起返回给调用者。然后在 `renderTemplate` 中将该结构体与模板对象整合。
|
||||
|
||||
万一发生错误,也就是说 wiki 页面在磁盘上不存在,错误会被返回给 `viewHandler`,此时会自动重定向,跳转请求对应标题的编辑页面。
|
||||
|
||||
- `editHandler` 基本上也差不多:尝试读取文件,如果存在则用“编辑”模板来渲染;万一发生错误,创建一个新的包含指定标题的 `Page` 对象并渲染。
|
||||
- 当在编辑页面点击“保存”按钮时,触发保存页面内容的动作。按钮须放在 html 表单中,它开头是这样的:
|
||||
```html
|
||||
<form action="/save/{{.Title |html}}" method="POST">
|
||||
```
|
||||
|
||||
这意味着,当提交表单到类似 `http://localhost/save/{Title}` 这样的 URL 格式时,一个 POST 请求被发往网页服务器。针对这样的 URL 我们已经定义好了处理函数:`saveHandler()`。在 request 对象上调用 `FormValue()` 方法,可以提取名称为 body 的文本域内容,用这些信息构造一个 `Page` 对象,然后尝试通过调用 `save()` 方法保存其内容。万一运行失败,执行 `http.Error` 以将错误显示到浏览器。如果保存成功,重定向浏览器到该页的阅读页面。`save()` 函数非常简单,利用 `ioutil.WriteFile()`,写入 `Page` 结构体的 `Body` 字段到文件 `filename` 中,之后会被用于模板替换占位符 `{{printf "%s" .Body |html}}`。
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[确保网页应用健壮](15.5.md)
|
||||
- 下一节:[探索 template 包](15.7.md)
|
2
eBook/examples/chapter_15/wiki/ANewPage.txt
Normal file
2
eBook/examples/chapter_15/wiki/ANewPage.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Testing input of new page!
|
||||
Go Go Go !
|
1
eBook/examples/chapter_15/wiki/TestPage.txt
Normal file
1
eBook/examples/chapter_15/wiki/TestPage.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is a sample Page.
|
6
eBook/examples/chapter_15/wiki/edit.html
Normal file
6
eBook/examples/chapter_15/wiki/edit.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<h1>Editing {{.Title |html}}</h1>
|
||||
|
||||
<form action="/save/{{.Title |html}}" method="POST">
|
||||
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body|html}}</textarea></div>
|
||||
<div><input type="submit" value="Save"></div>
|
||||
</form>
|
2
eBook/examples/chapter_15/wiki/page.txt
Normal file
2
eBook/examples/chapter_15/wiki/page.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Hello Go - World !!!
|
||||
This works great.
|
1
eBook/examples/chapter_15/wiki/page1.txt
Normal file
1
eBook/examples/chapter_15/wiki/page1.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is a test!!
|
3
eBook/examples/chapter_15/wiki/page5.txt
Normal file
3
eBook/examples/chapter_15/wiki/page5.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Page5 is hereby started.
|
||||
This is a first addition.
|
||||
2nd addition.
|
5
eBook/examples/chapter_15/wiki/view.html
Normal file
5
eBook/examples/chapter_15/wiki/view.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<h1>{{.Title |html}}</h1>
|
||||
|
||||
<p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>
|
||||
|
||||
<div>{{printf "%s" .Body |html}}</div>
|
97
eBook/examples/chapter_15/wiki/wiki.go
Normal file
97
eBook/examples/chapter_15/wiki/wiki.go
Normal file
@@ -0,0 +1,97 @@
|
||||
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
|
||||
}
|
32
eBook/examples/chapter_15/wiki/wiki_part1.go
Normal file
32
eBook/examples/chapter_15/wiki/wiki_part1.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func main() {
|
||||
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
|
||||
p1.save()
|
||||
p2, _ := load("TestPage")
|
||||
fmt.Println(string(p2.Body))
|
||||
}
|
39
eBook/examples/chapter_15/wiki/wiki_part2.go
Normal file
39
eBook/examples/chapter_15/wiki/wiki_part2.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const lenPath = len("/view/")
|
||||
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.URL.Path[lenPath:]
|
||||
p, _ := load(title)
|
||||
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/view/", viewHandler)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
Reference in New Issue
Block a user