模板(HTML Template)&静态文件
感谢Geek 兔的教程
示例代码,解释摘抄 https://geektutu.com/ + 结合自己的理解
- 实现静态资源服务(Static Resource)
- 支持HTML模板渲染。
服务端渲染
现在越来越流行前后端分离的开发模式,即 Web 后端提供 RESTful 接口,返回结构化的数据(通常为 JSON 或者 XML)。
前端使用 AJAX 技术请求到所需的数据,利用 JavaScript 进行渲染。Vue/React 等前端框架持续火热,这种开发模式前后端解耦,优势非常突出。
后端童鞋专心解决资源利用,并发,数据库等问题,只需要考虑数据如何生成;
前端童鞋专注于界面设计实现,只需要考虑拿到数据后如何渲染即可。
使用 JSP 写过网站的童鞋,应该能感受到前后端耦合的痛苦。
JSP 的表现力肯定是远不如 Vue/React
等专业做前端渲染的框架的。
而且前后端分离在当前还有另外一个不可忽视的优势。
因为后端只关注于数据,接口返回值是结构化的,与前端解耦。
同一套后端服务能够同时支撑小程序、移动APP、PC端 Web 页面,以及对外提供的接口。
随着前端工程化的不断地发展,Webpack,gulp 等工具层出不穷,前端技术越来越自成体系了。
但前后分离的一大问题在于,页面是在客户端渲染的,比如浏览器,这对于爬虫并不友好。
Google 爬虫已经能够爬取渲染后的网页,但是短期内爬取服务端直接渲染的 HTML 页面仍是主流。
今天的内容便是介绍 Web 框架如何支持服务端渲染的场景。
静态文件(Serve Static Files)
网页的三剑客,JavaScript、CSS 和 HTML。要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。
还记得我们之前设计动态路由的时候,支持通配符*
匹配多级子路径。
比如路由规则/assets/*filepath
,可以匹配/assets/
开头的所有的地址。
例如/assets/js/geektutu.js
,匹配后,参数filepath
就赋值为js/geektutu.js
。
那如果我么将所有的静态文件放在/usr/web
目录下,那么filepath
的值即是该目录下文件的相对地址。
映射到真实的文件后,将文件返回,静态服务器就实现了。
找到文件后,如何返回这一步,net/http
库已经实现了。
因此,gee
框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给http.FileServer
处理就好了。
Static
方法
Static
是暴露给框架使用者使用的!
// Static 方法用于注册静态文件服务路由。
// relativePath 表示 URL 上的路径前缀,root 表示本地文件系统上的目录。
// 该方法会创建一个静态文件处理器,将 HTTP 请求与静态文件映射起来,并注册到路由组中。
func (group *RouterGroup) Static(relativePath string, root string) {
// 创建静态文件处理器,用于提供静态文件服务 root 可以是绝对路径或者是相对路径! "/usr/geektutu/blog/static" || "./static"
handler := group.createStaticHandler(relativePath, http.Dir(root))
// 构建 URL 模式,以匹配包含文件路径的 URL 请求
urlPattern := path.Join(relativePath, "/*filepath")
// 将 HTTP GET 请求处理器与 URL 模式关联,以处理静态文件请求
group.GET(urlPattern, handler)
}
用户可以将磁盘上的某个文件夹root
映射到路由relativePath
。例如:
r := gee.New()
r.Static("/assets", "/usr/geektutu/blog/static")
// 或相对路径 r.Static("/assets", "./static")
r.Run(":9999")
用户访问localhost:9999/assets/js/geektutu.js
,最终返回/usr/geektutu/blog/static/js/geektutu.js
。
你可能发现static
这个路径段丢失了!是通过 http.StripPrefix(absolutePath, http.FileServer(fs))
这个方法完成的!
createStaticHandler 方法
// createStaticHandler 创建一个静态文件处理程序,用于提供静态文件服务。
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
// 构建静态文件的绝对路径,包括路由组的前缀和相对路径
absolutePath := path.Join(group.prefix, relativePath)
// 创建一个文件服务器,使用 http.StripPrefix 去除 URL 中的前缀以匹配相对路径
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
// 返回一个处理函数,该函数接受一个 Context 对象作为参数
return func(c *Context) {
// 从请求中获取文件路径参数
file := c.Param("filepath")
// 检查文件是否存在,如果不存在则返回 404 Not Found 状态码
if _, err := fs.Open(file); err != nil {
c.Status(http.StatusNotFound)
return
}
// 使用文件服务器来提供静态文件
fileServer.ServeHTTP(c.Writer, c.Req)
}
}
关于 http.StripPrefix
func StripPrefix(prefix string, h http.Handler) http.Handler
是 Go 语言标准库 net/http
包中的一个函数
- 返回值是
http.Handler
用于创建一个处理器(Handler),它用于从 URL 路径中删除指定的前缀,然后将剩余的路径传递给另一个处理器。这通常用于处理静态文件服务器或路由中的路径重写。
prefix
是一个字符串,表示要从URL路径中删除的前缀部分。h
是一个实现了http.Handler
接口的处理器,它将接收剩余的路径进行处理。
在Web应用中,有时候我们想要提供静态文件
比如图片、CSS样式表或JavaScript文件。
这些文件通常存储在服务器的特定目录中,例如 /static
。
现在,如果我们想要让用户通过URL来获取这些文件,我们可以使用 http.FileServer
来提供文件,但是我们需要指定一个URL路径来访问这些文件。
例如,我们可能想要通过访问 http://example.com/static/image.jpg
来获取图片。
但是,问题是我们并不希望在URL中包含 /static
前缀,我们希望用户只需要访问 http://example.com/image.jpg
。
这就是 http.StripPrefix
的作用。
按照上面的demo 传入给 createStaticHandler 参数为
-
relativePath = "/assets"
相对路径 -
http.Dir(root)
;root = "/usr/geektutu/blog/static")
func Dir(root string) FileSystem
Go 语言标准库
net/http
包中的一个函数,用于创建一个表示本地文件系统目录的类型。它返回一个实现了
http.FileSystem
接口的值,允许您通过 HTTP 服务器提供该目录中的文件
第一步先进行了 absolutePath := path.Join(group.prefix, relativePath)
的路径拼接
demo 示例中没用group prefix
,那么 absolutePath = "/assets"
http.StripPrefix(absolutePath, http.FileServer(fs))
⬇️ 等于⬇️
http.StripPrefix("/assets", http.FileServer(http.Dir("/usr/geektutu/blog/static")))
简单来说就是 当你访问 /assets/xx.png
这个路径的时候,其实是在访问 /static/xxx.png
, http.StripPrefix
理解为删除不太合适
真实场景理解:
例如
"/usr/geektutu/blog/static"
是一个真实存在于服务器的路径资源文件夹!但是不想让客户端通过请求URL猜到真实的服务器端请求URL!
所以可以通过一个“别名”的方式!给这个路径的资源起个别名叫做
"/assets"
,并且以这个别名生成了一个http handler
这个handler
本质上就是通过"/assets"
这个 “别名” 来访问 真实路径"/usr/geektutu/blog/static"
的静态资源!后面也通过真实的路径来判断请求的文件是不是存在,不存在则返回404
存在的话!使用文件服务器来提供静态文件,传入封装在
context
中的Writer&Req
这样一个文件服务就完成了!
下一步根据这个别名注册一个http的Get请求路由!
fileServer.ServeHTTP(c.Writer, c.Req)
构建 URL 模式,以匹配包含文件路径的 URL 请求,上面demo的最终拼接结果就是 "/assets/*filepath"
urlPattern := path.Join(relativePath, "/*filepath")
然后就可以注册到路由树了!
group.GET(urlPattern, handler)
测试使用
package main
import (
"fmt"
"gee"
"net/http"
)
func main() {
r := gee.NewV2()
tourister := r.Group("/tourister")
// 静态服务
tourister.Static("/assets", "./static")
tourister.Use(gee.Logger(), gee.PrintURL())
touristerV1 := tourister.Group("/v1")
touristerV1.GET("/post/:id", func(ctx *gee.Context) {
ctx.String(http.StatusOK, fmt.Sprintf("游客阅读Post%s", ctx.Param("id")))
})
admin := r.Group("/admin")
admin.Use(gee.AdminOnly())
adminV1 := admin.Group("/v1")
adminV1.GET("/post/:id", func(ctx *gee.Context) {
ctx.String(http.StatusOK, fmt.Sprintf("Admin阅读Post%s", ctx.Param("id")))
})
r.Run(":9999")
}
通过浏览器访问
net/http 库已经实现了静态资源服务器,也就是fileServer.ServeHTTP。
我们做的只是接收到请求,把请求的路径地址映射到静态资源所在的真实地址,剩下的就交给静态资源服务器去做就好了。
HTML 模版渲染
Go语言内置了text/template
和html/template
2个模板标准库,其中html/template为 HTML 提供了较为完整的支持。
包括普通变量渲染、列表渲染、对象渲染等。
这里我们实现的gee 框架的模板渲染直接使用
html/template
提供的能力。
type EngineV2 struct {
*RouterGroup
router *TrieRouter
groups []*RouterGroup // store all groups
htmlTemplates *template.Template //
funcMap template.FuncMap // 这是一个 template.FuncMap 类型的字段,它是一个映射(map),用于存储模板函数。
}
首先为 Engine 示例添加了 *template.Template
和 template.FuncMap
对象,
前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。
另外,给用户分别提供了设置自定义渲染函数funcMap
和加载模板的方法。
func (engine *EngineV2) SetFuncMap(funcMap template.FuncMap) {
engine.funcMap = funcMap
}
func (engine *EngineV2) LoadHTMLGlob(pattern string) {
// template.Must 用于检查是否在模板解析过程中出现了错误,如果有错误,它将引发 panic。这样可以确保模板的正确性,如果出现错误,程序将终止。
// template.New("") 创建一个新的空白模板,"" 是模板的名称,可以根据需要指定。
//.Funcs(engine.funcMap) 注册一个模板函数映射(funcMap)engine.funcMap 是一个包含自定义模板函数的映射,这允许在模板中调用这些自定义函数。
// ParseGlob(pattern) 解析模板文件,pattern 是一个用于匹配模板文件的通配符模式。它会查找满足模式的文件,并将它们解析为模板。
engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))
}
接下来,对原来的 (*Context).HTML()
方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。
type Context struct {
//...
+ engine *EngineV2
}
func (c *Context) HTML(code int, name string, data interface{}) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
c.String(500, err.Error())
}
}
ExecuteTemplate
是模板引擎的方法,用于执行指定的HTML模板,并将渲染的结果写入到指定的c.Writer
(通常是HTTP响应的写入器)。name
是模板的名称,用于标识要执行的特定HTML模板data
则是要传递给模板的数据。模板引擎会根据模板名称和数据执行模板,并将结果写入c.Writer
中,从而将HTML内容发送给客户端浏览器。- 如果在执行模板时发生错误,例如模板不存在或数据不匹配模板的预期,
ExecuteTemplate
方法会返回一个非空的错误,您可以通过检查err
变量来处理错误情况。如果一切顺利,模板引擎将渲染HTML内容并将其写入HTTP响应中,供客户端浏览器显示。
我们在 Context
中添加了成员变量 engine *Engine
,这样就能够通过 Context 访问 Engine 中的 HTML 模板。
实例化 Context
时,还需要给 c.engine
赋值。
func (engine *EngineV2) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
c.engine = engine // 给 `c.engine` 赋值。
engine.router.handle(c)
}
测试
├── static
│ ├── R-C.jpg
│ └── css
│ └── geektutu.css
├── templates
│ ├── arr.tmpl
│ └── css.tmpl
└── test
嵌入Css的
geektutu.css
*{
color:red
}
css.tmpl
<html>
<link rel="stylesheet" href="/tourister/assets/css/geektutu.css">
<p>geektutu.css is loaded</p>
</html>
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "css.tmpl", nil)
})
注入数据的
r.GET("/students", func(c *gee.Context) {
c.HTML(http.StatusOK, "arr.tmpl", gee.H{
"title": "gee",
"stuArr": []string{"code", "cunwang"},
})
})
arr.tmpl
<html>
<link rel="stylesheet" href="/tourister/assets/css/geektutu.css">
<p>geektutu.css is loaded</p>
{{.title}}
{{.stuArr}}
</html>
总结一下
静态文件部份
主要是通过
group.createStaticHandler(relativePath, http.Dir(root))
relativePath
指定请求path
root
指定真实文件目录内部通过
http.StripPrefix
将真实文件目录代理到relativePath
并且返回一个http.Handler
这里有一个思考了很久的小细节
createStaticHandler
这个函数返回了一个HandlerFunc
且内部调用了http.StripPrefix
返回handler
的SeverHTTP
方法,一开始没有绕过来,这里为什么还要手动调用一次?我们之前不都是统一在自定义的
engine
调用了吗?其实那里调用的是我们自己通过注册传入到路由表中的
handler
!但是这里的 我们使用的是
net/http
库已经实现了静态资源服务器——http.StripPrefix
返回的http.handler
我们外面包装一层,先通过我们自己的
Engine
调用到createStaticHandler
生成一个独立的文件相关的静态资源服务,文件内入的响应处理,还是需要调用独立的文件相关的静态资源服务的SeverHTTP
方法,对当前http请求进行相应
HTML 模板部份
首先
LoadHTMLGlob
方法内部根据 目录 pattern 注册了一个目录中的全部模版文件
(c *Context) HTML
方法根据
htmlTemplates.ExecuteTemplate(c.Writer, name, data)
向Writer
中写入根据name
找到的模板内部以及注入data
数据
评论区