Skip to content

应用程序 ​

mygo.App 就是应用程序本身:它的生命周期、它的身份标识,以及它与系统其余部分的交互方式。

main 函数 ​

go
func main() {
	mygo.Bind(&Notes{})                // 前端调用的服务
	mygo.App.WhenReady(func() {        // 应用启动完成后
		mygo.NewWindow(mygo.WindowOptions{URL: "/"})
	})
	if err := mygo.App.Run(); err != nil { // 事件循环,直到应用退出
		log.Fatal(err)
	}
}

App.Run 运行原生事件循环,并在应用退出时返回。它必须在 main goroutine 中调用,因为每个平台的 UI 工具包都必须在启动进程的线程上运行;MyGo 让 main goroutine 始终留在该线程上。只有应用就绪后窗口才存在:请在 WhenReady 中创建窗口,或在此之后从任意 goroutine 创建。

在 Run 之前,main 负责配置应用:绑定服务、处理协议、添加监听器,并进行诸如 App.SetName、App.SetMenu、App.Dock.SetMenu 和 Theme.SetSource 等设置,它们在应用启动时生效。它还可以读取那些不需要应用运行的信息,例如 App.Path、App.Locale 或 Power.IsOnBattery,并向系统注册应用(App.SetOpenAtLogin、App.RegisterURLScheme)。其余一切都需要应用处于运行状态:如果在 Run 之前,main 调用剪贴板、显示器、对话框、通知、全局快捷键、托盘、Dock 徽标计数、Theme.IsDark、Theme.Preferences 之类的内容,在所有平台上它们都会 panic,并在错误信息中指出调用名。请从 WhenReady 开始使用它们;其他 goroutine 可以更早调用,并等待应用启动。

线程 ​

MyGo 的每个函数和方法都可以从任意 goroutine 安全调用:非主线程发出的调用会被转发到主线程,并等待结果。各部分运行的位置:

  • 事件监听器,如 OnClose 或 OnActivate、菜单项点击和全局快捷键都在主线程上运行,主线程同时负责绘制 UI:保持它们简短,耗时的工作请另起 goroutine。
  • 绑定的方法在各自的 goroutine 上运行,每次调用一个:它们可以阻塞。
  • mygo.RunOnMain(fn) 在主线程上运行 fn,用于通过 Window.NativeHandle 调用的原生 API。

生命周期 ​

事件时机
OnReady应用已启动;当应用已经就绪时,WhenReady 也会立即运行其函数
OnActivate(func(hasVisibleWindows bool))应用再次被激活,例如点击了它的 Dock 图标(macOS)
OnDidBecomeActive, OnDidResignActive应用成为,或不再是,当前活动的应用
OnWindowCreated(func(w *mygo.Window))创建了窗口
OnWindowAllClosed最后一个窗口关闭,除非应用正在退出
OnBeforeQuit(func(e *mygo.QuitEvent))退出开始,窗口尚未关闭;e.PreventDefault() 可取消退出
OnWillQuit(func(e *mygo.QuitEvent))所有窗口都已关闭,应用即将退出;此时仍可取消
OnQuit事件循环停止,就在 Run 返回之前

每个方法都返回一个用于移除该监听器的函数。

退出 ​

当最后一个窗口关闭时,应用就会退出,除非 OnWindowAllClosed 注册了监听器。在 macOS 上,应用通常带着 Dock 图标继续运行,点击图标时重新打开一个窗口:

go
mygo.App.OnWindowAllClosed(func() {
	if runtime.GOOS != "darwin" {
		mygo.App.Quit()
	}
})
mygo.App.OnActivate(func(hasVisibleWindows bool) {
	if !hasVisibleWindows {
		openMainWindow()
	}
})
  • App.Quit() 先关闭每个窗口(OnClose 监听器可以拒绝),然后退出;OnBeforeQuit 和 OnWillQuit 监听器也可以取消退出。Cmd+Q、"退出"菜单项以及终端中的 Ctrl+C(SIGINT 和 SIGTERM)都以同样的方式退出;第二个信号会立即终止进程。
  • App.Exit(code) 立即退出,不触发事件。
  • App.Relaunch() 退出后以相同的参数重新启动应用,例如在某项需要重启才能生效的设置之后。

