首页 / Wails 入门教程 / 动态资产与静态资源

Wails 入门教程

动态资产与静态资源

本教程共 42 篇 · 第 11 篇 · 更新于 2026-08-03

Wails动态资产AssetServerembedHandler

11. 动态资产与静态资源

本节目标

  • 分清”编进二进制的静态资产”和”运行时动态文件”
  • 会用自定义 fs.FS 包装 embed,实现先查嵌入再动态生成
  • 会用 AssetServer.Handler 拦截特定请求(如 API)
  • 知道动态资产的两个大坑:安全暴露、dev 模式失效

11-1 静态资产 vs 动态资产

前面所有例子里,前端资源都是 //go:embed all:frontend/dist 编进二进制的。这类叫静态资产:编译时定死、运行时不变,AssetServer 直接从 embed.FS 读出来喂给 WebView。

但有些场景静态资产不够用:

  • 用户上传的头像、附件,存在程序数据目录,运行时才有。
  • 后端按请求临时生成的报表、图片缩略图。
  • 想在前端用 fetch('/api/xxx') 调一个由 Go 现场算出来的接口。

这些”运行时才确定内容”的文件或响应,就是动态资产。Wails 给了一条正路来处理它们,核心在 AssetServer 的两块:Assets(一个 fs.FS)和 Handler(一个 http.Handler)。

11-2 用自定义 fs.FS 包一层 embed

最干净的做法:不把 embed.FS 直接交给 AssetServer,而是写一个自己的 fs.FS,在 Open 里先去 embed 找,找不到再动态给。AssetServer 只认 fs.FS 接口,不关心背后是 embed 还是你动态生成的。

type DynamicFS struct {
	embedFS embed.FS
	userDir string
}

func (d *DynamicFS) Open(name string) (fs.File, error) {
	// 去掉前导斜杠,fs.FS 里路径不带 /
	name = strings.TrimPrefix(name, "/")

	// 先尝试嵌入的静态资源
	if f, err := d.embedFS.Open(name); err == nil {
		return f, nil
	}

	// 嵌入里没有,去用户数据目录动态找(如用户上传的文件)
	path := filepath.Join(d.userDir, name)
	if data, err := os.ReadFile(path); err == nil {
		return &memFile{name: name, data: data}, nil
	}

	return nil, os.ErrNotExist
}

memFile 是你实现 fs.File 接口的小包装,把字节切片包装成文件。这样前端请求 /user/avatar.png 时,先查嵌入(没有),再查用户目录(有就返回),逻辑对前端完全透明。

接进 main.go

AssetServer: &assetserver.Options{
	Assets: &DynamicFS{
		embedFS: assets,
		userDir: userDataDir,
	},
},
Tip

这套”包装 fs.FS”的思路最稳,因为 AssetServer 的所有请求都走 Open,你在一个地方就接管了”找不到就动态生成”的全部逻辑,不用管回退顺序。

11-3 用 AssetServer.Handler 拦截请求

如果你要的不是”动态文件”,而是”按路径返回一段计算结果的响应”(比如一个 JSON 接口),用 Handler 更直白。它是个标准 http.Handler,请求进来先过它:

AssetServer: &assetserver.Options{
	Assets: assets,
	Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.URL.Path, "/api/") {
			w.Header().Set("Content-Type", "application/json")
			w.Write([]byte(`{"status":"ok"}`))
			return
		}
		// 非 /api 路径:交给默认资产服务处理
	}),
},

这里有个关键细节:一旦设了 Handler,请求就先到它。对于你不想拦截的路径(比如 index.html、JS、CSS),必须自己把请求导回默认资产服务,否则界面会加载不出来。上面示例里留空的那行,就是该调用 Wails 默认资产服务(或直接用 assetsOpen)的地方。具体回退写法官方文档有完整示例,思路就是”我处理的我处理,剩下的交给 embed.FS”。

Warning

设了 Handler 很容易把前端资源也截胡。常见翻车是:只写了 /api 分支、忘了把其他请求导回 assets,结果整个界面白屏——因为 index.html 也被你拦下却没处理。写 Handler 时务必保留”非目标路径回退到默认资产”的分支。

11-4 安全:别把文件系统敞开门

动态资产最容易踩的坑不是技术,是安全。当你允许前端用路径来要文件时,如果直接把请求里的路径拼到磁盘读取,攻击者能构造 ../../etc/passwd 这类路径穿越,读到你机器上不该读的文件。

// 危险写法:直接用请求路径拼磁盘路径
os.ReadFile(r.URL.Path) // 千万别这么干

防护要点:

  • filepath.Clean 规范化路径,并校验最终路径确实落在你允许的根目录内。
  • 白名单优先:只放行已知的几个目录或文件类型。
  • 不要接受用户输入的任意绝对路径。
Note

你能动态服务文件,不等于前端能随便读你电脑任意文件。这条边界必须在 Go 侧用白名单和路径校验守住,前端不可信。

11-5 dev 模式的一个现实坑

vite 升到 5 之后,开发模式下前端请求基本都走 Vite 自己的 dev server,Wails 的 Handler 和自定义 fs.FS 对前端请求的拦截,在 wails dev 里可能根本不生效——因为请求压根没经过 AssetServer,而是被 Vite 接走了。

这意味着:动态资产的拦截逻辑,开发时可能表现和预期不符,但 wails build 之后(资源来自 embed、请求走 AssetServer)又正常。所以这类功能一定要 wails build 实测,别被 dev 模式下的”没反应”骗了。

Tip

调试动态资产时,临时把前端请求也走 AssetServer(或直接在 build 产物里测),比在 dev 模式里折腾 Vite 代理高效得多。记住:动态资产的正确性以生产构建为准。

11-6 一个最小 memFile 骨架

11-2 里 DynamicFS.Open 返回的 memFile 需要实现 fs.File 接口。最小可用版只要包一个 bytes.Reader,并实现 Stat() 返回文件信息、Read() 委托给它、Close() 返回 nil。逻辑很简单:动态内容先读进 []byte,用 bytes.NewReader 包起来,其他接口方法按需补空实现即可。

官方文档的动态资产示例里给了完整可抄的 memFile,照着补就行。记住它只是”把字节当文件返回”的适配层,真正的安全校验和路径白名单要在 DynamicFS.Open 里做,别下沉到 memFile。这样分层之后,资产来源(嵌入还是动态)和文件表示(memFile)各管各的,后面要加新的动态来源也只在 Open 里加分支。

常见误区

把动态资产当成”运行时改嵌入文件”。embed 是编译期固化的,运行时改不了。DynamicFS 是在”查找”这一层做了分支,不是改了二进制里的东西。

Handler 里忘了回退 assets 导致白屏。见 11-3 的警告,拦截器必须放行非目标请求。

dev 模式测动态资产”没生效”就以为代码错了。很可能是 Vite 5+ 把请求截走了。build 后再测一次。

动态读文件不做路径校验。路径穿越是真实风险,Go 侧白名单 + filepath.Clean 是底线。

小结

静态资产是 embed.FS 编进二进制的死文件;动态资产是运行时才确定的文件或响应。做法一:写个自定义 fs.FS 包住 embed,在 Open 里”先查嵌入、找不到再动态生成”,最稳。做法二:用 AssetServer.Handler 拦截特定请求(如 /api),但务必把其他路径导回默认资产,否则白屏。安全上严格校验路径、白名单放行,防止穿越读盘。dev 模式下 Vite 5+ 可能让拦截不生效,动态资产以 wails build 实测为准。到此,认识 Wails、环境、结构、配置、前端集成这前半程就讲完了,下一章起进入 Go 与前端通信的核心。