目录

  1. 引言:为什么路由是应用的“中枢神经”?

  2. 基础导航:Navigator 的命令式调用

  3. 核心实践:类型安全的参数传递

  4. 架构演进:解耦的艺术——构建 Router 服务

  5. Router 服务的实现:从定义到执行

  6. 高级场景(一):导航结果的回调

  7. 高级场景(二):路由守卫(如:登录拦截)

  8. 高级场景(三):深度链接(Deeplinking)

  9. 总结:从“跳转”到“管理”的思维转变


一、引言:为什么路由是应用的“中枢神经”?

在鸿蒙应用开发中,路由导航(Routing & Navigation)是连接所有独立页面的“中枢神经系统”。它不仅决定了用户如何从页面A跳转到页面B,更是一个应用架构设计中至关重要的环节。

一个糟糕的路由管理方案,会将你的应用变成一张错综复杂的“蜘蛛网”:

  • 高度耦合HomePage 需要知道 ProductDetailPage 的具体存在,ProductDetailPage 又需要知道 CheckoutPage。任何页面的重命名或参数修改,都可能导致多处代码崩溃。

  • 逻辑臃肿:每个跳转点都需要处理“是否登录?”、“权限是否足够?”等逻辑,导致大量重复代码。

  • 难以维护:无法清晰地了解整个应用的页面流向,难以添加或修改功能。

仓颉语言结合鸿蒙的UI框架,提供了一套强大的导航机制。本文将从最基础的页面跳转开始,一步步演进,最终构建一个高内聚、低耦合、可测试、可扩展的中央路由管理系统。

二、基础导航:Navigator 的命令式调用

仓颉提供了 Navigator 对象,允许我们以命令式的方式执行r` 对象,允许我们以命令式的方式执行页面跳转。这是最基础、最直接的导航方式。

@Component
struct PageA {
    func build() -> View {
        Column(spacing: 20.0) {
            Text("这是页面 A")
            
            Button("跳转到页面 B") {
                // 1. 推入 (Push)
                // 创建 PageB 实例并推入导航栈
                Navigator.push(target: PageB())
            }
            
            Button("跳转并替换当前页") {
                // 2. 替换 (Replace)
                // PageA 将被销毁,PageC 成为栈顶
                Navigator.replace(target: PageC())
            }
        }
    }
}

@Component
struct PageB {
    func build() -> View {
        Column(spacing: 20.0) {
            Text("这是页面 B")
            
            Button("返回页面 A") {
                // 3. 弹出 (Pop)
                Navigator.pop()
            }
            
            Button("返回首页") {
                // 4. 弹回根页面 (Pop to Root)
                Navigator.popToRoot()
            }
        }
    }
}

@Component
struct PageC { ... }

这种方式简单直接,但正如引言所说,它导致了 PageAPageB 的直接耦合。

三、核心实践:类型安全的参数传递

在实际业务中,跳转页面几乎总是需要传递数据(如商品ID、用户ID)。仓颉的优势在于其类型安全。我们通过目标页面的构造函数 init@Prop 来实现参数传递。

@Component
struct ProductListPage {
    @State var products: Array<Product> = [
        Product(id: "p-001", name: "仓颉T恤"),
        Product(id: "p-002", name: "鸿蒙笔记本")
    ]
    
    func build() -> View {
        List {
            for (product in this.products) {
                Text(product.name)
                    .onClick {
                        // 2. 类型安全地传递参数
                        Navigator.push(target: ProductDetailPage(
                            productId: product.id,
                            source: "List"
                        ))
                    }
            }
        }
    }
}

@Component
struct ProductDetailPage {
    // 1. 使用 @Prop 接收参数
    @Prop var productId: String
    @Prop var source: String? // 可选参数
    
    @State var productInfo: ProductInfo? = None
    
    func onMount() {
        // 3. 使用参数加载数据
        this.loadData(this.productId)
    }
    
    func build() -> View {
        // ... 页面布局 ...
        Text("商品ID: ${this.productId}")
        Text("来源: ${this.source ?? "未知"}")
    }
    
    func loadData(id: String) {
        // ...
    }
}

优点:编译时检查。如果你忘记传递 productId,或者传递了错误的类型(如 Int32),编译器会直接报错。
缺点ProductListPage 依然“知道” ProductDetailPage 的存在,耦合问题未解决。

四、架构演进:解耦的艺术——构建 Router 服务

为了解决耦合问题,我们引入**中央路由服务outer Service)
其核心思想是:**所有组件只与 Router 通信,Router 是唯一知道“如何”从路由路径映射到具体 View 组件的地方。

[Image of Router Architecture Diagram]
*(示意图:ViewA -> Router.navigate(.roductDetail("123")) -> Router Service -> (builds) ProductDetailPage -> Navigator.push(target))*

五、Router 服务的实现:从定义到执行

5.1 步骤一:定义抽象路由 AppRoute

我们使用 enum 来定义应用中所有可能的、类型安全的路由路径。

// AppRoute.cangjie

// 使用枚举定义所有可达的页面
enum AppRoute {
    // 简单页面
    case Home
    case Settings
    case Login
    
    // 带参数的页面
    case ProductDetail(productId: String)
    case UserProfile(userId: String)
    
    // 带回调的页面
    case PhotoPicker(onSelect: (Photo) -> Unit)
    
    // (可选) 增加路由元信息,用于权限控制
    func requiresAuth() -> Bool {
        switch (this) {
            case .UserProfile, .Settings => true
            default => false
        }
    }
}

5.2 步骤二:创建 RouterService

这个 class 是我们路由系统的核心,它负责解析 AppRoute 并执行真正的跳转。

// RouterService.cangjie

@Observable
class RouterService {

    // 依赖注入(示例,后续用于权限控制)
    @Consume("authService") var authService: AuthService
    
    // 核心方法:构建视图
    // 使用 @ViewBuilder (或类似能力) 确保返回的是 View
    private func buildView(for route: AppRoute) -> View {
        // 路由映射表
        switch (route) {
            case .Home => HomePage()
            case .Settings => SettingsPage()
            case .Login => LoginPage()
            
            case .ProductDetail(let id) =>
                ProductDetailPage(productId: id)
            
            case .UserProfile(let id) =>
                UserProfilePage(userId: id)
            
            case .PhotoPicker(let onSelect) =>
                PhotoPickerPage(onPhotoSelected: onSelect)
        }
    }
    
    // 公共 API:推入 (Push)
    func push(to route: AppRoute) {
        // 在这里可以插入通用逻辑,如权限检查、日志记录
        Logger.info("Navigating to ${route}")
        
        // 演示:路由守卫
        if (route.requiresAuth() && !this.authService.isLoggedIn) {
            Logger.warn("Route ${route} requires auth. Redirecting to Login.")
            
            // 拦截并重定向到登录页
            Navigator.push(target: LoginPage(onLoginSuccess: {
                // 登录成功后,自动跳转到原目标页面
                Logger.info("Login success, redirecting to original target.")
                Navigator.replace(target: this.buildView(for: route))
            }))
            
            return
        }
        
        // 执行跳转
        Navigator.push(target: this.buildView(for: route))
    }
    
    // 公共 API:替换 (Replace)
    func replace(with route: AppRoute) {
        Navigator.replace(target: this.buildView(for: route))
    }
    
    // 公共 API:返回
    func pop() {
        Navigator.pop()
    }
    
    // 公共 API:返回首页
    func popToRoot() {
        Navigator.popToRoot()
    }
}

5.3 步骤三:注入和使用 RouterService

在应用根节点提供 RouterService 实例,然后在任何子组件中消费它。

// AppRoot.cangjie (应用根)
@Component
struct AppRoot {
    // 实例化并提供 RouterService
    @Provide("router") var router: RouterService = RouterService()
    @Provide("authService") var auth: AuthService = AuthService() // 假设已有
    
    func build() -> View {
        // ...
        // 假设使用 NavigationStack 或类似容器
        NavigationStack {
            HomePage()
        }
    }
}

// ProductListPage.cangjie (使用方)
@Component
struct ProductListPage {
    @State var products: Array<Product> = [...]
    
    // 消费 RouterService
    @Consume("router") var router: RouterService
    
    func build() -> View {
        List {
            for (product in this.products) {
                Text(product.name)
                    .onClick {
                        // 4. 调用 Router,完全解耦!
                        this.router.push(to: .ProductDetail(productId: product.id))
                    }
            }
        }
    }
}

架构优势

  1. 解耦ProductListPage 不再知道 ProductDetailPage 的存在,它只知道 AppRoute.ProductDetail 这个“意图”。

  2. **集中**:所有路由映射都在 RouterServicebuildView 方法中,修改路由(如 ProductDetailPage -> `NewProductDetailPage)只需改一处。

  3. AOP(面向切面):日志、权限认证等逻辑可以集中添加到 `push 方法中,应用于所有导航。

六、高级场景(一):导航结果的回调

PageA 需要从 `PageB 获取一个结果(如选择一张照片、一个地址)。

我们已经在 AppRoute 中定义了 `PhotoPicker(onSelect: (Photo) -> Unit)

// PageA: 发起跳转
@Component
struct ProfileSettingsPage {
    @Consume("router") var router: RouterService
    @State var avatar: Photo? = None
    
    func build() -> View {
        Column {
            Image(src: this.avatar?.url ?? "default_avatar.png")
            
            Button("更换头像") {
                this.router.push(to: .PhotoPicker(onSelect: { selectedPhoto =>
                    // 1. 闭包回调被执行
                    this.avatar = selectedPhoto
                    Logger.info("Avatar updated!")
                }))
            }
        }
    }
}

// PageB: 执行回调
@Component
struct PhotoPickerPage {
    @Prop var onPhotoSelected: (Photo) -> Unit // 接收回调
    @State var photos: Array<Photo> = [...]
    
    func build() -> View {
        GridView(items: this.photos) { photo =>
            Image(src: photo.url)
                .onClick {
                    // 2. 执行回调
                    this.onPhotoSelected(photo)
                    // 3. 关闭当前页
                    Navigator.pop()
                }
        }
    }
}

这种基于闭包的回调方式是类型安全的,并且逻辑清晰,优于传统的事件总线或代理模式。

七、高级场景(二):路由守卫(如:登录拦截)

我们已经在 `RouterServicepush` 方法中实现了这个功能。

// RouterService.cangjie
func push(to route: AppRoute) {
    // 检查路由元信息
    if (route.requiresAuth() && !this.authService.isLoggedIn) {
        // 拦截并重定向
        Navigator.push(target: LoginPage(onLoginSuccess: {
            // 登录成功后,自动跳转到原目标页面
            Navigator.replace(target: this.buildView(for: route))
        }))
        return // 终止原导航
    }
    
    // ... 正常导航 ...
}

现在,SettingsPageUserProfilePage 组件自身完全不需要关心登录状态,它们可以假设自己被打开时一定是已登录的。登录逻辑被集中到了 Router

八、高级场景(三):深度链接(Deeplinking)

当用户点击一个URL (如 myapp://products/p-001) 打开应用时,RouterService 也是完美的处理者。

// AppRoot.cangjie
@Component
struct AppRoot {
    @Provide("router") var router: RouterService = RouterService()
    // ...
    
    func onMount() {
        // 1. 监听系统传递过来的 URL
        SystemEvents.onDeepLinkReceived { url =>
            this.handleDeepLink(url)
        }
    }
    
    func handleDeepLink(url: URL) {
        // 2. 解析 URL (伪代码)
        // "myapp://products/p-001"
        
        let path = url.path
        if (path.startsWith("/products/")) {
            let id = path.split("/")[2]
            // 3. 转换为 AppRoute
            let route = AppRoute.ProductDetail(productId: id)
            // 4. 使用 Router 执行跳转
            this.router.push(to: route)
            
        } else if (path == "/settings") {
            this.router.push(to: .Settings)
        }
    }
    
    // ...
}

九、总结:从“跳转”到“管理”的思维转变

路由管理是应用架构的基石。在仓颉中,我们:

  1. 始于 `Navigator.push/op` 的基础命令。

  2. 利用 @Prop 构造函数实现类型安全的参数传递。

  3. 演进到使用 enum AppRouteRouterService 构建中央路由系统。

  4. 实现了视图解耦、集中式权限控制、类型安全回调和深度链接处理。

通过将零散的“页面跳转”操作,收归到统一的“路由管理”服务,我们构建了一个可维护、可测试、可扩展的仓颉应用架构。这正是从“初学者”到“架构师”的关键思维转变。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