单实例 ​

大多数桌面应用只运行一次:再次启动会把正在运行的实例带到前台。在第二个实例中,RequestSingleInstanceLock 会先把它的命令行传递给第一个实例,然后返回 false:

go
func main() {
	if !mygo.App.RequestSingleInstanceLock() {
		return // 由正在运行的实例接管
	}
	mygo.App.OnSecondInstance(func(args []string, workingDir string) {
		if w := mainWindow(); w != nil {
			w.Restore()
			w.Focus()
		}
	})
	// ...
}

应用可以打开自己协议方案的 URL,例如 myapp://open?note=42。在 mygo.config.ts 中列出这些方案,应用在安装时会将它们注册到系统:

ts
export default defineConfig({
  urlSchemes: ["myapp"],
});

并用 OnOpenURL 处理这些 URL;它要在 Run 之前注册,这样应用启动时所携带的 URL 也能送达:

go
mygo.App.OnOpenURL(func(rawURL string) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return
	}
	openNote(u.Query().Get("note"))
})

在 Windows 和 Linux 上,一个 URL 会启动应用的新实例:请申请单实例锁,后续实例的 URL 就会送达第一个实例的 OnOpenURL。没有以 mygo build 生成的安装包安装的应用,例如可移植的可执行文件,可以在运行时用 App.RegisterURLScheme("myapp") 注册自己的方案,每次启动调用它的开销很小;与之配套的还有 UnregisterURLScheme 和 IsURLSchemeRegistered。在 macOS 上,方案必须列在配置中。

文件关联 ​

应用可以从文件管理器打开自己类型的文件。在 mygo.config.ts 中声明这些类型:

ts
export default defineConfig({
  fileAssociations: [
    {
      ext: ["md", "markdown"],
      name: "Markdown Document",
      mimeType: "text/markdown",
    },
  ],
});

并用 OnOpenFile 打开它们;它要在 Run 之前注册,这样应用启动时所携带的文件也能送达:

go
mygo.App.OnOpenFile(func(path string) {
	openDocument(path)
})

它还会收到拖到应用 Dock 图标上的文件(macOS)以及通过命令行传入的文件。与深度链接一样,单实例锁能让 Windows 和 Linux 上的第一个实例收到后续实例的文件。

每个窗口显示一个文件的应用,可以从文档获得这一功能以及基于文档的应用的其他所有能力。

最近使用的文档 ​

记录用户打开过的文档,以便应用和系统再次提供它们:

go
if err := mygo.App.AddRecentDocument(path); err != nil {
	log.Println(err)
}

App.RecentDocuments() 返回这些文档,最新的在前,最多十条,也包括先前运行时记录的,已不存在的文件会被排除;打开最近文档菜单(RoleOpenRecent,见菜单)会列出它们。macOS 还会在应用的 Dock 菜单中列出,Linux 在用户最近使用的文件中列出(~/.local/share/recently-used.xbel,即文件选择器和文件管理器的"最近"列表),Windows 则在应用跳转列表的"最近"类别中列出,仅限应用注册可打开的文件类型(见文件关联)。在打开最近文档菜单、Dock 菜单或跳转列表中选择其中一个,就会用应用打开该文件,应用通过 OnOpenFile 收到它。文档会记录它们打开和保存的文件,并自行打开被选中的文件;其他应用,例如打开文件夹的应用,则记录它们打开的内容。

ClearRecentDocuments 会移除全部记录,例如在用户要求清除应用历史记录时,正如打开最近文档菜单中的"清除菜单"项所做的那样。在 Linux 上,用户其他最近使用的文件不受影响。

登录时启动 ​

go
if err := mygo.App.SetOpenAtLogin(true); err != nil {
	log.Println(err)
}
enabled := mygo.App.OpenAtLogin()

