基于 Go 语言构建在线论坛(三):访问论坛首页
整体流程
前面两篇教程学院君分别给大家介绍了基于 Go 语言构建在线论坛的整体设计以及数据表的创建、模型类的编写,今天我们来看看如何在服务端处理用户请求。
用户请求的处理流程如下:
- 客户端发送请求;
- 服务端路由器(multiplexer)将请求分发给指定处理器(handler);
- 处理器处理请求,完成对应的业务逻辑;
- 处理器调用模板引擎生成 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.go
中 NewRouter
方法返回的 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://localhost:8080
:
一切与预期一致,下篇教程,我们将基于 Cookie + Session 实现用户认证并创建群组和主题。
21 Comments
golang 的优势确实比PHP大,不仅蚕食的Java的份额,连py 和PHP也没有放过。今年上半年应该能挤进编程语言 TOP10 了。
已经挤进了 ?♂️
这里有个疑问,
models.Threads()
读取出来的数据应该是由获取到的5个字段;但模板渲染的时候
{{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }}
这三个字段值是怎么来的呢?models/thread.go
中确实定义了CreatedAtDate
方法,难不成模板里面会自动调用这些定义的方法吗?他们怎么关联到一起的呢?还有那个{{ .User.Name }}
又是怎么拿到的值呢?这里看的确实有点懵,还麻烦学院君释疑~前面的
.
代表的是从处理器方法传入的 thread 这一块会在后面视图模板部分详细介绍有个大大的疑问,在layout模板文件里面导入了样式表路径是/static/css/bootstrap.min.css",但看文件结构应该是“../public/css/bootstrap.min.css”,请帮忙解惑一下,是否是入口文件设置了静态处理也能运用到这里,如果是,烦请告诉流程。
处理静态资源 这一节已经有相应的代码和解释
首页处理器方法
这里会查询数据库的 thread 数据,没有异常才会去解析 layout 模板,但这里没有做异常处理。
你可以打印一下报错
我估计你是
db.go
数据库连接信息没改为自己本地的,db, err := sql.Open("mysql", "用户名:密码@tcp(IP:端口)/数据库?charset=utf8")
http: panic serving 127.0.0.1:59176: runtime error: invalid memory address or nil pointer dereference
找不到错哪
go run main.go 能启动起来 http 服务器吗