Skip to content

Files

Latest commit

00d6687 · Oct 25, 2021

History

History
361 lines (259 loc) · 14.5 KB

File metadata and controls

361 lines (259 loc) · 14.5 KB

四、使用模板

第 2 章服务和路由中,我们探讨了如何在我们的 web 应用程序中获取 URL 并将其转换为不同的页面。在这样做的过程中,我们构建了动态的 URL,并从(非常简单的)net/http处理程序中得到动态响应。

我们将数据表示为真实的 HTML,但我们专门将 HTML 直接硬编码到 Go 源代码中。这对于生产级环境并不理想,原因有很多。

幸运的是,Go 为文本模板和 HTML 模板配备了一个强大但有时很棘手的模板引擎。

与许多其他避免将逻辑作为表示端的一部分的模板语言不同,Go 的模板包使您能够利用一些逻辑构造,例如模板中的循环、变量和函数声明。这允许您将一些逻辑偏移到模板,这意味着可以编写应用程序,但您需要允许模板端在不重写源代码的情况下为产品提供一些扩展性。

我们之所以说一些逻辑结构,是因为 Go 模板是以无逻辑的形式销售的。我们将在稍后讨论有关此主题的更多内容。

在本章中,我们将探讨不仅展示数据的方法,还将探讨本章中一些更高级的可能性。到最后,我们将能够利用我们的模板推进演示和源代码的分离。

我们将讨论以下主题:

  • 介绍模板、上下文和可见性
  • HTML 模板和文本模板
  • 显示变量和安全性
  • 使用逻辑和控制结构

介绍模板、上下文和可见性

很早就值得注意的是,当我们讨论从源代码中去掉 HTML 部分时,可以在 Go 应用程序中使用模板。事实上,如图所示声明模板没有什么错:

tpl, err := template.New("mine").Parse(`<h1>{{.Title}}</h1>`)

但是,如果我们这样做,我们需要在每次模板需要更改时重新启动应用程序。如果我们使用基于文件的模板,情况就不一定如此;相反,我们可以在不重新启动的情况下对表示(和一些逻辑)进行更改。

要从应用程序中的 HTML 字符串移动到基于文件的模板,我们需要做的第一件事是创建一个模板文件。让我们简要地看一下一个示例模板,该模板与我们在本章后面将要讨论的内容有几分相似:

<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
  <h1>{{.Title}}</h1>

  <div>{{.Date}}</div>

  {{.Content}}
</body>
</html>

非常简单,对吗?变量用双花括号内的名称清楚地表示。那么所有的句点是什么呢?与其他一些类似样式的模板系统(小胡子、棱角形等)不同,点表示范围或上下文。

证明这一点的最简单方法是在变量可能重叠的区域。假设我们有一个标题为博客条目的页面,然后我们列出所有已发表的博客文章。我们有一个页面标题,但我们也有单独的条目标题。我们的模板可能与此类似:

{{.Title}}
{{range .Blogs}}
  <li><a href="{{.Link}}">{{.Title}}</a></li>
{{end}}

在本例中,点指定了通过范围模板运算符语法的循环的特定范围。这允许模板解析器正确地利用{{.Title}}作为博客标题而不是页面标题。

这一点非常值得注意,因为我们将要创建的第一个模板将使用通用范围变量,这些变量以点符号作为前缀。

HTML 模板和文本模板

在我们的第一个将博客中的值从数据库显示到 Web 的示例中,我们生成了一个硬编码的 HTML 字符串,并直接注入我们的值。

以下是我们在第三章连接到的两条线路:

  html := `<html><head><title>` + thisPage.Title + `</title></head><body><h1>` + thisPage.Title + `</h1><div>` + thisPage.Content + `</div></body></html>
  fmt.Fprintln(w, html)

不难理解为什么这不是一个将我们的内容输出到网络的可持续系统。最好的方法是将其转换为模板,这样我们就可以将演示文稿与应用程序分开。

为了尽可能简洁地实现这一点,让我们修改调用前面代码[T0]的方法,以使用模板而不是硬编码的 HTML。

因此,我们将删除前面放置的 HTML,而是引用一个文件,该文件将封装我们要显示的内容。从根目录中创建一个templates子目录,并在其中创建blog.html

以下是我们包含的非常基本的 HTML,请随意添加一些技巧:

<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
  <h1>{{.Title}}</h1>
  <p>
    {{.Content}}
  </p>
  <div>{{.Date}}</div>
</body>
</html>

回到我们的应用程序中,在ServePage处理程序中,我们将稍微更改输出代码,以留下一个显式字符串,而不是解析并执行我们刚刚创建的 HTML 模板:

func ServePage(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  pageGUID := vars["guid"]
  thisPage := Page{}
  fmt.Println(pageGUID)
  err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE page_guid=?", pageGUID).Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date)
  if err != nil {
    http.Error(w, http.StatusText(404), http.StatusNotFound)
    log.Println("Couldn't get page!")
    return
  }
  // html := <html>...</html>

  t, _ := template.ParseFiles("templates/blog.html")
  t.Execute(w, thisPage)
}

如果您未能创建该文件或无法访问该文件,应用程序将在尝试执行时死机。如果引用不存在的struct值,您也可能会感到恐慌。我们需要更好地处理错误。

注意:别忘了在你的进口产品中加入html/template

从静态字符串中移开的好处应该是显而易见的,但是现在我们有了一个更可扩展的表示层的基础。

如果我们访问http://localhost:9500/page/hello-world我们会看到类似的东西:

HTML templates and text templates

显示变量和安全性

为了演示这一点,让我们通过将此 SQL 命令添加到 MySQL 命令行来创建一个新的博客条目:

INSERT INTO `pages` (`id`, `page_guid`, `page_title`, page_content`, `page_date`)

