路由守卫与登录拦截:HarmonyOS 里的"门卫"怎么当

做移动端开发,路由守卫这事绕不开。Web 前端有 vue-router 的 beforeEach,Android 有 Activity 的 onNewIntent 拦截,到了 HarmonyOS呢?Navigation + NavPathStack 这套体系给你留了几个口子,但不像 Web 端那么现成,得自己拼。今天就把这套"门卫体系"从零到一捋清楚。

一、路由前置守卫:pushUrl 前检查登录态

最朴素的想法:每次跳页面前,先看看用户登没登录,没登录就拦住。这个逻辑放在哪?最直接的做法是封装一个方法,所有路由跳转都走这个口子。

import { router } from '@kit.ArkUI';

export class RouteGuard {
  private static isLoggedIn(): boolean {
    let token = AppStorage.get<string>('userToken');
    return token !== undefined && token.length > 0;
  }

  public static pushWithAuth(url: string, params?: Record<string, string>): void {
    if (!RouteGuard.isLoggedIn()) {
      AppStorage.setOrCreate('pendingRoute', url);
      if (params !== undefined) {
        AppStorage.setOrCreate('pendingParams', JSON.stringify(params));
      }
      router.pushUrl({ url: 'pages/LoginPage' });
      return;
    }
    let options: Record<string, string> = params !== undefined ? params : {};
    router.pushUrl({ url: url, params: options });
  }
}

调用时一行搞定:

RouteGuard.pushWithAuth('pages/ProfilePage');

关键是:团队里所有人必须走这个方法跳转,谁要是直接 router.pushUrl 绕过去了,守卫形同虚设。这个靠规范约束,不靠技术强制。后面说的中间件模式能缓解这个问题。

二、目标页暂存模式:未登录记下 target,登录后自动跳

光拦住不够。用户本来想看"我的"页面,被拦到登录页,登录完了就停在那?那体验太差了。正确做法是:记住用户本来要去哪,登录成功后自动跳过去

思路很简单,用 AppStorage 存一个 pendingRoute,登录成功后检查这个值,有就跳,没有就回首页。

// LoginPage.ets
@Entry
@Component
struct LoginPage {
  @State inputPhone: string = '';
  @StorageLink('pendingRoute') pendingRoute: string = '';
  @StorageLink('pendingParams') pendingParams: string = '';

  build() {
    Column() {
      TextInput({ placeholder: '请输入手机号' })
        .onChange((value: string) => {
          this.inputPhone = value;
        })
      Button('登录')
        .onClick(() => {
          this.doLogin();
        })
    }
  }

  private doLogin(): void {
    // 模拟登录成功,存储 token
    AppStorage.setOrCreate('userToken', 'mock_token_123');
    // 检查是否有暂存的目标页
    if (this.pendingRoute.length > 0) {
      let targetUrl: string = this.pendingRoute;
      let paramStr: string = this.pendingParams;
      // 清除暂存
      AppStorage.setOrCreate('pendingRoute', '');
      AppStorage.setOrCreate('pendingParams', '');
      // 跳转到目标页
      if (paramStr.length > 0) {
        let params: Record<string, string> = JSON.parse(paramStr) as Record<string, string>;
        router.pushUrl({ url: targetUrl, params: params });
      } else {
        router.pushUrl({ url: targetUrl });
      }
    } else {
      router.replaceUrl({ url: 'pages/Index' });
    }
  }
}

这里有个细节:用完 pendingRoute 之后一定要清掉,不然下次登录还会跳到老页面。我见过有人忘了清,导致用户退出登录再重新登录时被跳到一个莫名其妙的页面,排查半天。

另一个细节是参数也要暂存。有些页面是要带参数跳转的,比如 pages/OrderDetail?id=123,光记 URL 不记参数,跳过去就是空白页。

三、onBackPress 拦截:返回时检查未保存数据

返回拦截和登录拦截是两码事,但都属于路由守卫的范畴。常见场景:表单编辑页,用户填了一半点返回,你不得问一句"确定不保存就走?"

@Entry 页面里,用 onBackPress 生命周期:

@Entry
@Component
struct EditPage {
  @State hasUnsavedData: boolean = false;
  @State draftContent: string = '';

  build() {
    Column() {
      TextArea({ placeholder: '请输入内容' })
        .onChange((value: string) => {
          this.draftContent = value;
          this.hasUnsavedData = value.length > 0;
        })
    }
  }