把它作为一项设置提供给用户。在 macOS 13 及更高版本上,应用成为用户的登录项(需要打包后的应用),在 macOS 12 上是 launch agent,在 Linux 上是 XDG 自启动条目,在 Windows 上是用户的 Run 项;系统设置会列出它们并可关闭。WasOpenedAtLogin 可以识别出这是登录时启动的情形,以便在后台启动:

go
mygo.NewWindow(mygo.WindowOptions{URL: "/", Hidden: mygo.App.WasOpenedAtLogin()})

目录 ​

App.Path 返回常用目录:

名称目录
PathUserData应用的数据目录:~/Library/Application Support/<Name>、~/.config/<Name>($XDG_CONFIG_HOME)或 %AppData%\<Name>;首次使用时创建
PathCache应用的缓存目录:~/Library/Caches/<Name>、~/.cache/<Name> 或 %LocalAppData%\<Name>;首次使用时创建
PathLogs写入日志的位置:~/Library/Logs/<Name>,否则为用户数据目录下的 logs;首次使用时创建
PathResources应用随附的文件,见资源
PathAppData每用户的配置目录,不含应用名称
PathHome, PathTemp, PathExe主目录、临时目录、可执行文件
PathDesktop, PathDocuments, PathDownloads, PathMusic, PathPictures, PathVideos用户的各个文件夹
go
dir, err := mygo.App.Path(mygo.PathUserData)
db := filepath.Join(dir, "notes.db")

App.SetPath 可覆盖某个目录,例如把测试数据分开存放。

名称与版本 ​

打包后的应用从配置中得知自己的名称和版本,mygo build 会将其链接进应用:App.Name() 和 App.Version()。用 go run 运行的程序只有可执行文件的名称而没有版本,除非它在 Run 之前调用 App.SetName 和 App.SetVersion。名称决定了用户数据、缓存和日志目录以及单实例锁。mygo dev 的开发构建命名为 <Name> Dev,以便它们与已安装应用的数据分开保存;调用 SetName 又会把它们关联到一起。

mygo.IsDev() 报告应用是否为开发构建:go run、go build 和 mygo dev 的构建是,mygo build 的构建不是,而 MYGO_ENV=production 会让任何构建都表现得像生产构建。App.IsPackaged() 报告应用是否从应用包(bundle)中运行。App.Locale() 返回应用所显示的语言,例如 "zh-Hans",取自应用已翻译的语言;App.PreferredLanguages() 返回用户阅读的语言,按系统列出的顺序;见应用显示的语言。在 0.5 之前,无论应用以哪些语言编写,App.Locale() 返回用户首选语言列表中的第一个。

Dock 与任务栏 ​

  • App.SetBadgeCount(n) 在应用图标上显示计数:macOS 的 Dock,或 Linux 上支持徽标的停靠栏启动项;置零即清除。
  • App.Dock 控制 macOS 上的 Dock 图标:SetBadge 显示任意文本,Bounce 请求用户注意,SetIcon 替换图标,SetMenu 向其菜单添加条目(见Dock 菜单),Hide 和 Show 则移除并恢复它。
  • App.SetActivationPolicy(mygo.ActivationPolicyAccessory) 会移除常驻菜单栏应用的 Dock 图标,见托盘图标;它的窗口仍可显示。App.ActivationPolicy 返回当前策略。
  • App.Focus 将应用带到前台,App.IsActive 报告它是否在前台:即活动应用,或在 Windows 和 Linux 上是拥有焦点窗口的应用。在 macOS 上,App.Hide 和 App.Show 隐藏并显示其全部窗口,App.ShowAboutPanel 显示标准的应用简介面板。

跳转列表 ​

在 Windows 上,右键点击应用的任务栏按钮会显示它的跳转列表:用户最近打开的文档(见最近使用的文档)以及任务——要运行的程序和参数,通常是应用自身,例如文本编辑器的"新建笔记"。App.JumpList.SetTasks 设置任务;应用退出后 Windows 仍会保留它们,所以启动时设置一次就够了:

go
mygo.App.OnOpenURL(func(rawURL string) {
	if rawURL == "myapp://new-note" {
		newNote()
	}
})
// ... 应用就绪后:
if err := mygo.App.JumpList.SetTasks([]mygo.JumpListTask{
	{Title: "New note", Description: "Write a new note", Args: []string{"myapp://new-note"}},
	{Title: "Notepad", Path: `C:\Windows\notepad.exe`},
}); err != nil {
	log.Println(err)
}

运行应用自身的任务会以 Args 再次启动应用:应用未运行时,新实例收到这些参数;应用正在运行时,若使用了单实例,则由正在运行的实例收到。无论哪种情况,应用某个方案的 URL 都会送达 OnOpenURL,其类型的文件会送达 OnOpenFile,所以任务最好由它们构成。其他参数进入新实例的 os.Args,以及正在运行实例的 OnSecondInstance:两处都要处理。每个参数都按原样传递,MyGo 会为命令行对它们加引号。

若未设置,Path 就是应用自己的可执行文件,WorkingDir 是其运行目录,Description 是工具提示;除非用 IconPath(配合 IconIndex)指定其他图标,否则图标就是程序自身的。传入空任务列表会移除全部任务。每当应用改动跳转列表的一部分时,Windows 都会替换整个列表,因此 App.JumpList 会保存各个部分并一起提交。其方法在 macOS 和 Linux 上不做任何事。

浏览数据 ​

App.ClearBrowsingData() 删除应用页面存储的内容:cookie、本地存储与会话存储、IndexedDB、service worker 与缓存,例如在用户登出时。已打开的页面会把内存中的内容保留到重新加载为止。它清除默认会话;Session.ClearData 可清除其他会话,或只清除某些种类的数据。

mygo.Cookies 读取并修改应用页面的 cookie,包括 HttpOnly 的:即默认 cookie 存储,所有窗口共享,系统 webview 会将其从本次运行保留到下次。例如,可以在第一个页面加载之前设置一个 cookie,用后端获取的令牌让页面登录;也可以读取某个页面登录时设置的会话:

go
err := mygo.Cookies.Set(mygo.Cookie{
	Name:     "session",
	Value:    token,
	Domain:   "example.com",
	Secure:   true,
	HTTPOnly: true,
	Expires:  time.Now().Add(30 * 24 * time.Hour),
})

// 向该 URL 发出的请求会携带的 cookie。
sent, err := mygo.Cookies.List(mygo.CookieFilter{URL: "https://example.com/account"})
// example.com 及其子域中名为 session 的 cookie。
sessions, err := mygo.Cookies.List(mygo.CookieFilter{Name: "session", Domain: "example.com"})
// 全部 cookie,按域、路径和名称排序。
all, err := mygo.Cookies.List(mygo.CookieFilter{})

err = mygo.Cookies.Delete(mygo.Cookie{Name: "session", Domain: "example.com"})
err = mygo.Cookies.Clear() // 全部 cookie,但不含页面的存储

cookie 的名称、域和路径共同标识它:Set 替换名称、域和路径都相同的 cookie,Delete 删除它而不顾其他字段,因此它能删除 List 返回的 cookie。不带前导点的域属于某个主机,只发送给该主机;带前导点的(.example.com)cookie 也会发送到其子域。域为 ASCII(其他字符用 punycode),空路径即 /。空的 SameSite 视为 Lax,None 需要配合 Secure。名称和值接受浏览器所能接受的内容:除控制字符、; 以及首尾空格之外的任何字符,名称中不含 =。Expires 为零的 cookie 一直有效直到应用退出;设为过去的时间则会删除该 cookie。

过滤器的各字段用于缩小列表范围:URL 是一个 http 或 https URL,保留向它发出请求时会携带的 cookie,在引擎能够支持的平台上由引擎匹配(macOS 上按 RFC 6265 的规定以域、路径和 Secure 匹配);Name 保留指定名称的 cookie;Domain 保留某个域及其子域的 cookie,带或不带前导点均可;Path 精确匹配某个路径。

应用运行后,这些方法可以从任意 goroutine 安全调用;在主线程上,它们等待结果期间应用会继续处理事件。cookie 可能携带用户的凭据:与其把该模块绑定给页面,不如绑定一个后端方法,由它完成页面所需的操作。

在 Windows 上,WebView2 只通过 web 视图提供 cookie:在窗口显示页面之前,这些方法以及 App.ClearBrowsingData 会创建一个隐藏的 web 视图,这同时也会启动 WebView2 浏览器进程;窗口的第一个页面在创建其 web 视图时,会等待正在进行的调用完成。WebView2 在其调用返回后才应用更改,因此这些方法会等待更改生效,若两秒内未生效则失败,例如引擎拒绝某个 cookie 时。在 Linux 上,它们需要 WebKitGTK 2.42 或更新版本,Set 还需要 libsoup 2.70 或更新版本;WebKitGTK 把 cookie 保存在应用用户数据目录(~/.config/<Name>)的 cookies.sqlite 中。

会话 ​

除非 PageOptions.Session 指定了其他会话,每个页面都在默认会话中显示:一个会话拥有自己的 cookie、本地与会话存储、IndexedDB、缓存和 service worker 存储,其他会话的页面看不到它们,类似于 Electron 的 partitions。并排登录同一服务的两个账号、一个隐私窗口、一个绝不能看到用户登录状态的预览,或一个与应用自身存储相互隔离的第三方页面,各自都需要一个会话:

go
work := mygo.NewSession("work")        // 从本次运行到下次保存在磁盘上
private := mygo.NewEphemeralSession()  // 仅存于内存

win := mygo.NewWindow(mygo.WindowOptions{
	URL:  "https://mail.example.com",
	Page: mygo.PageOptions{Session: work},
})

list, err := work.Cookies.List(mygo.CookieFilter{})
err = work.Cookies.Set(mygo.Cookie{Name: "session", Value: token, Domain: "example.com"})
err = work.ClearData(mygo.SiteDataAll)                             // 登出
err = work.ClearData(mygo.SiteDataCookies | mygo.SiteDataCaches)  // 某些种类

NewSession 对相同的名称返回同一个会话,其数据像默认会话一样保存在磁盘上。NewEphemeralSession 每次返回一个新会话,不向磁盘写入任何内容:使用它的最后一个页面关闭时,它的数据就消失了,它的下一个页面从零开始。在其第一个页面打开之前设置的 cookie 会交给那个页面。mygo.Cookies 和 App.ClearBrowsingData 操作的是默认会话,mygo.DefaultSession() 返回的也是它。页面用 window.open 或 target="_blank" 链接打开的窗口,处于打开者所在的会话中,无论打开处理函数返回的选项里的 Session 是什么。自定义协议(mygo.Protocol)为所有会话的页面服务,原生 UI中的 web 视图也可以在其 PageOptions 中指定会话。

ClearData 接受 SiteDataCookies、SiteDataLocalStorage(含会话存储)、SiteDataIndexedDB(含 WebSQL)、SiteDataCaches(HTTP 缓存与 Cache Storage)、SiteDataServiceWorkers,或 SiteDataAll:引擎为站点保存的全部数据,即上述种类加上它所拥有的其他种类。

持久会话临时会话
macOSmacOS 14 起使用 WKWebsiteDataStore dataStoreForIdentifier:,以名称生成 UUID;更早版本上则是持续到应用退出才消失的内存存储nonPersistentDataStore
Linux拥有自己的 web context,数据存放在用户数据目录的 Sessions/<UUID>,缓存存放在缓存目录的对应位置;需要 WebKitGTK 2.16 或更新带临时网站数据管理器的 web context
Windows应用的 WebView2 用户数据中的 WebView2 配置文件 mygo-<UUID>;需要 WebView2 Runtime 1.0.1185 或更新InPrivate 配置文件 mygo-private-<n>,WebView2 会在其最后一个 web 视图销毁时将其删除

在 Windows 上,会话的 cookie 来自其配置文件中的一个 web 视图,与默认会话相同:在其某个窗口显示页面之前,调用会先创建一个隐藏的 web 视图。