Flutter Favorite之路由包go_router - 基础篇
go_router
欢迎来到 go_router !
MaterialApp.router
指定的 Flutter Router API 需要 RouterDelegate
类和 RouteInformationParser
类的实现。
这两个实现本身意味着第三种类型的定义,该类型保有驱动创建 Navigator
的应用状态。 可以阅读 Medium 上一篇关于这些需求的优秀博客(需要借助一些工具访问)。这种责任分离允许 Flutter 开发者来实现多种路由和导航策略,包括深度链接和动态链接,但是代价是复杂度。
go_router 包的目的是使用声明式路由来降低复杂度,无论面向的是哪个目标平台(移动端、Web、桌面端)。在 Android、iOS 和 Web 上处理深度链接和动态链接,以及多个其它与导航关联的场景,同时仍然(有望)提供一个简单易用的开发者体验。
开始
go_router | Flutter Package (flutter-io.cn)
官网提供的视频在油管上,有兴趣的同学可前往观看。
声明式路由
go_router 由 GoRouter
构造器中指定的路由集合部分来管理。
class App extends StatelessWidget { ... final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const Page1Screen(), ), GoRoute( path: '/page2', builder: (context, state) => const Page2Screen(), ), ], ); } 复制代码
这种情况下,定义了两个路由,每个路由 path
都会匹配用户导航的位置。只有单个路由会被匹配,具体来说,就是某个路由的 path
会匹配整个位置(所以路由的列出顺序无关紧要)。 path
匹配时忽略大小写,即使参数会保留大小写。
除了 path
之外,每个路由会代表性地带有一个 builder
函数,该函数负责构建用来占据应用整个屏幕的组件。页面间会使用默认转换 ,取决于添加到组件树顶部的应用类型。例如,使用 MaterialApp
会让 go_router 使用 MaterialPage
的转换。
路由状态
如果在上面代码片段中不使用时,builder
函数会接收一个 state
对象,该对象是 GoRouterState
的一个实例,包含一些有用的信息:
GoRouterState
:
属性 | 描述 | 示例 1 | 示例 2 |
---|---|---|---|
location | 完整路由的位置,包含查询参数 | /login?from=/family/f2 | /family/f2/person/p1 |
subloc | 子路由的位置,不包含查询参数 | /login | /family/f2 |
name | 路由名称 | login | family |
path | 路由路径 | /login | family/:fid |
fullpath | 该子路由的完整路径 | /login | /family/:fid |
params | 从位置中提取的参数 | {} | {'fid': 'f2'} |
queryParams | 位置末尾的可选参数 | {'from': '/family/f1'} | {} |
extra | 可选的对象参数 | null | null |
error | 如果有 Exception('404') ,该属性为子路由关联的 Exception (异常) | ... | |
pageKey | 该子路由的唯一键 | ValueKey('/login') | ValueKey('/family/:fid') |
state
对象用于为参数化路由传递参数和重定向。不是每次都要设置所有的 state 参数。通常, GoRouterState
定义一个 GoRouterState
实例可能的当前 state 的超集。例如,仅当有错误时会设置 error
参数。
初始化
用路由列表可以创建 GoRouter
的实例,它自身提供了调用 MaterialApp.router
构造器(或CupertinoApp.router
构造器)所需的对象:
class App extends StatelessWidget { App({Key? key}) : super(key: key); @override Widget build(BuildContext context) => MaterialApp.router( routeInformationParser: _router.routeInformationParser, routerDelegate: _router.routerDelegate, ); final _router = GoRouter(routes: ...); } 复制代码
使用合适位置的路由,你的应用现在可以在页面间导航navigate between pages了。
错误处理
默认情况下,go_router 会为 MaterialApp
和 MaterialApp
转到默认的错误界面,即使没有为两者设置默认的错误界面。也可以通过设置 GoRouter
的 errorBuilder
参数来替换掉默认的错误界面。
class App extends StatelessWidget { ... final _router = GoRouter( ... errorBuilder: (context, state) => ErrorScreen(state.error), ); } 复制代码
错误界面,无论你或 go_router 有没有提供,都会在以下情况时使用:位置没有匹配路由(404),一个位置匹配了多个的路由,或者某个 builder 函数抛出了异常。
深度链接
Flutter 把 "深度链接" 定义为 打开一个 URL 来展示应用中的对应页面。任意作为 GoRoute
列出的内容都可以在 Android、iOS、Web 平台通过深度链接(打开)。对于支持 Web 开箱即用,当然是通过地址栏。但是在 Android 和 iOS 上需要另外的配置,在 Flutter docs 文档中有相关描述。
导航
在页面间导航,使用 GoRouter.go
方法:
// 使用 GoRouter 导航 onTap: () => GoRouter.of(context).go('/page2') 复制代码
go_router 还提供了一个使用 Dart 扩展方法的简化的导航:
// 使用 GoRouter 导航更容易 onTap: () => context.go('/page2') 复制代码
简化的版本直接映射到完整版本,所以也可以使用。 如果你对此好奇,只是调用 context.go(...)
就能发生魔法,这就是 go_router 的来历。
如果想用 Link
组件来导航,那也是有效的:
Link( uri: Uri.parse('/page2'), builder: (context, followLink) => TextButton( onPressed: followLink, child: const Text('Go to page 2'), ), ), 复制代码
如果为 Link
组件提供一个带 scheme (页面内跳转协议) 的 URL,例如:https://flutter.dev
,它会在浏览器中载入这个 URL。否则,它会在应用内使用内置的导航系统打开链接。
也可以导航到一个命名式路由 [中文] 。
页面入栈
除了 go
方法之外, go_router 还提供了 push
方法。 go
和 push
都可用来构建页面栈,但是方式不同。 go
方法通过使用 子路由 把单个位置转换为栈中的多个页面。
push
方法把单个页面推入到现有页面的栈中,这意味着可以编程式来构建栈,而不是声明式。当 push
方法通过子路由匹配整个栈时,它会从栈中选择最顶层的页面,并推入到栈中。
你也可以 push 一个命名式路由 [中文] 。
弹出页面
如果想要从栈中弹出一个页面,可以使用 pop
方法。这个方法只是简单地调用 Navigator.pop
。关于使用 Navigator
和 GoRouter
集成的注意事项的更多信息,参考导航集成。
初始化位置
如果想为路由设置一个初始化位置,可以设置 GoRouter
构造器的 initialLocation
参数:
final _router = GoRouter( routes: ..., initialLocation: '/page2', ); 复制代码
如果应用是通过深度链接 [中文]启动的,提供给 initialLocation
的值会被忽略。
当前位置
如果想知道当前位置,使用 GoRouter.location
属性。
如果想知道现在位置改变的时间点,由手动导航、或深度链接、或用户点击返回按钮的弹出,GoRouter
本身也是 ChangeNotifier
,这意味着可以调用 addListener
用来在现在位置改变时接收通知,手动或通过用 Flutter 为 ChangeNotifier
对象构建组件 AnimatedBuilder
,命名不是很直观。
class RouterLocationView extends StatelessWidget { const RouterLocationView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final router = GoRouter.of(context); return AnimatedBuilder( animation: router, builder: (context, child) => Text(router.location), ); } } 复制代码
或者,如果在使用 provider 包,当 ChangeNotifier
带有更清晰目的的类型进行改变时,会使用内置支持来重绘组件。
参数
路由路径通过 path_to_regexp 包来定义和实现,使在路由路径中包含参数成为可能:
final _router = GoRouter( routes: [ GoRoute( path: '/family/:fid', builder: (context, state) { // use state.params to get router parameter values // 使用 state.params 获取路由参数的值 final family = Families.family(state.params['fid']!); return FamilyScreen(family: family); }, ), ], ]); 复制代码
可以使用 state
对象的 params
来访问匹配的参数。
动态链接
动态链接的想法是用户可以为应用添加这样的对象,每个对象获得一个自身的链接,例如,一个新的 family 获得一个新的链接。这正是路由参数能做到的,例如,一个新的 family 有它自身的 ID,可以在 family 路由中变量化时使用,例:路径:/family/:fid
。
查询参数
在路径中包含参数是给页面传递信息的一种方式。需要一个 "路径" 参数内连对应的位置。另外一个方式是使用查询参数作为位置的一部分来传递数据,即在 URI 末尾的 ?
字符后跟着的名称值的集合。例:
void _tap() => context.go('/search?query=kitties'); 复制代码
这些参数是可选的,如果传递了这些参数,会在路由栈匹配的每个页面中的 state.queryParams
中提供。
GoRoute( path: '/search', builder: (context, state) { // use state.queryParams to get search query from query parameter // 使用 state.queryParams 从查询参数中获取 query (的值) final query = state.queryParams['query']; // may be null return SearchPage(query: query); }, ), 复制代码
因为查询参数是可选的,未传递参数时,它们的值为 null
。
附加参数
除了传递路径参数和查询参数之外,也可以传递附加的对象作为导航的一部分。例如:
void _tap() => context.go('/family', extra: _family); 复制代码
这个对象作为 state.extra
(属性)来提供:
GoRoute( path: '/family', builder: (context, state) => FamilyScreen(family: state.extra! as Family), ), 复制代码
只是想要简单地给 builder
函数传递单个对象,而不想通过在 URI 中传递对象 ID 从存储中查找对象时,state
对象会有用。当用户按下 AppBar
的返回按钮时, extra
对象也会正确传递。
尽管如此, extra
对象不能用于创建动态链接或者在深度链接中使用。 此外,按下浏览器的回退按钮会被当作深度链接导航的目的来对待, extra
对象会在用户使用浏览器的回退导航时丢失。 由于深层的原因, extra
对象不建议用于定位为 Flutter Web 的应用。
子路由
每个顶层路由会创建一个页面的导航栈。要产出页面的整个栈,可以使用子路由。 一个顶层路由只匹配位置的一部分,位置的其余部分匹配子路由。 规则还是一样的,即:在任意层级只有一个路由会匹配,然后整个位置会被匹配。
例如,位置 /family/f1/person/p2
,会使其匹配多个子路由来创建页面栈。
/ => HomeScreen() family/f1 => FamilyScreen('f1') person/p2 => PersonScreen('f1', 'p2') ← 显示此页面,后退时弹出栈 ↑ 复制代码
要指定这样的页面集合,可以通过 GoRouter
构造器的 routes
参数使用子页面路由。
final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(families: Families.data), routes: [ GoRoute( path: 'family/:fid', builder: (context, state) { final family = Families.family(state.params['fid']!); return FamilyScreen(family: family); }, routes: [ GoRoute( path: 'person/:pid', builder: (context, state) { final family = Families.family(state.params['fid']!); final person = family.person(state.params['pid']!); return PersonScreen(family: family, person: person); }, ), ], ), ], ), ], ); 复制代码
go_router 会匹配子路由下的所有路径树来构建一个页面栈。如果 go_router 没有匹配任意一个路径,会调用错误处理器。
go_router 也会从高一级的子路由传递参数,这样可以在低一级的子路由中使用,例如,fid
匹配 family/:fid
路由的一部分,但它也会传递给 person/:pid
路由,因为后者是 family/:fid
路由的子路由。
重定向
有时候,想让应用重定向到一个不同的位置。 go_router 允许在顶层为每个新的导航事件或为特定路由的路由层级来做这件事。
顶层重定向
有时想要守护某些页面防止不应允许的访问,例如,当用户没有登录时。举个例子,假设有个类用于追踪用户的登录信息:
class LoginInfo extends ChangeNotifier { var _userName = ''; String get userName => _userName; bool get loggedIn => _userName.isNotEmpty; void login(String userName) { _userName = userName; notifyListeners(); } void logout() { _userName = ''; notifyListeners(); } } 复制代码
可以在传递给 GoRouter
构造器的 redirect
函数的实现中使用这个信息。
class App extends StatelessWidget { final loginInfo = LoginInfo(); ... late final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(families: Families.data), ), ..., GoRoute( path: '/login', builder: (context, state) => LoginScreen(), ), ], // 用户未登录时,重定向到登录页面 redirect: (state) { final loggedIn = loginInfo.loggedIn; final goingToLogin = state.location == '/login'; // the user is not logged in and not headed to /login, they need to login // 用户未登录,且不是前往 /login,则需要登录。 if (!loggedIn && !goingToLogin) return '/login'; // the user is logged in and headed to /login, no need to login again // 用户已登录且是前往 /login,则不需要再次登录。 if (loggedIn && goingToLogin) return '/'; // no need to redirect at all // 无需重定向 return null; }, ); } 复制代码
在这段代码中,如果用户没有登录,并且不是前往 /login
路径,则重定向到 /login
。同样地,如果用户已经 登录,但是要前往 /login
, 则重定向到 /
。如果不需要重定向,则只返回 null
。 直到返回 null
使多次重定向可用时, redirect
函数会再次被调用。
为了使在APP中的任何需要的地方访问这些信息更简单,可以考虑使用状态管理选项如 provider 把登录信息放到组件树中。
class App extends StatelessWidget { final loginInfo = LoginInfo(); // add the login info into the tree as app state that can change over time // 添加登录信息到树中作为应用状态,可随时间改变 @override Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value( value: loginInfo, child: MaterialApp.router(...), ); ... } 复制代码
用组件树中的登录信息,可以简单地实现登录界面:
class LoginScreen extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: Text(_title(context))), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () { // log a user in, letting all the listeners know context.read<LoginInfo>().login('test-user'); // go home context.go('/'); }, child: const Text('Login'), ), ], ), ), ); } 复制代码
这种情况下,使用户为已登录状态,然后手动重定向到主页的界面。这是因为 go_router 不会感知应用状态是否已经以影响路由的方式发生改变。如果想要应用的状态触发 go_router 自动重定向,可以在 GoRouter
中使用 refreshListenable
参数:
class App extends StatelessWidget { final loginInfo = LoginInfo(); ... late final _router = GoRouter( routes: ..., redirect: ... // changes on the listenable will cause the router to refresh it's route // listenable 的改变会触发路由器刷新它的路由 refreshListenable: loginInfo, ); } 复制代码
因为 loginInfo
是一个 ChangeNotifier
,所以当它改变时会通知监听器。通过传递给 GoRouter
构造器, 当用户信息改变时, go_router 会自动刷新路由。这样允许在应用中简化登录逻辑:
onPressed: () { // log a user in, letting all the listeners know context.read<LoginInfo>().login('test-user'); // router will automatically redirect from /login to / because login info //context.go('/'); }, 复制代码
建议同时使用顶层的 redirect
和 refreshListenable
,因为应用数据改变时,它能自动处理路由。
用 Stream
刷新
如果是要获取到一个 Stream
来响应,可以用 GoRouterRefreshStream
包装一下获取到的流,这使 GoRouter
可以响应任何基于状态管理解决方案(例如: BLoC)的流。
class App extends StatelessWidget { final streamController = StreamController(); ... late final _router = GoRouter( routes: ..., redirect: ... // changes on the listenable will cause the router to refresh it's route refreshListenable: GoRouterRefreshStream(streamController.stream), ); } 复制代码
路由层级重定向
无论何时有新的导航事件和需要根据应用的当前状态进行决定(处理),当需要一个简单函数用于调用时,(把这个函数)传递给 GoRouter
构造器的顶层重定向处理器都很方便。 尽管如此,在想要决定为一个指定的路由(或子路由)重定向的情况时,也可以通过传递一个 redirect
函数给 GoRouter
构造器来做到:
final _router = GoRouter( routes: [ GoRoute( path: '/', redirect: (_) => '/family/${Families.data[0].id}', ), GoRoute( path: '/family/:fid', builder: ..., ], ); 复制代码
这种情况下,当用户导航到 /
时, redirect
函数会被调用,然后重定向到第一个 family 的页面。重定向只在最后一个子路由被匹配时触发,所以无论如何当已经准备好访问另一个页面时,不需要担心在访问的位置中间解析时发生重定向。
注意事项
当使用路由层级的重定向时,有一些微妙的事项需要注意。如果有一个路由,带有 redirect
函数,只在特定条件下重定向时,还需要提供一个 builder
函数,例如:
GoRoute( path: '/', redirect: (_) => kGoElsewhere ? '/elsewhere' : null, builder: (context, state) => ..., // need this if kGoElsewhere == false ) 复制代码
builder
函数会在重定向条件为 false 时调用。
如果 redirect
函数是子路由的一部分,也需要提供 builder
函数,例如:
GoRoute( path: '/profile', redirect: (_) => '/profile/home', // only called when going to /profile builder: (c, s) => ..., // need this to build /profile/:section stack routes: [ GoRoute( path: ':section', builder: ...ProfileScreen(state.params['section']!)..., ), ], ) 复制代码
redirect
函数只在匹配的路由为栈中最顶层的路由时被调用,所以如果 redirect
是在栈中间的路由被提供的话, builder
会在为上面的堆栈构建页面时被调用。
事实上,你可能无论如何也不想在页面栈的中间有 redirect
。你可能希望 redirect
函数在单独位于栈上,并重定向到另一个顶层路由:
GoRoute( path: '/profile', redirect: (_) => '/profile/home', // 这种情况下不需要 builder ), GoRoute path: '/profile/:section', builder: ...ProfileScreen(state.params['section']!)..., ) 复制代码
当然,在路由中没有任何一个路由层级的 redirect
函数的情况下,还是需要 builder
函数。
参数化重定向
在一些情况下,路径是参数化的,然后想在重定向时记住这些参数。可以用传递给redirect
函数的 state
对象的 params
变量来记住这些参数。
GoRoute( path: '/author/:authorId', redirect: (state) => '/authors/${state.params['authorId']}', ), 复制代码
多次重定向
单次导航也可以进行多次重定向,例如:/ => /foo => /bar
。这很方便,因为它允许构建路由列表而不必担心试图把每一个路由修整为它们的直接路由。此外,也可以在顶层路由和路由层级进行任意数量的组合进行重定向。
如果重定向次数过多,可能会在程序中出现 BUG 。 默认情况下,超过5次重定向会导致异常。可以通过设置 GoRouter
构造器的 redirectLimit
参数来改变重定向最大次数。
另外一个需要考虑的是进入了循环重定向, 例如:/ => /foo => /
。如果发生了循环重定向,会得到一个异常。
示例:重定向和查询参数
有时候,正在进行深度链接,但是希望用户在到达深度链接的位置之前先登录。这种情况下,可以在 redirect
函数中使用 查询参数 。
class App extends StatelessWidget { final loginInfo = LoginInfo(); ... late final _router = GoRouter( routes: ..., // redirect to the login page if the user is not logged in // 如果用户未登录,重定向到登录页面 redirect: (state) { final loggedIn = loginInfo.loggedIn; // check just the subloc in case there are query parameters // 检查子路径 final goingToLogin = state.subloc == '/login'; // the user is not logged in and not headed to /login, they need to login // 如果用户未登录,且不是前往 /login,则需要登录 if (!loggedIn && !goingToLogin) return '/login?from=${state.subloc}'; // the user is logged in and headed to /login, no need to login again // 用户已经登录,且是前往 /login,不需要再登录。 if (loggedIn && goingToLogin) return '/'; // no need to redirect at all // 无需重定向 return null; }, // changes on the listenable will cause the router to refresh it's route // 监听到的变化会导致路由器刷新路由。 refreshListenable: loginInfo, ); } 复制代码
本例中,如果用户未登录,会带着设置给深度链接的 from
查询参数重定向到 /login
。 state
对象可以选择 location
和 subloc
。 location
包含查询参数,subloc
不会包含。因为 /login
路由可能包含查询参数,这种情况下使用 subloc
是最简单的(使用原始的 location
会导致栈溢出,这作为留给读者的一个练习)。
现在,当 /login
路由匹配时,我们想从 state
对象中拉取出 from
参数,并把它传给 LoginScreen
:
GoRoute( path: '/login', builder: (context, state) => // pass the original location to the LoginScreen (if there is one) // 传递原始的 location 给 LoginScreen (如果有) LoginScreen(from: state.queryParams['from'] ), ), 复制代码
在 LoginScreen
界面,如果传递了 from
参数,我们会在登录成功后使用它来跳转到深度链接的位置:
class LoginScreen extends StatelessWidget { final String? from; const LoginScreen({this.from, Key? key}) : super(key: key); @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: Text(_title(context))), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () { // log a user in, letting all the listeners know // 登录了一个用户,通知所有的监听器 context.read<LoginInfo>().login('test-user'); // if there's a deep link, go there // 如果有深度链接的话,跳转过去 if (from != null) context.go(from!); }, child: const Text('Login'), ), ], ), ), ); } 复制代码
当手动重定向时,在 refreshListenable
中传递(信息)是一个不错的方式。 正如此例中所示,确保登录信息的任何改变都能自动触发正确地路由,例如:用户登出时会路由回退到登录页面。
命名式路由
当导航到带位置的路由时,需要在应用中对 URI 构造硬编码,例如:
void _tap(BuildContext context, String fid, String pid) => context.go('/family/$fid/person/$pid'); 复制代码
不只是容易出错,而且随着时间推移,应用中实际的 URI 格式也会发生改变。 固定的重定向会帮助原来的 URI 格式运转,但是你真希望各种版本的位置 URI 随意在代码中吗?
导航到命名式路由
命名式路由的想法是为了在不需要知道或关注 URI 格式的情况下使导航更容易。可以使用 GoRoute.name
参数为路由指定一个唯一的名称:
final _router = GoRouter( routes: [ GoRoute( name: 'home', path: '/', builder: ..., routes: [ GoRoute( name: 'family', path: 'family/:fid', builder: ..., routes: [ GoRoute( name: 'person', path: 'person/:pid', builder: ..., ), ], ), ], ), GoRoute( name: 'login', path: '/login', builder: ..., ), ], 复制代码
你不需要命名所有的路由,但是已命名的那些路由,可以使用名称和所需的参数来导航:
void _tap(BuildContext context, String fid, String pid) => context.go(context.namedLocation('person', params: {'fid': fid, 'pid': pid})); 复制代码
namedLocation
方法会以区分大小写的方式用名称查找路由,然后构造 URI 和填充合适的参数。如果参数缺失或者传递的参数不是路由的部分,会得到一个错误。因为两次解除参照 context
对象不方便, go_router 提供了 goNamed
方法在一个步骤中查找和导航:
void _tap(BuildContext context, String fid, String pid) => context.goNamed('person', params: {'fid': fid, 'pid': pid}); 复制代码
也有一个 pushNamed
方法,用名称来查找路由,在生成的匹配栈中拉取出最顶层的页面并把它推到现有的页面栈中。
重定向到命名式路由
除了导航,可能也想重定向到一个命名式路由,这也可以使用 GoRouter
或 GoRouterState
的 nameddLocation
方法来做到:
// redirect to the login page if the user is not logged in // 用户未登录时重定向到登录页面 redirect: (state) { final loggedIn = loginInfo.loggedIn; // check just the subloc in case there are query parameters // 检查子位置的查询查询 final loginLoc = state.namedLocation('login'); final goingToLogin = state.subloc == loginLoc; // the user is not logged in and not headed to /login, they need to login // 用户未登录,且当前不是前往 /login,则需要登录 if (!loggedIn && !goingToLogin) return state.namedLocation('login', queryParams: {'from': state.subloc}); // the user is logged in and headed to /login, no need to login again // 用户已登录,且当前是前往 /login,则不需要再登录 if (loggedIn && goingToLogin) return state.namedLocation('home'); // no need to redirect at all // 无需重定向 return null; }, 复制代码
本例中,使用了 namedLocation
为命名式路由 'login' 获取位置,然后和当前 的 subloc
比较来发现用户是否忆登录。此外,当为重定向构建位置时,使用 namedLocation
来传递参数用于构建位置。所有做的这些都不需要在代码中对 URI 格式进行硬编码。
URL路径策略
默认情况下,在 Web 应用中,Flutter 会在 URL 中添加 hash (#)。
关闭 Hash
关闭 Hash 的过程是有 记录 的,但很繁琐。 go_router 内置了设置 URL 路径策略的支持,这样,可以在调用 runApp
之前简单地调用 GoRouter.setUrlPathStrategy
来设置你的选择:
void main() { // turn on the # in the URLs on the web (default) // 在 WEB 的 URL 中打开 # (默认) // GoRouter.setUrlPathStrategy(UrlPathStrategy.hash); // turn off the # in the URLs on the web // 在 WEB 的 URL 中关闭 # GoRouter.setUrlPathStrategy(UrlPathStrategy.path); runApp(App()); } 复制代码
设置为 path ,而不是设置为 hash 策略在 URL 中关闭 # :
如果你的路由作为组件的构造部分传递给 runApp
方法,可以通过 GoRouter
构造器的 urlPathStrategy
参数使用快捷方式来设置 URL 路径策略:
// no need to call GoRouter.setUrlPathStrategy() here // 这里不需要调用 GoRouter.setUrlPathStrategy() void main() => runApp(App()); /// sample app using the path URL strategy, i.e. no # in the URL path /// 使用路径 URL 策略的示例应用,即在 URL 路径中没有 # class App extends StatelessWidget { ... final _router = GoRouter( routes: ..., // turn off the # in the URLs on the web // 在 Web 的 URL 中关闭 # urlPathStrategy: UrlPathStrategy.path, ); } 复制代码
配置 Web Server
最终,部署 Flutter Web 应用到 Web 服务器上时,需要配置如 Flutter Web 应用的 index.html
结尾的每个URL,否则 Flutter 无法路由应用的页面。如果在使用 Firebase 的主机,可以配置改写 使所有的 URL 都改写到 index.html
。
如果想在发布前在本地测试一下发布的构建,然后冷重定向到 index.html
的特性,可以使用 flutter run
本身:
$ flutter run -d chrome --release lib/url_strategy.dart 复制代码
注意,需要在 flutter run
能找到 index.html
文件的地方运行该命令。
当然,任何可配置为重定向所有的路径到 index.html
的本地 Web 服务器都能做到,例如:live-server。
调试路由
因为 go_router 会要求提供一个路径的集合,有时作为片段只会匹配位置的一部分, 比较难于知道在应用中有什么路由。这些情况下,作为一个调试工具,能够看到创建的完整的路由路径会很方便,例如:
GoRouter: known full paths for routes: GoRouter: => / GoRouter: => /family/:fid GoRouter: => /family/:fid/person/:pid GoRouter: known full paths for route names: GoRouter: home => / GoRouter: family => /family/:fid GoRouter: person => /family/:fid/person/:pid 复制代码
同样,有多种方式来导航,例如: context.go()
、 context.goNamed()
、 context.push()
、 context.pushNamed()
、 Link
组件等,重定向也一样,所以能看到幕后的处理会很方便,例如:
GoRouter: setting initial location / GoRouter: location changed to / GoRouter: getting location for name: "person", params: {fid: f2, pid: p1} GoRouter: going to /family/f2/person/p1 GoRouter: location changed to /family/f2/person/p1 复制代码
此外,如果使用 builder
代替 pageBuilder
方法在应用中创建界面, go_router 会寻找应用的类型来决定提供给页面哪种转换:
GoRouter: MaterialApp found 复制代码
最后,如果在进行路由时发生了异常,也可以看到调试的输出和调用栈,例:
GoRouter: Exception: no routes for location: /foobarquux ...call stack elided... 复制代码
要使此类输出在 GoRouter
首次创建时可用,可以使用 debugLogDiagnostics
参数:
final _router = GoRouter( routes: ..., // log diagnostic info for your routes debugLogDiagnostics: true, ); 复制代码
这个参数默认是 false ,不进行输出。
示例
You can see go_router in action via the following examples:
可以通过以下示例来看一下 go_router 的运转:
main.dart
: 使用声明式GoRoute
对象的集合定义一个基本的路由方针error_screen.dart
: 定义一个自定义错误界面用于路由错误或 builder 错误init_loc.dart
:启动时默认跳转到一个特定的位置而不是主页(/
)sub_routes.dart
: 基于子路由集合提供页面栈push.dart
: 基于调用context.push()
的序列提供页面栈redirection.dart
: 基于应用状态的改变从一个路由重定向到另一个路由query_params.dart
: 可选的查询参数会传递给所有的 builderrouter_stream_refresh.dart
: 使用Stream
而不是Listenable
来触发刷新named_routes.dart
: 使用名称而不是 URI 进行导航transitions.dart
: 在路由中使用自定义转换async_data.dart
: 异步数据查找nested_nav.dart
: 作为路由位置的一部分,包含关于子页面的信息nav_builder.dart
: 在Navigator
上注入组件url_strategy.dart
: 在 Flutter Web 的 URL 中关闭 #navigator_integration.dart
: 使用Navigator
导航到收集用户输入的页面nav_observer.dart
:通过MaterialPage
的默认变量传递信息给NavigatorObserver
,等state_restoration.dart
: 测试以确保 go_router 可适用于状态存储(它能做到)cupertino.dart
: 测试以确保和 Material 一样,go_router 也适用于 Cupertino 设计语言(它能做到)widgets_app.dart
: 测试以确保和MaterialApp
一样,go_router 也适用于 WidgetsApp (它能做到)router_neglect.dart
: 导航时阻止浏览器创建历史入口books/main.dart
: 使用 go_router 进行 导航和路由 示例的入口
可以在选择的 IDE 中运行这些示例,或者通过下面的命令在 example
文件夹下运行:
$ cd example $ flutter run lib/main.dart 复制代码
Issue
您有关于 go_router 的 issue 或特性请求吗?可以在 issue tracker 上记录。
作者:bettersun
链接:https://juejin.cn/post/7047035390003249189