PAYATHON 2026

如何在 Ember.Router 中限制特定 routes 的访问

支付小周

结论

不要在 serialize() 中做访问验证,也不需要用 setTimeout 修正 URL。更合适的做法是集中管理 checkout 的步骤状态,并在目标 route 的 beforeModel() 中检查前置条件。如果条件不满足,就用 replaceWith() 跳转到最早尚未完成的步骤。

无论用户正常点击、手动输入 URL,还是刷新深层 route,都会经过同一套验证逻辑。hash 和 history 只负责管理 URL,不参与流程判断。

为什么使用 beforeModel()

这些 hook 各有用途:

  • serialize() 负责把 model 转换成 URL 参数,不适合做权限或流程验证。
  • model() 负责加载当前 route 的数据。
  • afterModel() 或 redirect() 适合处理依赖当前 route model 的判断。
  • beforeModel() 适合检查登录状态、checkout 前置步骤等不依赖当前 route model 的条件。

如果验证过程需要读取服务端、localStorage 或其他异步数据,可以直接在 beforeModel() 中等待 Promise。Ember 会暂停当前 transition,无须额外添加 timeout。

集中定义步骤顺序

不要让每个 route 各自判断应该退回哪个步骤。可以使用 service 统一保存步骤顺序和完成状态:

// app/services/checkout-flow.js
import Service, { service } from '@ember/service';

export default class CheckoutFlowService extends Service {
  @service checkoutSession;

  steps = [
    {
      route: 'checkout.billing.address',
      isComplete: () => Boolean(this.checkoutSession.billingAddress)
    },
    {
      route: 'checkout.delivery.address',
      isComplete: () => Boolean(this.checkoutSession.deliveryAddress)
    },
    {
      route: 'checkout.billing.method',
      isComplete: () => Boolean(this.checkoutSession.billingMethod)
    },
    {
      route: 'checkout.delivery.method',
      isComplete: () => Boolean(this.checkoutSession.deliveryMethod)
    }
  ];

  firstIncompleteBefore(targetStep) {
    for (let index = 0; index < targetStep; index++) {
      let step = this.steps[index];

      if (!step.isComplete()) {
        return step.route;
      }
    }

    return null;
  }
}

这里只检查目标步骤之前的状态。访问 billing address 时,不应因为 billing address 还没填写就触发重定向。只有访问 billing method 时,才需要检查之前要求完成的 address 步骤。

建立可复用的 route guard

可以创建一个 checkout route 基类,避免在每个 route 中重复编写验证代码:

// app/routes/checkout/guarded.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class GuardedCheckoutRoute extends Route {
  @service checkoutFlow;
  @service checkoutSession;
  @service router;

  checkoutStep = 0;

  async beforeModel(transition) {
    await super.beforeModel(transition);

    // 从服务端或持久化存储恢复 checkout 状态。
    await this.checkoutSession.restore();

    let fallbackRoute =
      this.checkoutFlow.firstIncompleteBefore(this.checkoutStep);

    if (fallbackRoute) {
      return this.router.replaceWith(fallbackRoute);
    }
  }
}

各步骤再继承这个基类:

// app/routes/checkout/billing/method.js
import GuardedCheckoutRoute from '../../guarded';

export default class CheckoutBillingMethodRoute extends GuardedCheckoutRoute {
  checkoutStep = 2;
}
// app/routes/checkout/delivery/method.js
import GuardedCheckoutRoute from '../../guarded';

export default class CheckoutDeliveryMethodRoute extends GuardedCheckoutRoute {
  checkoutStep = 3;
}

如果项目不打算使用继承,也可以把检查方法放到 service 中,再由每个受保护 route 的 beforeModel() 调用。无论采用哪种写法,都应集中定义步骤规则,route 只需声明自己对应第几步。

正确处理页面刷新

刷新深层 URL 后,内存中的 service 会重新创建。能否恢复到刷新前的步骤,取决于 checkout 数据保存在哪里。

例如:

// app/services/checkout-session.js
import Service from '@ember/service';

export default class CheckoutSessionService extends Service {
  billingAddress = null;
  deliveryAddress = null;
  billingMethod = null;
  deliveryMethod = null;

  async restore() {
    // 可以从服务端、sessionStorage 或 localStorage 恢复状态。
    // 应保证重复调用不会产生副作用。
  }
}

如果 checkout 状态只保存在内存中,刷新后所有步骤都会恢复为未完成。此时跳转到 billing address 是预期结果,并非 router 出了问题。

如果状态保存在服务端,应先恢复数据,再判断跳转位置。否则 guard 可能在数据尚未加载时,把步骤误判为未完成。

replaceWith() 与 transitionTo() 的选择

这种回退通常适合使用:

this.router.replaceWith('checkout.billing.address');

replaceWith() 会替换当前浏览器历史记录。用户按“后退”时,不会再次进入那个无权访问的深层 URL。

如果希望无效地址继续保留在历史记录中,可以使用:

this.router.transitionTo('checkout.billing.address');

对于步骤前置条件校验,通常优先使用 replaceWith()。

注意事项

  • fallback route 本身必须允许进入,否则可能造成重定向循环。
  • restore() 应能安全地重复调用,最好缓存已经完成的加载操作。
  • 进入某一步骤时,只检查它之前的步骤,不要把用户从较早步骤自动推进到较晚步骤。
  • 如果条件依赖当前 route 的 model,应在 afterModel() 或 redirect() 中判断。
  • 较老版本的 Ember 可能使用不同的类语法或 route API,但思路不变:在 transition 完成前验证,验证失败后通过 router 正式重定向,不要直接修改 hash。

完整流程是先恢复 checkout 状态,再按照固定顺序查找最早未完成的前置步骤,最后使用 replaceWith() 导航到对应 route。这样可以让 URL、浏览器历史和应用状态保持一致。

备注:内容仅供参考。