  onBackPress(): boolean {
    if (!this.hasUnsavedData) {
      return false; // 没有未保存数据,正常返回
    }
    // 有未保存数据,弹确认框
    this.getUIContext().getPromptAction().showDialog({
      message: '有未保存的内容,确定要离开吗?',
      buttons: [
        { text: '留下编辑', color: '#007DFF' },
        { text: '不保存离开', color: '#FF0000' }
      ]
    }).then((data: ShowDialogSuccessResponse) => {
      if (data.index === 1) {
        this.hasUnsavedData = false;
        router.back();
      }
    });
    return true; // 拦截返回事件
  }
}

onBackPress 返回 true 表示消费了返回事件,系统不再执行默认返回逻辑;返回 false 则放行。这个设计跟 Android 的 onBackPressed 是一个路子。

如果你用的是 Navigation + NavDestination 体系,那拦截方式不一样,用 NavDestination 的 onBackPressed

@Component
struct EditDestination {
  @State hasUnsavedData: boolean = false;
  pathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Column() {
        TextArea({ placeholder: '输入内容' })
          .onChange((value: string) => {
            this.hasUnsavedData = value.length > 0;
          })
      }
    }
    .onBackPressed(() => {
      if (this.hasUnsavedData) {
        // 弹窗确认逻辑
        return true; // 拦截
      }
      return false; // 放行
    })
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

注意区分:onBackPress@Entry 页面的,onBackPressed 是 NavDestination 的,别搞混了。我见过有人在 NavDestination 里写 onBackPress,怎么都不生效,查了半天才发现用错 API。

还有一个坑:onBackPressed 能拦截系统侧滑和标题栏返回按钮,但拦不住 pathStack.pop() 的代码调用。如果团队里有人直接调 pop() 绕过了,那拦截就没用了。所以弹窗确认之后的"离开"操作,推荐用 pathStack.pop() 执行,而不是靠系统默认行为。

四、登录态持久化:AppStorage + PersistentStorage

上面几节都依赖"判断是否登录"这个动作,那登录态本身怎么存?

AppStorage 是内存级的,应用一杀就没了。PersistentStorage 才是磁盘持久化的。正确做法是:PersistentStorage 作为源头,AppStorage 作为运行时快照

// EntryAbility.ets 的 onWindowStageCreate 中初始化
onWindowStageCreate(windowStage: window.WindowStage): void {
  // 声明持久化 key,如果磁盘有值就同步到 AppStorage,没有就用默认值
  PersistentStorage.persistProp('userToken', '');
  PersistentStorage.persistProp('userRole', '');

  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) {
      hilog.error(0x0000, 'EntryAbility', 'loadContent failed');
      return;
    }
  });
}

之后在代码里直接用 AppStorage.get@StorageLink 读写就行,PersistentStorage 会自动同步:

// 登录成功后
AppStorage.setOrCreate('userToken', tokenValue);
AppStorage.setOrCreate('userRole', 'admin');

// 退出登录
AppStorage.setOrCreate('userToken', '');
AppStorage.setOrCreate('userRole', '');

有个大坑要提:别用 PersistentStorage.deleteProp 来清除登录态。这方法会把 key 从持久化存储里删掉,但 AppStorage 里的值可能还残留。正确做法是设为空字符串或默认值,保持 key 存在。

另外,敏感信息(比如 token)存 PersistentStorage 是明文存磁盘的,安全性一般。生产环境建议用 @ohos.data.preferences 配合加密,或者把 token 存 HUKS 管理的密钥区。不过对大部分应用来说,PersistentStorage 够用,先跑通再说。

五、权限路由表:不同角色看到的页面不一样

B 端应用常见需求:管理员能看设置页,普通员工看不到。这就需要一份"权限路由表"。

定义一个路由表结构:

export interface RouteConfig {
  name: string;
  path: string;
  requiredRole: string; // 'all' 表示不限制
}

export class RouteTable {
  private static routes: Array<RouteConfig> = [
    { name: '首页', path: 'pages/Index', requiredRole: 'all' },
    { name: '个人中心', path: 'pages/Profile', requiredRole: 'all' },
    { name: '订单列表', path: 'pages/OrderList', requiredRole: 'user' },
    { name: '管理后台', path: 'pages/Admin', requiredRole: 'admin' },
    { name: '数据统计', path: 'pages/Statistics', requiredRole: 'admin' }
  ];

