AI书签管理工具开发全记录(十九):嵌入资源处理

发布于:2025-06-10 ⋅ 阅读:(44) ⋅ 点赞:(0)

1.前言 📝

在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。

2.embed介绍 🎯

Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的模式。下面简单介绍一下embed包的用法,后面出文章详细介绍。

2.1. 嵌入单个文件

package main

import (
    _ "embed"
    "fmt"
)

//go:embed version.txt
var version string  // 自动推断为字符串类型

func main() {
    fmt.Println("App Version:", version)
}

2.2. 嵌入二进制文件

//go:embed logo.png
var logoBytes []byte  // 适用于二进制文件

func saveLogo() error {
    return os.WriteFile("logo_copy.png", logoBytes, 0644)
}

2.3. 嵌入文件集合

//go:embed templates/*.html
var templateFS embed.FS  // 文件系统接口

func loadTemplates() {
    // 遍历嵌入的HTML模板
    dirEntries, _ := templateFS.ReadDir("templates")
    for _, entry := range dirEntries {
        if !entry.IsDir() {
            data, _ := templateFS.ReadFile("templates/" + entry.Name())
            fmt.Printf("Loaded template: %s (%d bytes)\n", 
                entry.Name(), len(data))
        }
    }
}

3.embed改造 🛠️

3.1创建resources文件夹

在根目录下创建resources文件夹,然后创建static文件夹,用于存放前端文件。我们将resources整体作为文件系统嵌入,后续有其它需要嵌入的资源可以统一在resources处理。

resources和staic按自己喜好起,能区分即可。

需要运行npm build将dist文件夹内文件复制到static目录中

3.2 创建assets.go

创建assets.go,用于全局存储嵌入的资源文件

// assets/assets.go

package assets

import "embed"

var Resources embed.FS

3.2 嵌入resources资源文件

修改main.go

// main.go

package main

import (
	"embed"

	"github.com/ciclebyte/aibookmark/assets"
	"github.com/ciclebyte/aibookmark/cmd"
)

//go:embed resources
var resources embed.FS

func main() {
	assets.Resources = resources
	cmd.Execute()
}
  • 需要引入embed
  • 将resources交由assets管理,否则后面使用容易出现循环引用。

3.2 修改gin前端资源处理方式

