文件关联 File Association
本教程共 42 篇 · 第 28 篇 · 更新于 2026-08-03
28. 文件关联 File Association
本节目标
- 说清文件关联是什么、什么样的应用需要它
- 会在
wails.json里配置fileAssociations并放对图标 - 掌握 Windows / macOS / Linux 三条不同的接收路径
- 会把启动时拿到的文件路径安全地传给 React 前端
- 避开「emit 太早前端收不到」这个高频坑
28-1 文件关联解决什么问题
「文件关联」(File Association)就是把某个扩展名和某个应用绑在一起。双击 .docx 打开 Word,双击 .psd 打开 Photoshop,靠的都是它。
自己做的应用要是没配这个,用户只能先打开应用、再点「打开文件」、再在对话框里翻目录。多三步,体验就差一截。
哪些应用需要?编辑器、查看器、任何有自己文件格式的工具。你定义了一个 .myproj 格式,就应该让双击它直接进你的应用。
Wails 把配置入口放在项目根目录的 wails.json 里。这个能力是 v2.7.0 才加进来的,只覆盖 macOS 和 Windows;Linux 一直要自己动手,28-5 会讲怎么弄。用更老的版本翻不到 fileAssociations 字段,先升级再说。
28-2 在 wails.json 里声明
打开 wails.json,在 info 段里加一个 fileAssociations 数组:
{
"info": {
"fileAssociations": [
{
"ext": "wails",
"name": "Wails",
"description": "Wails Application File",
"iconName": "wailsFileIcon",
"role": "Editor"
},
{
"ext": "jpg",
"name": "JPEG",
"description": "Image File",
"iconName": "jpegFileIcon",
"role": "Editor"
}
]
}
}
每个字段的含义:
| 字段 | 说明 |
|---|---|
ext | 扩展名,不带前面的点。写 png,不是 .png |
name | 类型名称,例如 PNG File |
iconName | 图标文件名,不带扩展名。图标要放在 build 目录下,Wails 会从 .png 生成 macOS 和 Windows 各自需要的格式 |
description | 仅 Windows 生效。显示在资源管理器的「类型」列 |
role | 仅 macOS 生效。应用相对该类型的角色,对应 CFBundleTypeRole |
按上面的例子,你需要在 build 目录下准备 wailsFileIcon.png 和 jpegFileIcon.png。打包时 Wails 会自动转成 .icns(macOS)和 .ico(Windows)。
Tip图标源文件建议用 1024×1024 的 PNG,带透明通道。尺寸太小的话,生成出来的大图标会糊。
Warning别去抢系统常见格式。把
.txt、.jpg关联到自己的小工具上,用户装完发现所有图片都变成你的图标,这体验很糟。除非你确实在做图片查看器,否则用自定义扩展名。
28-3 Windows:只有 NSIS 安装包才算数
Windows 上的文件关联本质是往注册表里写键值。这件事只能在安装过程中做,所以 Wails 只在 NSIS 安装包里支持文件关联。
也就是说,你必须这样构建:
wails build -nsis
产物是 build/bin 下的安装程序。用户运行安装程序,安装器根据 wails.json 里的声明写注册表。直接把编译出来的 .exe 拷给用户,双击文件是不会唤起它的。
关联生效后,用户双击 .wails 文件,系统会启动一个新的应用进程,并把文件路径作为命令行参数传进来。你要自己解析 os.Args:
package main
import "os"
func main() {
// os.Args[0] 是程序自身路径,从 1 开始才是真正的参数
argsWithoutProg := os.Args[1:]
if len(argsWithoutProg) != 0 {
println("launchArgs", argsWithoutProg)
}
// ... wails.Run(...)
}
注意「启动一个新进程」这个行为。用户双击三个文件,就起三个应用窗口。这通常不是你想要的,解决办法是打开单实例锁,让后来的进程把参数交给已经在跑的那个。这部分第 30 章细讲,这里先看它长什么样:
err := wails.Run(&options.App{
Title: "wails-open-file",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{Assets: assets},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
SingleInstanceLock: &options.SingleInstanceLock{
UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc",
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
},
Bind: []interface{}{
app,
},
})
28-4 macOS:走 OnFileOpen 回调
macOS 的机制不一样。应用被打包成 .app bundle,文件类型声明写在 Info.plist 里,系统通过 Apple Event 通知应用,而不是传命令行参数。
Wails 把这层封装成了 Mac 选项里的 OnFileOpen 回调:
package main
import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/mac"
)
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "wails-open-file",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{Assets: assets},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Mac: &mac.Options{
OnFileOpen: func(filePaths []string) {
app.handleOpenFiles(filePaths)
},
},
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}
参数是 []string,一次可能来好几个路径——用户选中五个文件一起「打开方式」的时候就是这样。
这个回调不只在启动时触发。应用已经在跑,用户又双击了一个关联文件,系统同样会调它。所以 handleOpenFiles 要写成幂等的、可以被反复调用的。
28-5 Linux:手工打包
Wails 目前不负责 Linux 的 bundle 生成,文件关联得自己动手。以 .deb 包为例,需要准备这么几样东西。
第一样,.desktop 文件。 告诉桌面环境这个应用能处理哪些 MIME 类型:
[Desktop Entry]
Categories=Office
Exec=/usr/bin/wails-open-file %u
Icon=wails-open-file.png
Name=wails-open-file
Terminal=false
Type=Application
MimeType=application/x-wails;application/x-test
Exec 后面的 %u 不能少,它是给系统传参用的占位符。漏了这个,双击文件只会启动应用,路径传不进来。
第二样,MIME 类型定义。 一个 XML 文件,把扩展名和 MIME 类型对上:
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-wails">
<comment>Wails Application File</comment>
<glob pattern="*.wails"/>
</mime-type>
</mime-info>
第三样,图标。 推荐用 SVG,各种尺寸下都清晰。
第四样,安装后脚本。 装完要刷新系统的数据库,否则关联不会立刻生效:
# 重新加载 mime 类型,注册文件关联
update-mime-database /usr/share/mime
# 刷新桌面数据库,让应用出现在「打开方式」列表里
update-desktop-database /usr/share/applications
# 更新图标缓存
update-icon-caches /usr/share/icons/*
打包工具可以用 nfpm,配置文件里把上面这些文件放到对应位置:
name: "wails-open-file"
arch: "arm64"
platform: "linux"
version: "1.0.0"
maintainer: "FooBarCorp <FooBarCorp@gmail.com>"
description: "Sample Package"
license: "MIT"
contents:
- src: ../bin/wails-open-file
dst: /usr/bin/wails-open-file
- src: ./main.desktop
dst: /usr/share/applications/wails-open-file.desktop
- src: ./application-wails-mime.xml
dst: /usr/share/mime/packages/application-x-wails.xml
- src: ../appicon.svg
dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg
- src: ../wailsFileIcon.svg
dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-wails.svg
scripts:
postinstall: ./postInstall.sh
postremove: ./postRemove.sh
然后打包:
nfpm pkg --packager deb --target .
NoteUbuntu 有时候不认 hicolor 主题里的文件图标。官方示例的做法是把同一批图标额外拷一份到 Yaru 主题目录下。遇到图标不显示,可以试试这招。
接收路径的方式和 Windows 一样,从 os.Args[1:] 里拿。
28-6 把路径送到前端
Go 侧拿到路径只是第一步,界面得知道该渲染什么。自然会想到用事件把路径 emit 给前端:
runtime.EventsEmit(a.ctx, "open-file", paths)
这里藏着一个非常容易踩的坑:应用启动时,Go 侧比前端快得多。OnStartup 执行的时候,React 还没挂载,事件监听还没注册。这时候 emit 出去,等于对着空气喊,前端什么也收不到。
解决思路是加一个待处理队列,等前端准备好了再发。
type App struct {
ctx context.Context
mu sync.Mutex
pending []string
frontendReady bool
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// 启动参数先存起来,不急着发
if args := os.Args[1:]; len(args) > 0 {
a.mu.Lock()
a.pending = append(a.pending, args...)
a.mu.Unlock()
}
}
// FrontendReady 由前端在挂载完成后主动调用
func (a *App) FrontendReady() {
a.mu.Lock()
defer a.mu.Unlock()
a.frontendReady = true
if len(a.pending) > 0 {
runtime.EventsEmit(a.ctx, "open-file", a.pending)
a.pending = nil
}
}
// handleOpenFiles 运行期间收到新文件时调用
func (a *App) handleOpenFiles(paths []string) {
a.mu.Lock()
defer a.mu.Unlock()
if !a.frontendReady {
a.pending = append(a.pending, paths...)
return
}
runtime.EventsEmit(a.ctx, "open-file", paths)
}
前端这样接:
import { useEffect } from "react";
import { EventsOn } from "../wailsjs/runtime/runtime";
import { FrontendReady } from "../wailsjs/go/main/App";
export function useOpenFile(onOpen: (paths: string[]) => void) {
useEffect(() => {
// 先注册监听,再告诉 Go 我准备好了,顺序不能反
const off = EventsOn("open-file", (paths: string[]) => {
onOpen(paths);
});
FrontendReady();
return () => off();
}, [onOpen]);
}
监听要在通知 Go 之前注册,这个顺序反了就白搭。
这里事件相关的 API 是从 ../wailsjs/runtime/runtime 里 import 进来的,和第 14 章讲事件时的写法保持一致。上两章为了演示 window.runtime 的用法自己写了类型声明,那套声明里并没有 EventsOn;事件用得多,直接用生成好的模块类型更全,也省得每加一个方法就去补一次 .d.ts。
Warning从命令行参数拿到的路径不能无条件信任。用
filepath.Clean规整一下,检查扩展名是不是你支持的,确认文件确实存在再去读。直接os.ReadFile(os.Args[1])是危险写法。
常见误区
误区一:ext 写成 .png。 带点是错的,会导致关联注册失败。
误区二:Windows 上用 wails build 出来的裸 exe 测试关联。 不加 -nsis 就没有安装器,注册表里什么都没写,怎么点都不会唤起。
误区三:以为 macOS 的 OnFileOpen 只在启动时触发一次。 应用运行中被再次「打开方式」调用,它还会触发。逻辑要能重入。
误区四:卸载后关联残留。 Windows 的 NSIS 卸载脚本、Linux 的 postremove 脚本都要负责清理。不然用户卸载了应用,双击文件系统还想去找它。
误区五:调试时反复安装卸载。 效率太低。开发阶段可以用 wails dev -appargs "C:\path\to\test.wails" 模拟带参数启动,逻辑跑通了再去测真实关联。
小结
文件关联的配置很集中——就 wails.json 里那一个数组,加上 build 目录下的几张图标。真正花时间的是三个平台各自的接收方式:Windows 和 Linux 从命令行参数拿,macOS 走 OnFileOpen 回调。
跨平台代码建议这样组织:写一个统一的 handleOpenFiles(paths []string),Windows / Linux 在 startup 里从 os.Args 调它,macOS 在 OnFileOpen 里调它。上层逻辑只有一份。
还有两件事一定别忘:多开问题用单实例锁解决,前端接不到事件用待处理队列解决。下一章讲的自定义协议,配置方式和接收路径都和本章高度相似,理解了这章那章会很快。