  public static canAccess(pagePath: string): boolean {
    let userRole = AppStorage.get<string>('userRole');
    if (userRole === undefined || userRole.length === 0) {
      return false;
    }
    for (let i = 0; i < RouteTable.routes.length; i++) {
      let route = RouteTable.routes[i];
      if (route.path === pagePath) {
        if (route.requiredRole === 'all') {
          return true;
        }
        return userRole === route.requiredRole;
      }
    }
    return false;
  }

  public static getAccessibleRoutes(): Array<RouteConfig> {
    let userRole = AppStorage.get<string>('userRole');
    if (userRole === undefined || userRole.length === 0) {
      return [];
    }
    let result: Array<RouteConfig> = [];
    for (let i = 0; i < RouteTable.routes.length; i++) {
      let route = RouteTable.routes[i];
      if (route.requiredRole === 'all' || userRole === route.requiredRole) {
        result.push(route);
      }
    }
    return result;
  }
}

在导航或菜单渲染时,用 getAccessibleRoutes() 过滤可见项。跳转时用 canAccess() 做二次校验:

if (RouteTable.canAccess('pages/Admin')) {
  RouteGuard.pushWithAuth('pages/Admin');
} else {
  this.getUIContext().getPromptAction().showToast({ message: '无权访问该页面' });
}

这个方案是客户端侧的前端权限控制,不能替代后端鉴权。服务端接口该验 token 验 token,该验角色验角色,前端路由表只是优化体验,避免用户看到一个没权限的页面再被接口弹回来。

六、NavPathStack 守卫:setInterception 回调拦截

如果你用的是 Navigation 体系(HarmonyOS NEXT 主推),NavPathStack 提供了 setInterception 方法,可以在路由切换前拦截。这是最接近 vue-router beforeEach 的能力。

@Entry
@Component
struct MainPage {
  pathStack: NavPathStack = new NavPathStack();

  aboutToAppear(): void {
    this.pathStack.setInterception({
      willShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
        operation: NavigationOperation, isAnimated: boolean) => {
        // 从 to 获取目标页名称
        let targetName = '';
        if (to instanceof NavDestinationContext) {
          targetName = to.pathInfo.name;
        }
        // 检查是否需要登录
        if (this.needAuth(targetName)) {
          let token = AppStorage.get<string>('userToken');
          if (token === undefined || token.length === 0) {
            AppStorage.setOrCreate('pendingNavName', targetName);
            this.pathStack.pushPathByName('LoginPage', null, false);
            return; // 拦截原跳转
          }
        }
      },
      didShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
        operation: NavigationOperation, isAnimated: boolean) => {
        // 页面已显示后的回调,可用于埋点
      }
    });
  }

  private needAuth(pageName: string): boolean {
    // 定义需要登录的页面列表
    let authPages: Array<string> = ['ProfilePage', 'OrderPage', 'AdminPage'];
    for (let i = 0; i < authPages.length; i++) {
      if (authPages[i] === pageName) {
        return true;
      }
    }
    return false;
  }

  build() {
    Navigation(this.pathStack) {
      Column() {
        Text('首页内容')
      }
    }
    .navDestination(this.pageMap);
  }

  @Builder
  pageMap(name: string) {
    if (name === 'LoginPage') {
      LoginPage();
    } else if (name === 'ProfilePage') {
      ProfilePage();
    }
  }
}

setInterception 有三个回调:

  • willShow:页面即将显示前触发,可以做拦截
  • didShow:页面已显示后触发
  • 实际开发中最常用的是 willShow,配合 needAuth 判断逻辑

注意一点:setInterception 只能拦截用户操作触发的导航(侧滑、系统返回),代码里直接调用 pushPathByName 也会触发 willShow,但在 willShow 里再调用 pushPathByName 跳登录页时要注意避免循环拦截。登录页本身不能被 needAuth 判定为需要登录,否则就死循环了。

七、深链拦截:从外部跳转进来时的权限检查

HarmonyOS 支持 App Linking 和 Deep Link,用户可以从浏览器、短信等外部场景直接跳到你应用的某个页面。这种场景下的权限检查很多人漏掉了。

深链的入口在 UIAbility 的 onNewWantonCreate 中:

// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleDeepLink(want);
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleDeepLink(want);
  }

  private handleDeepLink(want: Want): void {
    let uri = want.parameters !== undefined ? want.parameters['uri'] : undefined;
    if (uri === undefined || typeof uri !== 'string') {
      return;
    }
    // 解析深链目标
    let targetPage = this.parseDeepLink(uri);
    // 检查目标页是否需要登录
    let token = AppStorage.get<string>('userToken');
    if (this.isProtectedPage(targetPage) && (token === undefined || token.length === 0)) {
      // 未登录,暂存目标,登录后跳转
      AppStorage.setOrCreate('deepLinkTarget', targetPage);
      AppStorage.setOrCreate('deepLinkUri', uri);
    } else {
      // 已登录或无需登录,直接跳转
      AppStorage.setOrCreate('deepLinkTarget', targetPage);
    }
  }

  private parseDeepLink(uri: string): string {
    // 简单解析,实际项目按业务规则处理
    if (uri.indexOf('/order/') >= 0) {
      return 'OrderDetailPage';
    }
    if (uri.indexOf('/profile') >= 0) {
      return 'ProfilePage';
    }
    return 'Index';
  }

  private isProtectedPage(pageName: string): boolean {
    let protectedPages: Array<string> = ['OrderDetailPage', 'ProfilePage', 'AdminPage'];
    for (let i = 0; i < protectedPages.length; i++) {
      if (protectedPages[i] === pageName) {
        return true;
      }
    }
    return false;
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    PersistentStorage.persistProp('userToken', '');
    PersistentStorage.persistProp('userRole', '');
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'EntryAbility', 'loadContent failed');
      }
    });
  }
}

深链拦截的要点:

  1. onCreate 是应用冷启动时触发,onNewWant 是应用已存在时再次被拉起触发,两个都要处理
  2. 深链跳转进来时,页面可能还没加载完,AppStorage 的值可能还没从 PersistentStorage 同步过来,所以初始化顺序很重要——persistProp 要在 loadContent 之前调用
  3. 深链带来的 URI 可能包含敏感参数,解析时要过滤和校验,别把原始 URI 直接当路由路径用了

八、路由中间件模式:封装 checkAndNavigate 通用方法

前面说的方法各有各的调用方式,维护起来比较散。更工程化的做法是封装一个"路由中间件",所有跳转统一走一个入口,中间件链式执行检查逻辑。

export interface MiddlewareResult {
  allowed: boolean;
  redirectUrl: string;
}

export interface RouteMiddleware {
  name: string;
  check(targetUrl: string, params: Record<string, string>): MiddlewareResult;
}

export class RouteMiddlewareChain {
  private static middlewares: Array<RouteMiddleware> = [];
  private static targetPage: string = '';
  private static targetParams: Record<string, string> = new Record<string, string>();

  public static register(middleware: RouteMiddleware): void {
    RouteMiddlewareChain.middlewares.push(middleware);
  }

  public static checkAndNavigate(url: string, params?: Record<string, string>): void {
    let currentParams: Record<string, string> = params !== undefined ? params : new Record<string, string>();
    // 依次执行中间件
    for (let i = 0; i < RouteMiddlewareChain.middlewares.length; i++) {
      let middleware = RouteMiddlewareChain.middlewares[i];
      let result = middleware.check(url, currentParams);
      if (!result.allowed) {
        // 被拦截,跳转重定向页
        if (result.redirectUrl.length > 0) {
          AppStorage.setOrCreate('pendingRoute', url);
          router.pushUrl({ url: result.redirectUrl });
        }
        return;
      }
    }
    // 全部通过,执行跳转
    router.pushUrl({ url: url, params: currentParams });
  }
}

然后定义具体的中间件:

// 登录检查中间件
export class AuthMiddleware implements RouteMiddleware {
  name: string = 'AuthMiddleware';
  private protectedPages: Array<string> = ['ProfilePage', 'OrderPage', 'SettingsPage'];

  check(targetUrl: string, params: Record<string, string>): MiddlewareResult {
    let isProtected = false;
    for (let i = 0; i < this.protectedPages.length; i++) {
      if (targetUrl.indexOf(this.protectedPages[i]) >= 0) {
        isProtected = true;
        break;
      }
    }
    if (!isProtected) {
      return { allowed: true, redirectUrl: '' };
    }
    let token = AppStorage.get<string>('userToken');
    if (token !== undefined && token.length > 0) {
      return { allowed: true, redirectUrl: '' };
    }
    return { allowed: false, redirectUrl: 'pages/LoginPage' };
  }
}

