在开发大型Angular应用时,路由管理往往变得复杂且难以维护。为了解决这个问题,尝试了Netanel文章中描述的路由路径管理方案。这个方案对于管理大型Angular应用中的路由非常有用。但是,很快发现,对于拥有多个特性模块及其自己的路由的大型应用来说,这个方案并不完全适用。如果这些特性模块还有自己的懒加载特性模块和路由,那么单一的服务类就不够用了。让用一个简化的例子来说明意思。
AppModule示例
假设有一个AppModule,其中定义了以下路由:
const appRoutes: Routes = [
  {
    path: '',
    component: LandingComponent
  },
  {
    path: 'about',
    component: AboutComponent
  },
  {
    path: 'contact',
    component: ContactComponent
  },
  {
    path: 'products',
    loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
  },
  {
    path: 'customers',
    loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
  }
];
        
这里有两个懒加载模块用于路由“products”和“customers”。Products模块包含了一个特性模块。以下是与之相关的特征路由声明:
const productsRoutes: Routes = [
  {
    path: 'orders',
    component: OrdersComponent
  },
  {
    path: 'edit',
    loadChildren: () => import('./edit/edit.module').then(m => m.EditModule),
    canActivate: [AdminGuard]
  }
];
        
const editOrdersRoutes: Routes = [
  { path: '', component: EditOrdersComponent },
  { path: ':orderId', component: EditOrderComponent },
];
        
在Netanel的文章中,一个类中的方法可以很好地处理平面路由结构:
@Injectable({ providedIn: 'root' })
export class PathResolverService {
  about() {
    return '/about';
  }
  contact() {
    return '/contact';
  }
  products() {
    // ???
  }
}
        
但是,对于懒加载特性模块的路由,能做些什么呢?以下是考虑的三个简单的选项。
简单选项#1
创建仅在顶级的方法,忽略路由的嵌套性质:
products() {
  return '/products';
}
productsOrders() {
  return '/products/orders';
}
productsEdit(orderId?: string) {
  const commands = ['products', 'edit'];
  if (orderId) {
    commands.push(orderId);
  }
  return this.router.createUrlTree(commands).toString();
}
        
这种方法有一些明显的缺点:
- 特性模块的方法在同一个类中管理。
- 方法名称长且重复。
- 每个子路由明确指定了父路径/products。
- 对于edit特性模块的子路由,这将变得非常丑陋。
简单选项#2
让products方法返回一个对象,尝试表示路由的嵌套性质:
products() {
  return {
    orders: () => '/products/orders',
    edit: (orderId?: string) => {
      const commands = ['products', 'edit'];
      if (orderId) {
        commands.push(orderId);
      }
      return this.router.createUrlTree(commands).toString();
    }
  };
}
        
现在可以这样输入:
const url = this.pathResolver.products.edit(orderId);
        
这感觉更好一些,但仍然有一些缺点:
- 特性模块的方法在同一个类中管理。
- 根products路由丢失了。
- 每个子路由明确指定了父路径/products。
简单选项#3
为products路由创建一个单独的类:
class AppRoutes {
  products = new RoutesForProducts();
}
class RoutesForProducts() {
  private parentPath = 'products';
  orders() {
    return `/${this.parentPath}/orders`;
  }
  edit() {
    return new RoutesForEditOrders();
  }
}
        
这种方法还允许像这样使用路由:
const url = this.pathResolver.products.edit(orderId);
        
现在,获得了在单独的文件中管理子路由的能力,但是失去了使用Angular依赖注入的能力!以下缺点仍然存在:
- 根products路由丢失了(可以添加一个root()方法?)。
- 明确使用this.parentPath并不感觉DRY。
- parentPath需要知道它在懒加载特性路由的层次结构中的位置。否则,生成的URL将是错误的。
长话短说,决定创建一个解决方案,它将保留Netanal解决方案的所有优点,并添加正在寻找的功能:
- 通过单独的类管理特性模块的路由。
- 使用属性链来反映路由的嵌套性质。
- 在方法实现中不显式使用parentPath。使用相对URL部分来组装URL。
- 灵活的返回类型:可以访问url,urlTree(对于RouteGuards很有用),或者无缝navigate()到所需的路由。
- 一个实用函数,简化了this.route.createUrlTree(commands)方法的使用。
欢迎来到@ngspot/route-path-builder。
@ngspot/route-path-builder库由一个抽象类组成——RoutePathBuilder。以下是新库如何使用上面的假设示例来描述路由:
import { RoutePathBuilder } from '@ngspot/route-path-builder';
@Injectable({ providedIn: 'any' })
export class AppRoutes extends RoutePathBuilder {
  products = this.childRoutes('products', RoutesForProducts);
  customers = this.childRoutes('customers', RoutesForCustomers);
  about() {
    return this.url('about');
  }
  contact() {
    return this.url('contact');
  }
}
@Injectable({ providedIn: 'any' })
export class RoutesForProducts extends RoutePathBuilder {
  edit = this.childRoutes('edit', RoutesForEditOrders);
  orders() {
    return this.url('orders');
  }
}
@Injectable({ providedIn: 'any' })
export class RoutesForEditOrders extends RoutePathBuilder {
  order(orderId?: string) {
    return this.urlFromCommands([orderId]);
  }
}
        
通过这种设置,在应用的任何地方注入AppRoutes并使用它!
const url1 = this.appRoutes.products.orders().url;
console.log(url1);
// "/products/orders"
const url2 = this.appRoutes.products.edit.order(orderId).url;
console.log(url2);
// "/products/edit/15"
// 这将导航到所需的路由
this.appRoutes.products.edit.order(orderId).navigate();
        
AppRoutes可以在路由解析器中这样使用:
@Injectable()
export class AuthGuardService implements CanActivate {
  constructor(
    public auth: AuthService,
    public appRoutes: AppRoutes
  ) {}
  canActivate(): boolean | UrlTree {
    if (!this.auth.isAuthenticated()) {
      return this.appRoutes.login().urlTree;
    }
    return true;
  }
}
        
RoutePathBuilder提供了一个root()方法,返回给定特性模块的根路径的AppUrl。例如:
const productsRootUrl = this.appRoutes.products.root().url;
console.log(productsRootUrl);
// "/products"
        
RoutePathBuilder还公开了两个受保护的属性——router和injector。router是一个方便的方式,可以在不需要在组件或服务中注入额外服务的情况下访问router。injector也在那里,以避免在构造函数中提供依赖项。例如:
@Injectable({ providedIn: 'any' })
export class AppRoutes extends RoutePathBuilder {
  private ff = this.injector.get(FeatureFlag);
  todos() {
    return this.ff.hasAccess()
      ? this.url('v2/todos')
      : this.url('todos');
  }
}
        
当然,依赖项也可以在构造函数中提供,但在这种情况下,需要将Injector添加到依赖项中,并将super(injector)添加到构造函数的主体中。
注意,扩展RoutePathBuilder的服务使用了{ providedIn: 'any' }。这意味着应用程序的每个懒加载特性模块将创建该服务的一个单独实例。这很重要,因为injector应该是对该懒加载模块的injector的引用,而不是根模块的injector。这样,访问在懒加载特性模块中声明的服务就不会失败。