func NewServer(db *gorm.DB) *Server {
	server := &Server{db: db}
	router := gin.New()

	// 配置中间件
	router.Use(gin.Logger())
	router.Use(gin.Recovery())

	// 禁用重定向
	router.RedirectTrailingSlash = false
	router.RedirectFixedPath = false
	router.HandleMethodNotAllowed = true

	// 打印嵌入资源文件结构
	fmt.Println("=== 嵌入资源文件结构 ===")
	fs.WalkDir(assets.Resources, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			fmt.Printf("访问路径 %s 时出错: %v\n", path, err)
			return err
		}
		if d.IsDir() {
			fmt.Printf("目录: %s\n", path)
		} else {
			fmt.Printf("文件: %s\n", path)
		}
		return nil
	})
	fmt.Println("=====================")

	// 配置CORS
	router.Use(func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	})

	// 添加请求日志中间件
	router.Use(func(c *gin.Context) {
		// 开始时间
		start := time.Now()
		path := c.Request.URL.Path
		method := c.Request.Method

		// 处理请求
		c.Next()

		// 结束时间
		end := time.Now()
		latency := end.Sub(start)

		// 获取状态码
		statusCode := c.Writer.Status()

		// 打印请求日志
		fmt.Printf("[%s] %s %s %d %v\n", method, path, c.ClientIP(), statusCode, latency)
	})

	staticFS, _ := fs.Sub(assets.Resources, "resources/static")
	fmt.Println("=== 静态资源文件结构 ===")
	fs.WalkDir(staticFS, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			fmt.Printf("访问路径 %s 时出错: %v\n", path, err)
			return err
		}
		if d.IsDir() {
			fmt.Printf("目录: %s\n", path)
		} else {
			fmt.Printf("文件: %s\n", path)
		}
		return nil
	})
	fmt.Println("=====================")

	// 设置静态文件服务
	router.StaticFS("/assets", http.FS(staticFS))
	// 添加一个处理 favicon 的路由
	router.GET("/favicon.ico", func(c *gin.Context) {
		c.FileFromFS("/favicon.ico", http.FS(staticFS))
	})

	// 读取 index.html 内容
	indexContent, err := fs.ReadFile(staticFS, "index.html")
	if err != nil {
		fmt.Printf("读取 index.html 失败: %v\n", err)
	} else {
		fmt.Printf("成功读取 index.html,长度: %d\n", len(indexContent))
	}

	// 添加根路径处理
	router.GET("/", func(c *gin.Context) {
		fmt.Printf("处理根路径请求: %s\n", c.Request.URL.Path)
		fmt.Printf("请求头: %v\n", c.Request.Header)

		// 设置响应头
		c.Header("Content-Type", "text/html; charset=utf-8")
		c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
		c.Header("Pragma", "no-cache")
		c.Header("Expires", "0")

		// 直接写入响应
		c.Writer.WriteHeader(200)
		c.Writer.Write(indexContent)
	})

	// API路由分组
	api := router.Group("/api")
	{
		category := api.Group("/categories")
		{
			category.POST("", server.createCategory)
			category.GET("", server.listCategories)
			category.GET("/all", server.getAllCategories)
			category.GET("/:id", server.getCategory)
			category.PUT("/:id", server.updateCategory)
			category.DELETE("/:id", server.deleteCategory)
		}

		bookmark := api.Group("/bookmarks")
		{
			bookmark.POST("", server.createBookmark)
			bookmark.GET("", server.listBookmarks)
			bookmark.GET("/:id", server.getBookmark)
			bookmark.PUT("/:id", server.updateBookmark)
			bookmark.DELETE("/:id", server.deleteBookmark)
			bookmark.POST("/ai", server.createAIBookmark)
			bookmark.POST("/import", server.importBookmarks)
			bookmark.POST("/export", server.exportBookmarks)
		}
	}

	// 添加Swagger路由
	router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// SPA 前端路由兜底
	router.NoRoute(func(c *gin.Context) {
		// 只对 GET 请求兜底
		if c.Request.Method == "GET" {
			fmt.Printf("处理前端路由请求: %s\n", c.Request.URL.Path)

			// 检查是否是静态资源请求
			if strings.HasPrefix(c.Request.URL.Path, "/assets/") {
				// 获取文件扩展名
				ext := filepath.Ext(c.Request.URL.Path)
				contentType := "application/octet-stream"

				// 根据扩展名设置 Content-Type
				switch ext {
				case ".js":
					contentType = "application/javascript"
				case ".css":
					contentType = "text/css"
				case ".html":
					contentType = "text/html; charset=utf-8"
				case ".json":
					contentType = "application/json"
				case ".png":
					contentType = "image/png"
				case ".jpg", ".jpeg":
					contentType = "image/jpeg"
				case ".svg":
					contentType = "image/svg+xml"
				case ".ico":
					contentType = "image/x-icon"
				}

				c.Header("Content-Type", contentType)
				c.FileFromFS(c.Request.URL.Path[1:], http.FS(staticFS))
				return
			}

			// 其他请求返回 index.html
			c.Header("Content-Type", "text/html; charset=utf-8")
			c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
			c.Header("Pragma", "no-cache")
			c.Header("Expires", "0")
			c.Writer.WriteHeader(200)
			c.Writer.Write(indexContent)
		} else {
			fmt.Printf("404 Not Found: %s %s\n", c.Request.Method, c.Request.URL.Path)
			c.Status(404)
		}
	})

	server.router = router
	return server
}

4.启动

我们还是先启动后端服务

go run main.go serve

但是这次我们无需再启动前端服务,可以直接通过后端服务进行访问
image.png


往期系列


网站公告

今日签到

点亮在社区的每一天
去签到