// 角色权限中间件
export class RoleMiddleware implements RouteMiddleware {
  name: string = 'RoleMiddleware';
  private adminPages: Array<string> = ['AdminPage', 'StatisticsPage'];

  check(targetUrl: string, params: Record<string, string>): MiddlewareResult {
    let needAdmin = false;
    for (let i = 0; i < this.adminPages.length; i++) {
      if (targetUrl.indexOf(this.adminPages[i]) >= 0) {
        needAdmin = true;
        break;
      }
    }
    if (!needAdmin) {
      return { allowed: true, redirectUrl: '' };
    }
    let role = AppStorage.get<string>('userRole');
    if (role === 'admin') {
      return { allowed: true, redirectUrl: '' };
    }
    return { allowed: false, redirectUrl: 'pages/NoPermissionPage' };
  }
}

注册中间件,在应用启动时执行一次:

// 应用初始化时
RouteMiddlewareChain.register(new AuthMiddleware());
RouteMiddlewareChain.register(new RoleMiddleware());

之后所有路由跳转用一行:

RouteMiddlewareChain.checkAndNavigate('pages/AdminPage');

中间件模式的好处是扩展方便。以后要加"VIP 检查"、“版本检查”、“地区限制”,只要写一个新的 Middleware 注册进去就行,不用改已有代码。符合开闭原则。

但也别过度设计。如果你的应用总共就三四个页面、一种角色,搞一整套中间件链纯属折腾。根据项目规模选方案,够用就行。

九、页面级鉴权:aboutToAppear 中检查权限

上面说的都是"跳转前拦截",但有时候你在页面内部也需要做鉴权——比如页面已经打开了,但发现权限不对,需要主动退出。

这种场景用 aboutToAppear 生命周期:

@Component
struct AdminPage {
  @StorageLink('userRole') userRole: string = '';
  pathStack: NavPathStack = new NavPathStack();

  aboutToAppear(): void {
    if (this.userRole !== 'admin') {
      // 无权限,弹提示后跳走
      this.getUIContext().getPromptAction().showToast({ message: '无管理员权限' });
      this.pathStack.pop();
    }
  }