价值观:

  (2, 'a-new-blog', 'A New Blog', 'I hope you enjoyed the last blog!  Well brace yourself, because my latest blog is even <i>better</i> than the last!', '2015-04-29 02:16:19');

当然,这是另一个激动人心的内容。但是请注意,当我们尝试更好地将单词斜体化时,我们在其中嵌入了一些 HTML。

尽管存在关于如何存储格式的争论,但这让我们可以了解 Go 的模板在默认情况下是如何处理的。如果我们访问http://localhost:9500/page/a-new-blog我们会看到类似的东西:

Displaying variables and security

如您所见,Go 会自动清理我们的数据以供输出。这样做有很多非常非常明智的理由,这就是为什么它是默认行为。当然,最大的一个是避免 XSS 和代码注入攻击向量来自不可信的输入源,例如站点的普通用户等等。

但从表面上看,我们正在创建这些内容,应该被认为是值得信任的。因此,为了将其验证为受信任的 HTML,我们需要更改template.HTML的类型:

type Page struct {
  Title   string
  Content template.HTML
  Date   string
}

如果您试图简单地将结果 SQL 字符串值扫描到一个[T0]中,您将发现以下错误:

sql: Scan error on column index 1: unsupported driver -> Scan pair: []uint8 -> *template.HTML

解决此问题的最简单方法是将字符串值保留在RawContent中,并将其分配回Content

type Page struct {
  Title    string
  RawContent string
  Content    template.HTML
  Date    string
}
  err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE page_guid=?", pageGUID).Scan(&thisPage.Title, &thisPage.RawContent, &thisPage.Date)
  thisPage.Content = template.HTML(thisPage.RawContent)

如果我们再次这样做,我们将看到我们的 HTML 是可信的:

Displaying variables and security

使用逻辑和控制结构

在本章前面的中,我们研究了如何在模板中使用范围,就像我们在代码中直接使用范围一样。请看下面的代码:

{{range .Blogs}}
  <li><a href="{{.Link}}">{{.Title}}</a></li>
{{end}}

您可能还记得,我们说过 Go 的模板没有任何逻辑,但这取决于您如何定义逻辑,以及共享逻辑是否只存在于应用程序、模板或两者中的一小部分。这是一个小问题,但因为 Go 的模板提供了很大的灵活性;这是一个值得思考的问题。

在前面的模板中有一个范围特性,就其本身而言,为我们的博客提供了许多新的展示方式。我们现在可以显示一个博客列表,或者将我们的博客拆分为段落,并允许每个段落作为一个单独的实体存在。这可以用于允许注释和段落之间的关系,这在近年来一些出版物系统中已开始作为一项功能弹出。

但是现在,让我们利用这个机会在一个新的索引页面中创建一个博客列表。为此,我们需要添加一条路线。既然我们有/page,我们可以用/pages,但既然这是一个索引,我们就用//home

  routes := mux.NewRouter()
  routes.HandleFunc("/page/{guid:[0-9a-zA\\-]+}", ServePage)
  routes.HandleFunc("/", RedirIndex)
  routes.HandleFunc("/home", ServeIndex)
  http.Handle("/", routes)

我们将使用RedirIndex自动重定向到/home端点作为规范主页。

在我们的方法中,提供简单的[T0]或[T1]重定向只需要很少的代码,如图所示:

func RedirIndex(w http.ResponseWriter, r *http.Request) {
  http.Redirect(w, r, "/home", 301)
}

这足以接受来自/的任何请求,并将用户自动带到/home。现在,让我们看看在ServeIndexHTTP 处理程序的索引页上循环浏览我们的博客:

