105 lines
5.5 KiB
Markdown
105 lines
5.5 KiB
Markdown
# 04. 路由方案
|
||
|
||
## 决策
|
||
|
||
使用 **[go_router](https://pub.dev/packages/go_router)**(`^17.5.0`,2026-08 快照,Flutter 官方维护),声明式路由 + 嵌套 `ShellRoute`,不使用 `Navigator 1.0` 命令式 push/pop 作为主路由方式。
|
||
|
||
## 依赖
|
||
|
||
```yaml
|
||
dependencies:
|
||
go_router: ^17.5.0
|
||
```
|
||
|
||
## 路由注册规则
|
||
|
||
- 每个 `feature_*` 包在自己的 `feature_xxx.dart`(对外唯一导出文件)里暴露一个 `List<RouteBase> buildXxxRoutes()` 函数,只声明属于自己的路由,不感知其他 feature。
|
||
- `core_router` 包负责把所有 feature 的路由函数聚合成最终的 `GoRouter` 实例,是唯一知道"全部路由长什么样"的地方。
|
||
- 路径命名统一用 `kebab-case`,前缀按业务域分组,例如 `/store/:storeId`、`/payment/confirm`。
|
||
- 底部导航等常驻 UI 用 `ShellRoute`/`StatefulShellRoute` 包裹对应的 feature 路由,不在每个页面里重复搭一遍导航栏。
|
||
- 登录态校验统一在 `core_router` 聚合层用 `redirect` 实现,不在每个页面里各自判断 token 是否过期。
|
||
- 跨 feature 跳转只能传**可序列化参数**(path 参数、query 参数,或可序列化的 `extra`),不允许把一个 feature 内部的 Dart 类实例通过 `extra` 传给另一个 feature——这是 [01-project-structure.md](./01-project-structure.md) "Feature 间通信" 规则在路由层的具体落地。
|
||
|
||
## 参考链接
|
||
|
||
- [go_router 官方文档](https://pub.dev/packages/go_router)
|
||
- [go_router | Dart package](https://pub.dev/packages/go_router)
|
||
|
||
## 附录:go_router 是什么,日常怎么用
|
||
|
||
给还没接触过声明式路由的同学看的入门说明。
|
||
|
||
### 要解决的问题
|
||
|
||
`Navigator 1.0` 的命令式写法(`Navigator.push(context, MaterialPageRoute(...))`)在页面不多的时候很直观,但规模上来后有几个明显问题:
|
||
|
||
1. **深链接(deep link)/ Web URL 支持差**:命令式 push 本质是"从当前页面跳到下一个页面",很难直接根据一个 URL 字符串恢复出正确的页面栈——比如从推送通知直接打开"门店详情页",命令式写法需要手动拼一串 `push` 调用重建整个栈。
|
||
2. **没有统一的登录拦截点**:每个需要登录态的页面都要自己在 `initState` 里判断要不要跳转到登录页,逻辑散落在各处。
|
||
3. **底部导航这种"多个 tab 各自维护自己的页面栈"的场景很难优雅表达**。
|
||
|
||
**go_router** 是 Flutter 官方团队维护的声明式路由方案:路由表是一份**声明式配置**(一棵 `GoRoute` 树),当前 URL 决定当前应该显示什么页面栈,而不是"一步步 push 出来的"。因为路由是声明式的、和 URL 强绑定,深链接、Web 浏览器前进/后退、登录拦截都能用同一套机制解决。
|
||
|
||
### 核心概念
|
||
|
||
1. **`GoRoute`**:一条路由规则,`path` 是路径模板(支持 `:id` 这种参数),`builder`/`pageBuilder` 返回对应页面。
|
||
2. **`ShellRoute` / `StatefulShellRoute`**:包一层常驻 UI(比如带底部导航栏的外壳),内部嵌套的子路由切换时,外壳本身不重建;`StatefulShellRoute` 还能让每个 tab 各自保留自己的页面栈(切 tab 不丢失之前的浏览位置)。
|
||
3. **`GoRouterState`**:在 `builder` 里能拿到当前路由的 path 参数(`state.pathParameters`)、query 参数(`state.uri.queryParameters`)、`extra` 对象。
|
||
4. **`redirect`**:每次路由变化前会先跑一遍 `redirect` 回调,返回非空字符串就强制跳转——这是实现"未登录访问需要登录的页面 → 自动跳登录页"的地方。
|
||
5. **`context.go()` / `context.push()`**:`go` 是替换当前路由(浏览器前进后退语义),`push` 是在当前栈上叠加一层(可以 `pop` 回去)——日常最容易混淆的两个 API,选错会导致返回键行为不符合预期。
|
||
|
||
### 使用示例(底部导航 + 门店详情页 + 登录拦截)
|
||
|
||
```dart
|
||
// packages/core_router/lib/src/app_router.dart
|
||
final goRouterProvider = Provider<GoRouter>((ref) {
|
||
final authState = ref.watch(authStateProvider);
|
||
|
||
return GoRouter(
|
||
initialLocation: '/store',
|
||
redirect: (context, state) {
|
||
final loggingIn = state.matchedLocation == '/login';
|
||
if (!authState.isLoggedIn && !loggingIn) return '/login';
|
||
if (authState.isLoggedIn && loggingIn) return '/store';
|
||
return null; // 不需要重定向
|
||
},
|
||
routes: [
|
||
GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
|
||
StatefulShellRoute.indexedStack(
|
||
builder: (context, state, navigationShell) =>
|
||
MainShell(navigationShell: navigationShell),
|
||
branches: [
|
||
StatefulShellBranch(routes: buildStoreRoutes()),
|
||
StatefulShellBranch(routes: buildPaymentRoutes()),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
});
|
||
```
|
||
|
||
```dart
|
||
// packages/feature_store/lib/feature_store.dart
|
||
List<RouteBase> buildStoreRoutes() => [
|
||
GoRoute(
|
||
path: '/store',
|
||
builder: (context, state) => const StoreListPage(),
|
||
routes: [
|
||
GoRoute(
|
||
path: ':storeId', // 完整路径 /store/:storeId
|
||
builder: (context, state) {
|
||
final storeId = state.pathParameters['storeId']!;
|
||
return StoreDetailPage(storeId: storeId);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
];
|
||
```
|
||
|
||
```dart
|
||
// 从任意页面跳转到门店详情
|
||
context.push('/store/${store.id}');
|
||
```
|
||
|
||
`buildStoreRoutes()` 只在 `feature_store` 包内声明,`app_router.dart` 里只 import 这个函数、不 import `feature_store` 的任何页面 widget 类型——保持 [01-project-structure.md](./01-project-structure.md) 的编译期边界。
|