基于 Go 语言构建在线论坛(三):访问论坛首页

Go 在线论坛首页

整体流程

前面两篇教程学院君分别给大家介绍了基于 Go 语言构建在线论坛的整体设计以及数据表的创建、模型类的编写,今天我们来看看如何在服务端处理用户请求。

用户请求的处理流程如下:

  1. 客户端发送请求;
  2. 服务端路由器(multiplexer)将请求分发给指定处理器(handler);
  3. 处理器处理请求,完成对应的业务逻辑;
  4. 处理器调用模板引擎生成 HTML 并将响应返回给客户端。

接下来我们按照这个流程来编写服务端代码。

定义路由器

这里我们基于 gorilla/mux 来实现路由器,所以需要安装对应依赖:

go get github.com/gorilla/mux

然后我们遵循仿照 Laravel 框架对 Go 路由处理器代码进行拆分这篇教程介绍的组织架构将路由器定义在 routes 目录下的 router.go 中:

package routes

import "github.com/gorilla/mux"

// 返回一个 mux.Router 类型指针,从而可以当作处理器使用
func NewRouter() *mux.Router {

    // 创建 mux.Router 路由器示例
    router := mux.NewRouter().StrictSlash(true)

    // 遍历 web.go 中定义的所有 webRoutes
    for _, route := range webRoutes {
        // 将每个 web 路由应用到路由器
        router.Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    return router
}

将所有路由定义在同一目录的 routes.go 中:

package routes

import "net/http"

// 定义一个 WebRoute 结构体用于存放单个路由
type WebRoute struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

// 声明 WebRoutes 切片存放所有 Web 路由
type WebRoutes []WebRoute

// 定义所有 Web 路由
var webRoutes = WebRoutes{

}

启动 HTTP 服务器

最后在项目根目录下的 main.go 中引入上述路由器来启动 HTTP 服务器:

package main

import (
    . "github.com/xueyuanjun/chitchat/routes"
    "log"
    "net/http"
)

func main()  {
    startWebServer("8080")
}

// 通过指定端口启动 Web 服务器
func startWebServer(port string)  {
    r := NewRouter()
    http.Handle("/", r)  // 通过 router.go 中定义的路由器来分发请求

    log.Println("Starting HTTP service at " + port)
    err := http.ListenAndServe(":" + port, nil) // 启动协程监听请求

    if err != nil {
        log.Println("An error occured starting HTTP listener at port " + port)
        log.Println("Error: " + err.Error())
    }
}

具体代码含义已经在注释中介绍清楚了,这里我们指定 HTTP 服务器监听 8080 端口,使用的路由器正是上述 router.goNewRouter 方法返回的 mux.Router 指针类型实例,这里可以看到引用的时候并没有带上包名前缀,之所以可以这么做是因为通过如下这种方式引入的 routes 包:

. "github.com/xueyuanjun/chitchat/routes"

注意到前面的 . 别名,通过这种方式引入的包可以直接调用包中对外可见的变量、方法和结构体,而不需要加上包名前缀。

还有一种方式是通过 _ 别名引入,这样一来只会调用该包里定义的 init 方法,我们在上篇教程引入 go-sql-driver/mysql 包时就是这么做的:

_ "github.com/go-sql-driver/mysql"

处理静态资源

在线论坛涉及到前端静态资源文件的处理,我们可以在 startWebServer 方法中新增如下这两行代码:

r := NewRouter() // 通过 router.go 中定义的路由器来分发请求

// 处理静态资源文件
assets := http.FileServer(http.Dir("public"))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", assets))

http.Handle("/", r) // 应用路由器到 HTTP 服务器

...

其中 http.FileServer 用于初始化文件服务器和目录为当前目录下的 public 目录。

然后在第二段代码中指定静态资源路由及处理逻辑:将 /static/ 前缀的 URL 请求去除 static 前缀,然后在文件服务器查找指定文件路径是否存在(public 目录下的相对地址)。

比如 URL 请求路径为 http://localhost:8080/static/css/bootstrap.min.css,对应的查找路径是:

<application root>/public/css/bootstrap.min.css

对于静态资源文件直接返回文件内容,不会进行额外处理。

编写处理器实现

首页处理器方法

做好上述准备工作后,接下来,我们来创建论坛首页的路由处理器,在 handlers 目录下新增一个 index.go 来定义首页的处理器方法:

package handlers

import (
    "github.com/xueyuanjun/chitchat/models"
    "html/template"
    "net/http"
)

// 论坛首页路由处理器方法
func Index(w http.ResponseWriter, r *http.Request) {
    files := []string{"views/layout.html", "views/navbar.html", "views/index.html",}
    templates := template.Must(template.ParseFiles(files...))
    threads, err := models.Threads();
    if err == nil {
        templates.ExecuteTemplate(w, "layout", threads)
    }
}

创建视图模板

这里我们使用 Go 自带的 html/template 作为模板引擎,需要传入位于 views 目录下的视图模板文件,这里传入了多个模板文件,包括主布局文件 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 }}

顶部导航模板 navbar.html

{{ 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

{{ 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 }}

引入多个视图模板是为了提高模板代码的复用性,因为对于同一个应用的不同页面来说,可能基本布局、页面顶部导航和页面底部组件都是一样的,关于视图模板的细节,我们在后面视图模板部分会详细介绍,这里简单了解下即可。

渲染视图模板

我们可以从数据库查询群组数据并将该数据传递到模板文件,最后将模板视图渲染出来,对应代码如下:

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

编译多个视图模板时,默认以第一个模板名作为最终视图模板名,所以这里第二个参数传入的是 layout,第三个参数传入要渲染的数据 threads,对应的渲染逻辑位于 views/index.html 中:

{{ 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 }}

其中 {{ range . }} 表示将处理器方法传入的变量,这里是 threads 进行循环。

注册首页路由

最好,我们在 routes/routes.go 中注册首页路由及对应的处理器方法 Index

import "github.com/xueyuanjun/chitchat/handlers"

// 定义所有 Web 路由
var webRoutes = WebRoutes{
    {
        "home",
        "GET",
        "/",
        handlers.Index,
    },
}

访问论坛首页

访问论坛首页之前,我们将相应的前端资源文件拷贝到 public 目录下,此时项目整体目录结构如下:

在线论坛目录结构

注:对应的前端资源可以从项目的 Github 仓库获取:https://github.com/nonfu/chitchat.git

然后我们在项目根目录下运行如下代码启动 HTTP 服务器:

go run main.go

启动 HTTP 服务器

然后我们在浏览器访问论坛首页 http://localhost:8080

访问在线论坛首页

一切与预期一致,下篇教程,我们将基于 Cookie + Session 实现用户认证并创建群组和主题。

上一篇: 基于 Go 语言开发在线论坛(二):通过模型类与 MySQL 数据库交互

下一篇: 基于 Go 语言构建在线论坛(四):通过 Cookie + Session 实现用户认证