func ServeIndex(w http.ResponseWriter, r *http.Request) {
  var Pages = []Page{}
  pages, err := database.Query("SELECT page_title,page_content,page_date FROM pages ORDER BY ? DESC", "page_date")
  if err != nil {
    fmt.Fprintln(w, err.Error)
  }
  defer pages.Close()
  for pages.Next() {
    thisPage := Page{}
    pages.Scan(&thisPage.Title, &thisPage.RawContent, &thisPage.Date)
    thisPage.Content = template.HTML(thisPage.RawContent)
    Pages = append(Pages, thisPage)
  }
  t, _ := template.ParseFiles("templates/index.html")
  t.Execute(w, Pages)
}

以下是templates/index.html

<h1>Homepage</h1>

{{range .}}
  <div><a href="!">{{.Title}}</a></div>
  <div>{{.Content}}</div>
  <div>{{.Date}}</div>
{{end}}

Using logic and control structures

我们在这里强调了我们的Page struct中的一个问题,我们无法获得对页面GUID的引用。因此,我们需要修改我们的struct以将其作为可导出的Page.GUID变量:

type Page struct {
  Title  string
  Content  template.HTML
  RawContent  string
  Date  string
  GUID   string
}

现在,我们可以将索引页面上的列表链接到各自的博客条目,如图所示:

  var Pages = []Page{}
  pages, err := database.Query("SELECT page_title,page_content,page_date,page_guid FROM pages ORDER BY ? DESC", "page_date")
  if err != nil {
    fmt.Fprintln(w, err.Error)
  }
  defer pages.Close()
  for pages.Next() {
    thisPage := Page{}
    pages.Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date, &thisPage.GUID)
    Pages = append(Pages, thisPage)
  }

我们可以用以下代码更新我们的 HTML 部分:

<h1>Homepage</h1>

{{range .}}
  <div><a href="/page/{{.GUID}}">{{.Title}}</a></div>
  <div>{{.Content}}</div>
  <div>{{.Date}}</div>
{{end}}

但这只是模板威力的开始。如果我们有一段更长的内容,并且想要截断它的描述,会怎么样?

我们可以在Page struct中创建一个新字段并截断它。但这有点笨重;它要求字段始终存在于struct中,无论是否填充了数据。将方法公开给模板本身更有效。

那我们就这么做吧。

首先,创建另一个博客条目,这次具有更大的内容价值。选择您喜欢的内容或选择INSERT命令,如图所示:

INSERT INTO `pages` (`id`, `page_guid`, `page_title`, `page_content`, `page_date`)

价值观:

  (3, 'lorem-ipsum', 'Lorem Ipsum', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sem tortor, lobortis in posuere sit amet, ornare non eros. Pellentesque vel lorem sed nisl dapibus fringilla. In pretium...', '2015-05-06 04:09:45');

注意:为了简洁起见,我们截断了前面 Lorem Ipsum 文本的完整长度。

现在,我们需要将截断表示为Page类型的方法。让我们创建该方法以返回表示缩短文本的字符串。

这里最酷的是,我们可以在应用程序和模板之间共享一个方法:

func (p Page) TruncatedText() string {
  chars := 0
  for i, _ := range p.Content {
    chars++
    if chars > 150 {
      return p.Content[:i] + ` ...`
    }
  }
  return p.Content
}

此代码将循环内容的长度,如果字符数超过150,它将返回索引中该数字的切片。如果没有超过这个数字,TruncatedText将返回整个内容。

在模板中调用这个函数很简单,只是您可能需要一个传统的函数语法调用,例如TruncatedText()。相反,它与范围内的任何变量一样被引用:

<h1>Homepage</h1>

{{range .}}
  <div><a href="/page/{{.GUID}}">{{.Title}}</a></div>
  <div>{{.TruncatedText}}</div>
  <div>{{.Date}}</div>
{{end}}

打电话。TruncatedText实际上,我们通过该方法内联处理值。生成的页面反映了我们现有的博客,而不是被截断的博客,以及我们新的博客条目,其中添加了被截断的文本和省略号:

Using logic and control structures

我相信你可以想象一下,在模板中直接引用嵌入方法可以打开一个展示可能性的世界。

总结

我们刚刚初步了解了 Go 的模板可以做什么,我们将继续探讨更多的主题,但本章希望介绍直接使用模板所需的核心概念。

我们已经研究了简单变量,以及应用程序中的实现方法,以及模板本身。我们还探讨了如何绕过受信任内容的注入保护。

在下一章中,我们将集成一个后端 API,用于以 RESTful 方式访问信息,以读取和操作底层数据。这将允许我们使用 Ajax 在模板上做一些更有趣、更动态的事情。