(5) 静态前端网站容器化 - 容器篇 - 自定义 HTTP Server

建议点击 查看原文 查看最新内容。

原文链接: https://typonotes.com/posts/2025/02/09/static-sap-dockerize-customize-httpserve/

众所周知, 我们在容器化 静态网站 的时候为了实现 一次编译, 处处运行 的目标, 在 index.html 中插入了一个变量 __CONFIG__, 在启动的时候进行替换为正式后端的地址。 可以参考

  1. (2) Vue3 / React 静态网站项目容器化 - 实战案例
  2. (3) 静态前端网站容器化 - 容器篇

然后在实践中经常会遇到各种条件的约束。 平台团队以安全为由,锁定了 K8s pod 策略, 将目录限制为只读状态。 虽然可以使用 mount 目录方式绕过, 但是我懒啊, 不想因为不同平台再定制不同的管理策略。

对于这种情况, 我使用 gin-gonic/gin 自定义一个 HTTP Server, 使用 https://github.com/spf13/afero 创建了一个内存文件系统, 将所有静态资源全部复制 内存空间 中, 这样就随便我修改了。

除此之外还有一个好处: 自定服务器还有一个好处时可以通过 环境变量 更方便的管理各种功能, 比花实践折腾配置模版(nginx) 体验更好。

Demo 项目可以参考: https://github.com/tangx/httpstatic

重点来了

在 gin 中提供了 StaticFS, 但参数要求是 http.FileSystem 接口

1
func (group *gin.RouterGroup) StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes

于是使用了 http.FS()fs.FS 接口进行封装。

1
2
3
4
5
// 定义
// func http.FS(fsys fs.FS) http.FileSystem

// 使用
r.StaticFS("/", http.FS(fsys))

但是, 使用 spf13/afero 创建的内存系统的默认返回接口是 afreo.Fs

1
2
3
// func afero.NewMemMapFs() afero.Fs

m := afero.NewMemMapFs()

解决方法

解决方法很简单, 自定一个 结构体 mfswrapper 实现 fs.FS 接口即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// mfswrapper wraps afero.Fs to implement fs.FS.
// cause gin.StaticFS() requires fs.FS.
// but afero.Fs does not implement fs.FS.
type mfswrapper struct {
	afero.Fs
}

func (w mfswrapper) Open(name string) (fs.File, error) {
	return w.Fs.Open(name)
}

部分代码如下

  1. 创建内存文件系统 m
  2. copywalk() 将本地文件复制到内存文件系统 m 中。
  3. 读取 index.html 并使用 inject() 函数替换 __CONFIG__ 为真实变量。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// mfsMode serves files from the memory directory.
// if k8s restrict modifying files in the container.
func mfsMode(dist string) fs.FS {
	m := afero.NewMemMapFs()

	err := copywalk(dist, m, dist)
	if err != nil {
		panic(err)
	}

	// replace index.html content.
	index := filepath.Join(dist, "index.html")
	if data, replaced := inject(index); replaced {
		if err := afero.WriteFile(m, "index.html", data, 0644); err != nil {
			panic(err)
		}
	}

	mw := &mfswrapper{m}

	return mw
}

为什么要这样

这样就绕过了 只读文件系统 的要求。 更重要的是, 无论以后应用部署到其他任何容器平台, 都不用再为其管理专门的 部署配置