  build() {
    NavDestination() {
      Column() {
        Text('管理后台')
          .fontSize(24)
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

页面级鉴权是最后一道防线。即使前置拦截有漏洞(比如有人绕过了中间件直接 pushPathByName),页面内部的 aboutToAppear 还能兜底。

但有个问题:用户会看到页面"闪一下"——页面先渲染出来,然后 aboutToAppear 里发现没权限又跳走。体验不好。优化方案是加个 loading 态:

@Component
struct AdminPage {
  @StorageLink('userRole') userRole: string = '';
  @State isChecking: boolean = true;
  @State hasPermission: boolean = false;
  pathStack: NavPathStack = new NavPathStack();

  aboutToAppear(): void {
    this.hasPermission = this.userRole === 'admin';
    this.isChecking = false;
    if (!this.hasPermission) {
      this.getUIContext().getPromptAction().showToast({ message: '无管理员权限' });
      this.pathStack.pop();
    }
  }

  build() {
    NavDestination() {
      if (this.isChecking) {
        LoadingProgress()
      } else if (this.hasPermission) {
        Column() {
          Text('管理后台')
            .fontSize(24)
        }
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

这样在权限检查完成前显示 loading,不会闪烁。

十、路由重定向:未登录访问受保护页 -> 登录 -> 回原页

这是最经典的流程,也是前文各节内容的串联。把完整链路画清楚:

  1. 用户点击"个人中心" -> 前置守卫检测未登录 -> 暂存 pendingRoute = 'ProfilePage' -> 跳转登录页
  2. 用户在登录页完成登录 -> 检测到 pendingRoute 非空 -> 跳转到 ProfilePage -> 清除 pendingRoute
  3. 如果用户在登录页点了返回(没登录) -> pendingRoute 也得清掉,否则下次登录会误跳

完整实现:

// RouteManager.ets - 统一路由管理器
export class RouteManager {
  private static instance: NavPathStack = new NavPathStack();

  public static bind(stack: NavPathStack): void {
    RouteManager.instance = stack;
    RouteManager.setupInterception();
  }

  private static setupInterception(): void {
    RouteManager.instance.setInterception({
      willShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
        operation: NavigationOperation, isAnimated: boolean) => {
        let targetName = '';
        if (to instanceof NavDestinationContext) {
          targetName = to.pathInfo.name;
        }
        if (targetName.length === 0) {
          return;
        }
        let token = AppStorage.get<string>('userToken');
        let isLoggedIn = token !== undefined && token.length > 0;
        if (!isLoggedIn && RouteManager.isProtectedPage(targetName)) {
          // 暂存目标页
          AppStorage.setOrCreate('pendingRoute', targetName);
          // 取消原跳转,跳登录
          RouteManager.instance.pushPathByName('LoginPage', null, false);
        }
      }
    });
  }

  private static isProtectedPage(name: string): boolean {
    let protectedList: Array<string> = ['ProfilePage', 'OrderPage', 'SettingsPage'];
    for (let i = 0; i < protectedList.length; i++) {
      if (protectedList[i] === name) {
        return true;
      }
    }
    return false;
  }

  // 登录成功后调用
  public static onLoginSuccess(): void {
    let pending = AppStorage.get<string>('pendingRoute');
    if (pending !== undefined && pending.length > 0) {
      let target = pending;
      AppStorage.setOrCreate('pendingRoute', '');
      RouteManager.instance.pushPathByName(target, null);
    } else {
      RouteManager.instance.pushPathByName('Index', null);
    }
  }

  // 退出登录后调用
  public static onLogout(): void {
    AppStorage.setOrCreate('userToken', '');
    AppStorage.setOrCreate('userRole', '');
    AppStorage.setOrCreate('pendingRoute', '');
    RouteManager.instance.clear();
  }
}

在登录页的登录按钮回调中:

Button('登录')
  .onClick(() => {
    // 登录逻辑...
    AppStorage.setOrCreate('userToken', 'new_token');
    RouteManager.onLoginSuccess();
  })

退出登录时:

Button('退出登录')
  .onClick(() => {
    RouteManager.onLogout();
  })

这样整个重定向链路就通了。关键点在 onLoginSuccess 里先检查 pendingRoute,有就跳过去,没有就去首页。onLogout 里务必清掉 pendingRoute,防止残留脏数据。
在这里插入图片描述

常见坑与实践建议

问题 表现 原因 解决方案
PersistentStorage 未初始化就读 冷启动首次判断登录态失败 persistProp 在 loadContent 之后调用 把 persistProp 放在 loadContent 之前
setInterception 循环拦截 登录页也被 willShow 拦截,死循环 needAuth 没排除登录页 登录页加入白名单,不被拦截
onBackPress 不生效 在 NavDestination 里写 onBackPress 无反应 @Entry 页面用 onBackPress,NavDestination 用 onBackPressed 区分使用场景,NavDestination 里用 onBackPressed
pop() 绕过 onBackPressed 代码调 pathStack.pop() 时返回拦截不触发 onBackPressed 只拦截系统返回和侧滑 统一封装 pop 方法,在方法内加检查
pendingRoute 未清除 退出登录再登录,跳到旧页面 退出时没清 pendingRoute onLogout 里主动清除所有暂存路由
深链参数丢失 从外部进来后跳转缺参数 URI 解析不完整,只取了页面名没取参数 parseDeepLink 要完整解析并暂存参数
页面闪烁 无权限页面先渲染再跳走 aboutToAppear 是页面已创建后才执行 加 isChecking 状态,检查前显示 loading
多角色权限遗漏 普通用户看到管理员入口 菜单渲染没过滤 用 RouteTable.getAccessibleRoutes() 过滤菜单项
token 过期未处理 本地有 token 但服务端已过期 只检查有无 token,没校验有效期 加 token 过期时间判断或服务端 401 时清 token
AppStorage 并发写入 多处同时写 userToken 导致状态不一致 AppStorage 是同步的但无事务 限定写入入口,统一通过 RouteManager 操作

总结

HarmonyOS 的路由守卫不像 Web 端有一个统一的 beforeEach 钩子,但它给了你足够的积木来拼。setInterception 做全局拦截,onBackPressed 做返回拦截,aboutToAppear 做页面级兜底,PersistentStorage 做持久化,AppStorage 做运行时共享。把这些拼起来,就是一套完整的路由守卫体系。

核心原则就一条:所有路由操作走统一入口,不给人绕过守卫的机会。中间件模式也好、RouteManager 也好,都是在解决这个问题。剩下的就是根据业务复杂度选方案——简单项目一个 AuthMiddleware 就够了,复杂项目才需要完整的中间件链。

别过度设计,也别裸奔。找到你项目对应的那个度,写出来的代码才好维护。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