index处理器函数里面的大部分代码都是用来为客户端生成HTML的。首先,函数把每个需要用到的模板文件都放到了Go切片里面(这里展示的是私有页面的模板文件,公开页面的模板文件也是以同样方式进行组织的):

private_tmpl_files := []string{"templates/layout.html",
                               "templates/private.navbar.html",
                               "templates/index.html"}

跟Mustache和CTemplate等其他模板引擎一样,切片指定的这3个HTML文件都包含了特定的嵌入命令,这些命令被称为动作(action),动作在HTML文件里面会被符号包围。

接着,程序会调用ParseFiles函数对这些模板文件进行语法分析,并创建出相应的模板。为了捕捉语法分析过程中可能会产生的错误,程序使用了Must函数去包围ParseFiles函数的执行结果,这样当ParseFiles返回错误的时候,Must函数就会向用户返回相应的错误报告:

templates := template.Must(template.ParseFiles(private_tmpl_files...))

好的,关于模板文件的介绍已经足够多了,现在是时候来看看它们的庐山真面目了。

ChitChat论坛的每个模板文件都定义了一个模板,这种做法并不是强制的,用户也可以在一个模板文件里面定义多个模板,但模板文件和模板一一对应的做法可以给开发带来方便,我们在之后就会看到这一点。代码清单2-7展示了layout.html模板文件的源代码,源代码中使用了define动作,这个动作通过文件开头的{{ define "layout" }}和文件末尾的{{ end }},把被包围的文本块定义成了layout模板的一部分。

代码清单2-7 layout.html模板文件

{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
  <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=9">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>ChitChat</title>
      <link href="/static/css/bootstrap.min.css" rel="stylesheet">
      <link href="/static/css/font-awesome.min.css" rel="stylesheet">
  </head>
  <body>
      {{ template "navbar" . }}    
      <div class="container">
      {{ template "content" . }}
      </div> <!-- /container -->

      <script src="/static/js/jquery-2.1.1.min.js"></script>
      <script src="/static/js/bootstrap.min.js"></script>

  </body>
</html>
{{ end }}

除了define动作之外,layout.html模板文件里面还包含了两个用于引用其他模板文件的template动作。跟在被引用模板名字之后的点.代表了传递给被引用模板的数据,比如{{ template "navbar" . }}语句除了会在语句出现的位置引入navbar模板之外,还会将传递给layout模板的数据传递给navbar模板。

代码清单2-8展示了public.navbar.html模板文件中的navbar模板,除了定义模板自身的define动作之外,这个模板没有包含其他动作(严格来说,模板也可以不包含任何动作)。

{{ define "navbar" }}
<div class="navbar navbar-default navbar-static-top" role="navigation">
    <div class="container">
    <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed"
            data-toggle="collapse" data-target=".navbar-collapse">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="/">
            <i class="fa fa-comments-o"></i>
                ChitChat
        </a>
    </div>
    <div class="navbar-collapse collapse">
        <ul class="nav navbar-nav">
            <li><a href="/">Home</a></li>
        </ul>
        <ul class="nav navbar-nav navbar-right">
            <li><a href="/login">Login</a></li>
        </ul>
    </div>
    </div>
</div>
{{ end }}

最后,让我们来看看定义在index.html模板文件中的content模板,代码清单2-9展示了这个模板的源代码。注意,尽管之前展示的两个模板都与模板文件拥有相同的名字,但实际上模板和模板文件分别拥有不同的名字也是可行的。

代码清单2-9 index.html模板文件

{{ define "content" }}

<p class="lead">
    <a href="/thread/new">Start a thread</a> or join one below!
</p>

{{ range . }}
    <div class="panel panel-default">
        <div class="panel-heading">
            <span class="lead"> <i class="fa fa-comment-o"></i> {{ .Topic }}</span>
        </div>
        <div class="panel-body">
            Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }}  
            posts.      
                <div class="pull-right">
                    <a href="/thread/read?id={{.Uuid }}">Read more</a>
                </div>
        </div>
     </div>

{{ end }}
{{ end }}

index.html文件里面的代码非常有趣,特别值得一提的是文件里面包含了几个以点号.开头的动作,比如

{{ .User.Name }}{{ .CreatedAtDate }},这些动作的作用和之前展示过的index处理器函数有关:

threads, err := data.Threads(); if err == nil {
    templates.ExecuteTemplate(writer, "layout", threads)
}

在以下这行代码中:

templates.ExecuteTemplate(writer, "layout", threads)

程序通过调用ExecuteTemplate函数,执行(execute)已经经过语法分析的layout模板。执行模板意味着把模板文件中的内容和来自其他渠道的数据进行合并,然后生成最终的HTML内容,具体过程如图2-6所示。

图2-6 模板引擎通过合并数据和模板来生成HTML

现在,你应该已经明白了,点号(.)代表的就是传入到模板里面的数据(实际上还不仅如此,接下来的小节会对这方面做进一步的说明)。图2-7展示了程序根据模板生成的ChitChat论坛的样子。

图2-7 ChitChat Web应用示例的主页

整理代码

因为生成HTML的代码会被重复执行很多次,所以我们决定对这些代码进行一些整理,并将它们移到代码清单2-10所示的generateHTML函数里面。

代码清单2-10 generateHTML函数

func generateHTML(w http.ResponseWriter, data interface{}, fn ...string) {
    var files []string  
    for _, file := range fn {
       files = append(files, fmt.Sprintf("templates/%s.html", file))
    }
    templates := template.Must(template.ParseFiles(files...))
    templates.ExecuteTemplate(writer, "layout", data)
}

generateHTML函数接受一个ResponseWriter、一些数据以及一系列模板文件作为参数,然后对给定的模板文件进行语法分析。data参数的类型为空接口类型(empty interface type),这意味着该参数可以接受任何类型的值作为输入。刚开始接触Go语言的人可能会觉得奇怪——Go不是静态编程语言吗,它为什么能够使用没有类型限制的参数?

但实际上,Go程序可以通过接口(interface)机制,巧妙地绕过静态编程语言的限制,并藉此获得接受多种不同类型输入的能力。Go语言中的接口由一系列方法构成,并且每个接口就是一种类型。一个空接口就是一个空集合,这意味着任何类型都可以成为一个空接口,也就是说任何类型的值都可以传递给函数作为参数。

generateHTML函数的最后一个参数以3个点(...)开头,它表示generateHTML函数是一个可变参数函数(variadic func-tion),这意味着这个函数可以在最后的可变参数中接受零个或任意多个值作为参数。generateHTML函数对可变参数的支持使我们可以同时将任意多个模板文件传递给该函数。在Go语言里面,可变参数必须是可变参数函数的最后一个参数。

在实现了generateHTML函数之后,让我们回过头来,继续对index处理器函数进行整理。代码清单2-11展示了经过整理之后的index处理器函数,现在它看上去更整洁了。

代码清单2-11 index处理器函数的最终版本

func index(writer http.ResponseWriter, request *http.Request) {
    threads, err := data.Threads(); if err == nil {
        _, err := session(writer, request)
        if err != nil {
            generateHTML(writer, threads, "layout", "public.navbar", "index")
        } else {
            generateHTML(writer, threads, "layout", "private.navbar", "index")    
        }
    }
}

在这一节中,我们学习了很多关于模板的基础知识,之后的第5章将对模板做更详细的介绍。但是在此之前,让我们先来了解一下ChitChat应用使用的数据源(data source),并藉此了解一下ChitChat应用的数据是如何与模板一同生成最终的HTML的。

results matching ""

    No results matching ""