用模板编写网页应用
Last updated
Was this helpful?
Last updated
Was this helpful?
以下程序是用 100 行以内代码实现可行的 wiki 网页应用,它由一组页面组成,用于阅读、编辑和保存。它是来自 Go 网站 codelab 的 wiki 制作教程,我所知的最好的 Go 教程之一,非常值得进行完整的实验,以见证并理解程序是如何被构建起来的()。这里,我们将以自顶向下的视角,从整体上给出程序的补充说明。程序是网页服务器,它必须从命令行启动,监听某个端口,例如 8080。浏览器可以通过请求 URL 阅读 wiki 页面的内容,例如:http://localhost:8080/view/page1
。
接着,页面的文本内容从一个文件中读取,并显示在网页中。它包含一个超链接,指向编辑页面(http://localhost:8080/edit/page1
)。编辑页面将内容显示在一个文本域中,用户可以更改文本,点击“保存”按钮保存到对应的文件中。然后回到阅读页面显示更改后的内容。如果某个被请求阅读的页面不存在(例如:http://localhost:8080/edit/page999
),程序可以作出识别,立即重定向到编辑页面,如此新的 wiki 页面就可以被创建并保存。
wiki 页面需要一个标题和文本内容,它在程序中被建模为如下结构体,Body 字段存放内容,由字节切片组成。
为了在可执行程序之外维护 wiki 页面内容,我们简单地使用了文本文件作为持久化存储。程序、必要的模板和文本文件可以在 中找到。
示例 15.12
让我们来通读代码:
首先导入必要的包。由于我们在构建网页服务器,http
当然是必须的。不过还导入了 io/ioutil
来方便地读写文件,regexp
用于验证输入标题,以及 template
来动态创建 html 文档。
为避免黑客构造特殊输入攻击服务器,我们用如下正则表达式检查用户在浏览器上输入的 URL(同时也是 wiki 页面标题):
makeHandler
会用它对请求管控。
必须有一种机制把 Page
结构体数据插入到网页的标题和内容中,可以利用 template
包通过如下步骤完成:
先在文本编辑器中创建 html 模板文件,例如 view.html:
把要插入的数据结构字段放在 {{
和 }}
之间,这里是把 Page
结构体数据 {{.Title |html}}
和 {{printf "%s" .Body |html}}
插入页面(当然可以是非常复杂的 html,但这里尽可能地简化了,以突出模板的原理。)({{.Title |html}}
和 {{printf "%s" .Body |html}}
语法说明详见后续章节)。
template.Must(template.ParseFiles(tmpl + ".html"))
把模板文件转换为 *template.Template
类型的对象,为了高效,在程序运行时仅做一次解析,在 init()
函数中处理可以方便地达到目的。所有模板对象都被保持在内存中,存放在以 html 文件名作为索引的 map
中:
这种技术被称为模板缓存,是推荐的最佳实践。
为了真正从模板和结构体构建出页面,必须使用:
它基于模板执行,用 Page
结构体对象 p
作为参数对模板进行替换,并写入 ResponseWriter
对象 w
。必须检查该方法的 error
返回值,万一有一个或多个错误,我们可以调用 http.Error()
来明示。在我们的应用程序中,这段代码会被多次调用,所以把它提取为单独的函数 renderTemplate()
。
在此定义了 3 个处理函数,由于包含重复的启动代码,我们将其提取到单独的 makeHandler()
函数中。这是一个值得研究的特殊高阶函数:其参数是一个函数,返回一个新的闭包函数:
闭包封闭了函数变量 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 表单中,它开头是这样的:
这意味着,当提交表单到类似 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}}
。
在 main()
中网页服务器用 ListenAndServe()
启动并监听 8080 端口。但正如 那样,需要先为紧接在 URL localhost:8080/
之后, 以 view
, edit
或 save
开头的 url 路径定义一些处理函数。在大多数网页服务器应用程序中,这形成了一系列 URL 路径到处理函数的映射,类似于 Ruby 和 Rails,Django 或 ASP.NET MVC 这样的 MVC 框架中的路由表。请求的 URL 与这些路径尝试匹配,较长的路径被优先匹配。如不与任何路径匹配,则调用 / 的处理程序。
上一节:
下一节: