仓颉架构实战:构建可控、可扩展的路由导航系统
目录
-
引言:为什么路由是应用的“中枢神经”?
-
基础导航:
Navigator的命令式调用 -
核心实践:类型安全的参数传递
-
架构演进:解耦的艺术——构建
Router服务 -
Router服务的实现:从定义到执行 -
高级场景(一):导航结果的回调
-
高级场景(二):路由守卫(如:登录拦截)
-
高级场景(三):深度链接(Deeplinking)
-
总结:从“跳转”到“管理”的思维转变
一、引言:为什么路由是应用的“中枢神经”?
在鸿蒙应用开发中,路由导航(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 { ... }
这种方式简单直接,但正如引言所说,它导致了 PageA 和 PageB 的直接耦合。
三、核心实践:类型安全的参数传递
在实际业务中,跳转页面几乎总是需要传递数据(如商品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))
}
}
}
}
}
架构优势:
-
解耦:
ProductListPage不再知道ProductDetailPage的存在,它只知道AppRoute.ProductDetail这个“意图”。 -
**集中**:所有路由映射都在
RouterService的buildView方法中,修改路由(如ProductDetailPage-> `NewProductDetailPage)只需改一处。 -
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 // 终止原导航
}
// ... 正常导航 ...
}
现在,SettingsPage 或 UserProfilePage 组件自身完全不需要关心登录状态,它们可以假设自己被打开时一定是已登录的。登录逻辑被集中到了 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)
}
}
// ...
}
九、总结:从“跳转”到“管理”的思维转变
路由管理是应用架构的基石。在仓颉中,我们:
-
始于 `Navigator.push/op` 的基础命令。
-
利用
@Prop构造函数实现类型安全的参数传递。 -
演进到使用
enum AppRoute和RouterService构建中央路由系统。 -
实现了视图解耦、集中式权限控制、类型安全回调和深度链接处理。
通过将零散的“页面跳转”操作,收归到统一的“路由管理”服务,我们构建了一个可维护、可测试、可扩展的仓颉应用架构。这正是从“初学者”到“架构师”的关键思维转变。
更多推荐
所有评论(0)