Skip to content

页面与导航 ​

ui.Router 像浏览器为标签页所做的那样,保存一个窗口(或窗口的一部分)的页面历史。页面是一个路径,如 /notes/42,View 对它进行匹配来构建所显示的页面——这是一段对模式进行匹配的 switch,其中 {name} 匹配路径的一个部分,{name...} 匹配其余部分。

路由器是你应用状态的一部分:创建一次,带上要首先显示的页面,并保存在一个字段中。每帧都会运行的视图负责构建它显示的页面:

go
// notesApp 是应用的状态。
type notesApp struct {
	router *ui.Router
	// …… 备忘录和文件
}

func main() {
	app := &notesApp{router: ui.NewRouter("/notes")}
	mygo.App.WhenReady(func() {
		mygo.NewWindow(mygo.WindowOptions{Title: "Notes", Content: ui.View(app.view)})
	})
	if err := mygo.App.Run(); err != nil {
		log.Fatal(err)
	}
}

func (app *notesApp) view(c *ui.Context) {
	ui.Row(c).Fill().AlignItems(ui.Stretch).Children(func() {
		app.sidebar(c) // 页面切换时保持不变,见下文
		app.router.View(c, func(r *ui.Route) {
			switch {
			case r.Match("/notes"):
				r.Title("Notes")
				app.notes(c)
			case r.Match("/notes/{id}"):
				r.Title("Note")
				app.note(c, r.Param("id"))
			case r.Match("/files/{path...}"):
				app.files(c, r.Param("path"))
			default:
				ui.Text(c, "Not found")
			}
		})
	})
}

app.notes、app.note 和 app.files 是 notesApp 的方法,分别构建这些页面,就像 view 构建窗口一样。

跳转到页面 ​

Push 跳转到某个页面(排在当前显示的页面之后),Replace 用另一个页面替换当前页面,Back、Forward 和 Go 在历史中前后移动;相对于当前页面的路径,其解析方式与网页中的链接相同,因此 Push("?tab=info") 只改变查询串,Push("edit") 跳转到同级页面。页面中指向某个路径的链接会在其路由器中跳转到该路径;选择侧边栏的某一项会推入对应的页面,正如上面应用的侧边栏所做的那样:

go
func (app *notesApp) sidebar(c *ui.Context) {
	page := app.router.Path()
	ui.Sidebar(c, &page, func() {
		ui.SidebarItem(c, "/notes", nil, "Notes")
		ui.SidebarItem(c, "/files", nil, "Files")
	}).Width(220).OnChange(func() {
		app.router.Push(page)
	})
}

// 在页面中:
ui.Link(c, "Open note", "/notes/42")

Path、Query 和 Location 读取当前显示的页面;Location 带有其查询串,NewRouter 接受它,以便应用启动时重新显示该页面。深度链接(mygo.App.OnOpenURL)会推入其对应的页面。

后退与前进 ​

用户可以像在浏览器和 Finder 中那样后退与前进:macOS 上的 Cmd+[ 和 Cmd+]、Linux 和 Windows 上的 Alt+Left 和 Alt+Right、鼠标的后退/前进按键以及带有这些按键的键盘按键、macOS 触控板上的页面滑动,以及工具栏中的后退与前进按钮——右键单击它们会按标题列出可以后退或前进到的页面。这些按键作用于持有键盘焦点的路由器,若没有,则作用于窗口的第一个路由器。

在浏览器中,窗口显示的第一个路由器跟随标签页的历史,记录在 URL 的片段(#/notes/42)中:跳转到某个页面会向浏览器历史添加一条记录,后退/前进按钮在路由器的页面之间移动,而带有片段的 URL 在打开或重新加载时会显示对应的页面。重新加载后,路由器会随着浏览器的前后移动逐渐了解当前页面之前和之后的页面。嵌在其他页面框架中的 MyGo 应用不会动标签页的历史。

布局 ​

围绕 View 构建的任何内容(如侧边栏或工具栏)都会在页面切换时保持不变。一个页面也可以是围绕其路径其余部分的页面的布局,例如设置页会在所显示分区的旁边列出各个分区:在其路由上调用 r.View 来构建它们,匹配模式 {name...} 所取到的部分。当这些页面切换时,布局保持不变、保留其状态,而页面在布局中滑动或淡入淡出;Param 同样可以读取布局中的通配部分。设置页就是上面视图中 switch 的又一个分支:

go
case r.Match("/settings/{section...}"):
	ui.Row(c).Grow(1).Children(func() {
		app.sectionList(c) // 推入 "/settings/general"、"/settings/fonts"
		r.View(c, func(r *ui.Route) {
			switch {
			case r.Match("/general"):
				r.Title("General")
				app.general(c)
			case r.Match("/fonts"):
				r.Title("Fonts")
				app.fonts(c)
			}
		})
	})

页面内部还可以有第二个 Router,保存在自己的字段中,它拥有独立的历史,例如分栏视图中的详情页可以不断深入,而旁边的列表保持不变。

页面保留状态 ​

页面只要还在历史中,就会保留其元素的状态(前后各十个页面):返回某个页面时,它仍停在原来滚动到的位置,文本和键盘焦点也保持原样。带不同查询串的同一路径是同一个页面,作为历史中的另一条记录保留其状态,就像把标签页或搜索内容放在查询串里的页面一样。

焦点与屏幕阅读器 ​

跳转到某个页面会把键盘焦点移入其中:如果页面内有元素曾经持有焦点,就移给该元素,否则移给页面本身,再按 Tab 可进入页面内容;屏幕阅读器会朗读页面的 Title。如果焦点位于发生切换的页面之外(例如在侧边栏或布局的列表中选择页面),焦点会留在原处,屏幕阅读器则会朗读新页面的标题。

过渡 ​

路径层级更深的页面(如从 /notes 进入的 /notes/42)会从右侧滑入,覆盖在它离开的页面上,返回时再从左侧滑回;其他页面则淡入。当桌面要求减少动态效果时,所有页面都只淡入;router.Transition = ui.TransitionNone 会让页面直接显示。正在离开的页面在滑动过程中不接收指针和键盘输入。

router.Animation 设置页面的移动方式,可以是弹簧或曲线(见动态效果),用来替代默认的滑动时 250 ms 缓出、淡入时 150 ms 缓出:

go
router.Animation = &ui.Snappy