目 录CONTENT

文章目录
Go

06.跟着Geek兔兔学习- 模板(HTML Template)&静态文件

Hello!你好!我是村望~!
2023-09-06 / 0 评论 / 0 点赞 / 121 阅读 / 3,787 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

模板(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")
}

image-20230906183912438

通过浏览器访问

image-20230906184046932

net/http 库已经实现了静态资源服务器,也就是fileServer.ServeHTTP。

我们做的只是接收到请求,把请求的路径地址映射到静态资源所在的真实地址,剩下的就交给静态资源服务器去做就好了。

HTML 模版渲染

Go语言内置了text/templatehtml/template2个模板标准库,其中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.Templatetemplate.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)
})

image-20230906211454021

注入数据的

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>

image-20230906211621923

总结一下

静态文件部份

主要是通过 group.createStaticHandler(relativePath, http.Dir(root))

  • relativePath 指定请求path
  • root 指定真实文件目录

内部通过 http.StripPrefix 将真实文件目录代理到 relativePath 并且返回一个 http.Handler

这里有一个思考了很久的小细节

createStaticHandler 这个函数返回了一个 HandlerFunc 且内部调用了 http.StripPrefix 返回 handlerSeverHTTP 方法,一开始没有绕过来,这里为什么还要手动调用一次?

我们之前不都是统一在自定义的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数据

0

评论区