介绍常见 web 服务的实现与输入、输出数据处理。包括:静态文件服务、js 请求支持、模板输出、表单处理、Filter 中间件设计。
访问 html 静态网页是 web 基础服务。
代码与文件结构:: github.com/pmlpml/golang-learning/web/cloudgo-static
1.1 使用文件服务
一般来说,生产环境静态文件的访问交给 WEB 服务器 Apache / Lighttpd / Nginx 处理。在开发、测试阶段也可以让 net/http 库处理。
server.go
package service
import (
"net/http"
"os"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/unrolled/render"
)
// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {
formatter := render.New(render.Options{
IndentJSON: true,
})
n := negroni.Classic()
mx := mux.NewRouter()
initRoutes(mx, formatter)
n.UseHandler(mx)
return n
}
func initRoutes(mx *mux.Router, formatter *render.Render) {
webRoot := os.Getenv("WEBROOT")
if len(webRoot) == 0 {
if root, err := os.Getwd(); err != nil {
panic("Could not retrive working directory")
} else {
webRoot = root
//fmt.Println(root)
}
}
//mx.HandleFunc("/api/test", apiTestHandler(formatter)).Methods("GET")
mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
}
Go 的 net/http 包中提供了静态文件的服务,ServeFile
和 FileServer
等函数。
首先我们需要在服务器上创建目录,以存放静态内容。例如:
assets(静态文件虚拟根目录)
|-- js
|-- images
+-- css
仅一条语句就实现了 mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
静态文件服务。它的含义是将 path 以 “/” 前缀的 URL 都定位到 webRoot + "/assets/"
为虚拟根目录的文件系统。 有必要描述这语句中的函数:
http.Dir
是类型。将字符串转为 http.Dir
类型,这个类型实现了 FileSystem
接口。(Dir 不是函数)http.FileServer()
是函数,返回 Handler
接口,该接口处理 http 请求,访问 root
的文件请求。mx.PathPrefix
添加前缀路径路由。 创建 assets
目录,不要向该目录放任何内容, 运行程序 go run main.go
!
首先,用浏览器 http;//localhost:8080/
访问。在缺少文件情况下,观察 FileServer 的行为。逐步添加 css
,js
等目录与文件,确认是否可以浏览文件。直到,根目录添加 index.html 看发生了什么?
问题:为什么会添加了 index.html
就显示网页呢? 自己跟踪代码!
1.2 支持 JavaScript 访问
随着 web 页面技术的进步,页面中大量使用 javascript。 添加一个服务:
apitest.go
package service
import (
"net/http"
"github.com/unrolled/render"
)
func apiTestHandler(formatter *render.Render) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
formatter.JSON(w, http.StatusOK, struct {
ID string `json:"id"`
Content string `json:"content"`
}{ID: "8675309", Content: "Hello from Go!"})
}
}
这段代码非常简单,输出了一个 匿名结构 ,并 JSON (JavaScript Object Notation) 序列化输出。 打开前面程序的注释,运行网站并用 curl 测试输出!
$ curl http://localhost:8080/api/test
{
"id": "8675309",
"content": "Hello from Go!"
}
为了便于理解,课程给的案例非常简答。index.html 是
<html>
<head>
<link rel="stylesheet" href="css/main.css"/>
<script src="http://code.jquery.com/jquery-latest.js">script>
<script src="js/hello.js">script>
head>
<body>
<img src="images/cng.png" height="48" width="48"/>
Sample Go Web Application!!
<div>
<p class="greeting-id">The ID is p>
<p class="greeting-content">The content is p>
div>
body>
html>
使用的 hello.js 是:
$(document).ready(function() {
$.ajax({
url: "/api/test"
}).then(function(data) {
$('.greeting-id').append(data.id);
$('.greeting-content').append(data.content);
});
});
通过 web 应用控制台 Negroni 输出追踪,获知网页使用 javascript 获取了信息。
现在,你应该可以顺利的与 VUE,Bootstrap 这些做的应用前端与 golang 集成在一起了!
问题: 交互路由两个语句位置,会发生什么?
1.3 处理静态路径前缀
在 web 应用中,部分应用会将所有静态文件访问路径用独立前缀,例如: http://localhost:8080/static/js/hello.js
,这时路由如何设置呢?
mx.PathPrefix("/static").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
显然,404
页面出现了。
正确的代码是:
mx.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(webRoot+"/assets/"))))
请自己研究 StripPrefix
的实现。好奇妙的 HandlerFunc 类型
,把一个函数转为一个接口!
小练习:
501 Not Implemented
函数 NotImplemented
和 NotImplementedHandler
/api/unkown
时返回页面提示 501 Not Implemented
由于 Angluar,React 等 web 前端框架的流行, web 服务器对模板的需求已经不是非常强烈。然而,它依然是格式化数据输出的一种重要手段。 熟悉 jinja2 模板的人对 go 的模板设计应非常亲切。
代码与文件结构:github.com/pmlpml/golang-learning/web/cloudgo-template
2.1 输出 html 页面
如果你仅是打算输出 html 页面,github.com/unrolled/render
是最为简单和直接。
srver.go
package service
import (
"net/http"
"os"
"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/unrolled/render"
)
// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {
formatter := render.New(render.Options{
Directory: "templates",
Extensions: []string{".html"},
IndentJSON: true,
})
n := negroni.Classic()
mx := mux.NewRouter()
initRoutes(mx, formatter)
n.UseHandler(mx)
return n
}
func initRoutes(mx *mux.Router, formatter *render.Render) {
webRoot := os.Getenv("WEBROOT")
if len(webRoot) == 0 {
if root, err := os.Getwd(); err != nil {
panic("Could not retrive working directory")
} else {
webRoot = root
//fmt.Println(root)
}
}
mx.HandleFunc("/", homeHandler(formatter)).Methods("GET")
mx.PathPrefix("/").Handler(http.FileServer(http.Dir(webRoot + "/assets/")))
}
要点:
(1)formatter 构建,指定了模板的目录,模板文件的扩展名
(2)homeHandler 使用了模板
我们在当前目录下,建立了 assets
和 templates
目录。 index.html 在 templates 目录中
<html>
<head>
<link rel="stylesheet" href="css/main.css"/>
head>
<body>
<img src="images/cng.png" height="48" width="48"/>
Sample Go Web Application!!
<div>
<p class="greeting-id">The ID is {{.ID}}p>
<p class="greeting-content">The content is {{.Content}}p>
div>
body>
html>
其中 {{.}}
表示数据填充位置。 {{.ID}}
表示该数据的 ID 属性。
home.go 处理了数据填充。
package service
import (
"net/http"
"github.com/unrolled/render"
)
func homeHandler(formatter *render.Render) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
formatter.HTML(w, http.StatusOK, "index", struct {
ID string `json:"id"`
Content string `json:"content"`
}{ID: "8675309", Content: "Hello from Go!"})
}
}
我们使用 formatter 的 HTML 直接将数据注入模板,并输出到浏览器。 更多内容见 render 在 git 上的 README。
运行程序:$ go run main.go
在浏览器访问:http://localhost:8080
注意观察控制台 negroni 输出。
2.2 使用 text/template 库
golang 提供了强大的 template 库,上述 Render 仅是它的简单包装。 官网的例子也非常简单:
type Inventory struct {
Material string
Count uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil { panic(err) }
这里给出了模板使用的过程:创建 - 编译 - 执行。 即 template.New("name").Parse("{{.Content}} string").Execute(writer,data)
问题1:New("name")
为什么需要 name ? 如果程序中多处创建同名模板但内容不一样,有问题吗?
阅读 text/template API 文档,解释输出
t := template.Must(template.New("letter").Parse("A{{.}}\n"))
t1 := template.Must(t.New("letter1").Parse("B{{.}}\n"))
t.Execute(os.Stdout, "1")
t1.Execute(os.Stdout, "2")
fmt.Println(len(t1.Templates()))
for _, tt := range t1.Templates() {
fmt.Println(tt.Name())
}
这段代码的输出是?…
如果 letter1
改为 letter
呢?
如果 t.New
改为 template.New
呢?
问题2:如果 ParseFiles(...)
文件模板都被缓存,如何才能实现模板热更新(运行时检测文件更新)?
注意:网上很多文章使用模板的方法都是不正确的,应该用 ParseFiles
或 ParseGlob
获得一个模板 set 的第一个元素。具体参考 formatter.HTML 的实现。
简明了解模板的语法:golang模板语法简明教程
代码与实例:Go语言核心之美 3.6-template模版
看完后,请完成:
3.1 request 定义
https://go-zh.org/pkg/net/http/#Request
要点:
3.2 如何处理表单
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/04.0.md
补充:
return &MyStruct{map["xx"]}
java web 有三大神器 Filter、Servlet、Listener。
对比 servlet: Java Servlet pk Golang Handler
对比 Filter: Java Filter pk negroni Handler
java 文档给出了常用的 Filter!
4.1 Filter 原理
go negroni “中间件”模板代码:
func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// do some stuff before
next(rw, r)
// do some stuff after
}
4.2 negroni组件
使用 negroni : https://github.com/urfave/negroni/blob/master/translations/README_zh_CN.md
negroni classic 的实现:
// Classic returns a new Negroni instance with the default middleware already
// in the stack.
//
// Recovery - Panic Recovery Middleware
// Logger - Request/Response Logging
// Static - Static File Serving
func Classic() *Negroni {
return New(NewRecovery(), NewLogger(), NewStatic(http.Dir("public")))
}
https://github.com/urfave/negroni#third-party-middleware
例如,让 web 支持 gzip 协议:
https://github.com/phyber/negroni-gzip
注意,如果要支持静态文件压缩,请注意顺序!