外观
应用程序
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 可清除其他会话,或只清除某些种类的数据。
Cookie
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:引擎为站点保存的全部数据,即上述种类加上它所拥有的其他种类。
| 持久会话 | 临时会话 | |
|---|---|---|
| macOS | macOS 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 视图。