# 常见路由任务

本主题讲述当把 Angular 路由器添加到应用中时,如何实现多种常见路由任务。

# 生成一个支持路由的应用

下面的命令会用 Angular CLI 来生成一个带有应用路由模块(AppRoutingModule)的基本 Angular 应用,它是一个 NgModule,可用来配置路由。下面的例子中应用的名字是 routing-app

ng new routing-app --routing --defaults

# 为路由添加组件

为了使用 Angular 的路由器,应用至少要有两个组件才能从一个导航到另一个。要使用 CLI 创建组件,请在命令行输入以下内容,其中 first是组件的名称:

ng generate component first

为第二个组件重复这个步骤,但给它一个不同的名字。这里的新名字是 second

ng generate component second

CLI 会自动添加 Component后缀,所以如果在编写 first-component,那么其组件名就是 FirstComponentComponent

TIP

<base href>本指南适用于 CLI 生成的 Angular 应用。如果你是手动工作的,请确保你的 index.html 文件的 <head>中有 <base href="/">语句。这里假定 app文件夹是应用的根目录,并使用 "/"作为基础路径。

# 导入这些新组件

要使用这些新组件,请把它们导入到该文件顶部的 AppRoutingModule中,具体如下:

AppRoutingModule (excerpt)

import { FirstComponent } from './first/first.component';
import { SecondComponent } from './second/second.component';

# 定义一个基本路由

创建路由有三个基本的构建块。

AppRoutingModule导入 AppModule并把它添加到 imports数组中。

Angular CLI 会为你执行这一步骤。但是,如果要手动创建应用或使用现存的非 CLI 应用,请验证导入和配置是否正确。下面是使用 --routing标志生成的默认 AppModule

Default CLI AppModule with routing

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module'; // CLI imports AppRoutingModule
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule // CLI adds AppRoutingModule to the AppModule's imports array
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
  • RouterModuleRoutes导入到你的路由模块中。

Angular CLI 会自动执行这一步骤。CLI 还为你的路由设置了 Routes数组,并为 @NgModule()配置了 importsexports数组。

CLI application routing module

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; // CLI imports router

const routes: Routes = []; // sets up routes constant where you define your routes

// configures NgModule imports and exports
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
  • Routes数组中定义你的路由。

这个数组中的每个路由都是一个包含两个属性的 JavaScript 对象。第一个属性 path定义了该路由的 URL 路径。第二个属性 component定义了要让 Angular 用作相应路径的组件。

AppRoutingModule (excerpt)

const routes: Routes = [
  { path: 'first-component', component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
];
  • 把这些路由添加到你的应用中。

现在你已经定义了路由,可以把它们添加到应用中了。首先,添加到这两个组件的链接。把要添加路由的链接赋值给 router属性。将属性的值设置为该组件,以便在用户点击各个链接时显示这个值。接下来,修改组件模板以包含 <router-outlet>标签。该元素会通知 Angular,你可以用所选路由的组件更新应用的视图。

Template with router and router-outlet

<h1>Angular Router App</h1>
<!-- This nav gives you s to click, which tells the router which route to use (defined in the routes constant in  AppRoutingModule) -->
<nav>
  <ul>
    <li><a router="/first-component" routerActive="active" ariaCurrentWhenActive="page">First Component</a></li>
    <li><a router="/second-component" routerActive="active" ariaCurrentWhenActive="page">Second Component</a></li>
  </ul>
</nav>
<!-- The routed views render in the <router-outlet>-->
<router-outlet></router-outlet>

# 路由顺序

路由的顺序很重要,因为 Router在匹配路由时使用“先到先得”策略,所以应该在不那么具体的路由前面放置更具体的路由。首先列出静态路径的路由,然后是一个与默认路由匹配的空路径路由。通配符路由是最后一个,因为它匹配每一个 URL,只有当其它路由都没有匹配时,Router才会选择它。

# 获取路由信息

通常,当用户导航你的应用时,你会希望把信息从一个组件传递到另一个组件。比如,考虑一个显示杂货商品购物清单的应用。列表中的每一项都有一个唯一的 id。要想编辑某个项目,用户需要单击“编辑”按钮,打开一个 EditGroceryItem组件。你希望该组件得到该商品的 id,以便它能向用户显示正确的信息。

可以用一个路由把这种类型的信息传给你的应用组件。要做到这一点,你可以使用 ActivatedRoute 接口。

要从路由中获取信息:

  • ActivatedRouteParamMap导入你的组件。

In the component class (excerpt)

import { Router, ActivatedRoute, ParamMap } from '@angular/router';

这些 import语句添加了组件所需的几个重要元素。要详细了解每个 API,请参阅以下 API 页面:

  • Router

  • ActivatedRoute

  • ParamMap

  • 通过把 ActivatedRoute的一个实例添加到你的应用的构造函数中来注入它:

In the component class (excerpt)

constructor(
  private route: ActivatedRoute,
) {}
  • 更新 ngOnInit()方法来访问这个 ActivatedRoute并跟踪 name参数:

In the component (excerpt)

ngOnInit() {
  this.route.queryParams.subscribe(params => {
    this.name = params['name'];
  });
}

注意:

前面的例子使用了一个变量 name,并根据 name参数给它赋值。

# 设置通配符路由

当用户试图导航到那些不存在的应用部件时,在正常的应用中应该能得到很好的处理。要在应用中添加此功能,需要设置通配符路由。当所请求的 URL 与任何路由器路径都不匹配时,Angular 路由器就会选择这个路由。

要设置通配符路由,请在 routes定义中添加以下代码。

AppRoutingModule (excerpt)

{ path: '**', component: <component-name> }

这两个星号 **告诉 Angular,这个 routes定义是通配符路由。对于 component 属性,你可以使用应用中的任何组件。常见的选择包括应用专属的 PageNotFoundComponent,你可以定义它来向用户展示 404 页面,或者跳转到应用的主组件。通配符路由是最后一个路由,因为它匹配所有的 URL。关于路由顺序的更多详细信息,请参阅路由顺序。

# 显示 404 页面

要显示 404 页面,请设置一个通配符路由,并将 component属性设置为你要用于 404 页面的组件,如下所示:

AppRoutingModule (excerpt)

const routes: Routes = [
  { path: 'first-component', component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
  { path: '**', component: PageNotFoundComponent },  // Wildcard route for a 404 page
];

path**的最后一条路由是通配符路由。如果请求的 URL 与前面列出的路径不匹配,路由器会选择这个路由,并把该用户送到 PageNotFoundComponent

# 设置重定向

要设置重定向,请使用重定向源的 path、要重定向目标的 component和一个 pathMatch值来配置路由,以告诉路由器该如何匹配 URL。

AppRoutingModule (excerpt)

const routes: Routes = [
  { path: 'first-component', component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
  { path: '',   redirectTo: '/first-component', pathMatch: 'full' }, // redirect to `first-component`
  { path: '**', component: PageNotFoundComponent },  // Wildcard route for a 404 page
];

在这个例子中,第三个路由是重定向路由,所以路由器会默认跳到 first-component路由。注意,这个重定向路由位于通配符路由之前。这里的 path: ''表示使用初始的相对 URL('')。

关于 pathMatch的详情,请参阅聚焦 pathMatch部分。

# 嵌套路由

随着你的应用变得越来越复杂,你可能要创建一些根组件之外的相对路由。这些嵌套路由类型称为子路由。这意味着你要为你的应用添加第二 &lt;router-outlet&gt;,因为它是 AppComponent之外的另一个 &lt;router-outlet&gt;

在这个例子中,还有两个子组件,child-achild-b。这里的 FirstComponent有它自己的 &lt;nav&gt;AppComponent之外的第二 &lt;router-outlet&gt;

In the template

<h2>First Component</h2>

<nav>
  <ul>
    <li><a router="child-a">Child A</a></li>
    <li><a router="child-b">Child B</a></li>
  </ul>
</nav>

<router-outlet></router-outlet>

子路由和其它路由一样,同时需要 pathcomponent。唯一的区别是你要把子路由放在父路由的 children数组中。

AppRoutingModule (excerpt)

const routes: Routes = [
  {
    path: 'first-component',
    component: FirstComponent, // this is the component with the <router-outlet> in the template
    children: [
      {
        path: 'child-a', // child route path
        component: ChildAComponent, // child route component that the router renders
      },
      {
        path: 'child-b',
        component: ChildBComponent, // another child route component that the router renders
      },
    ],
  },
];

# 设置页面标题

应用程序中的每个页面都应该有一个唯一的标题,以便可以在浏览器历史记录中识别它们。Router使用 Route配置中的 title属性设置文档的标题。

AppRoutingModule (excerpt)

const routes: Routes = [
  {
    path: 'first-component',
    title: 'First component',
    component: FirstComponent,  // this is the component with the <router-outlet> in the template
    children: [
      {
        path: 'child-a',  // child route path
        title: resolvedChildATitle,
        component: ChildAComponent,  // child route component that the router renders
      },
      {
        path: 'child-b',
        title: 'child b',
        component: ChildBComponent,  // another child route component that the router renders
      },
    ],
  },
];

const resolvedChildATitle: ResolveFn<string> = () => Promise.resolve('child a');

注意:

title属性遵循与实现 ResolveFn的静态路由 data和动态值相同的规则。

你还可以通过扩展 TitleStrategy来提供自定义标题策略。

AppRoutingModule (excerpt)

@Injectable({providedIn: 'root'})
export class TemplatePageTitleStrategy extends TitleStrategy {
  constructor(private readonly title: Title) {
    super();
  }

  override updateTitle(routerState: RouterStateSnapshot) {
    const title = this.buildTitle(routerState);
    if (title !== undefined) {
      this.title.setTitle(`My Application | ${title}`);
    }
  }
}

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: [
    {provide: TitleStrategy, useClass: TemplatePageTitleStrategy},
  ]
})
export class AppRoutingModule {
}

# 使用相对路径

相对路径允许你定义相对于当前 URL 段的路径。下面的例子展示了到另一个组件 second-component的相对路由。FirstComponentSecondComponent在树中处于同一级别,但是,指向 SecondComponent的链接位于 FirstComponent中,这意味着路由器必须先上升一个级别,然后进入二级目录才能找到 SecondComponent。可以用 ../符号来上升一个级别,而不用写出到 SecondComponent的完整路径。

In the template

<h2>First Component</h2>

<nav>
  <ul>
    <li><a router="../second-component">Relative Route to second component</a></li>
  </ul>
</nav>
<router-outlet></router-outlet>

除了 ../,还可以使用 ./或者不带前导斜杠来指定当前级别。

# 指定相对路由

要指定相对路由,请使用 NavigationExtras中的 relativeTo属性。在组件类中,从 @angular/router导入 NavigationExtras

然后在导航方法中使用 relativeTo参数。在链接参数数组(它包含 items)之后添加一个对象,把该对象的 relativeTo属性设置为当前的 ActivatedRoute,也就是 this.route

RelativeTo

goToItems() {
  this.router.navigate(['items'], { relativeTo: this.route });
}

goToItems()方法会把目标 URI 解释为相对于当前路由的,并导航到 items路由。

# 访问查询参数和片段

有时,应用中的某个特性需要访问路由的部件,比如查询参数或片段(fragment)。本教程的这个阶段使用了一个“英雄之旅”中的列表视图,你可以在其中点击一个英雄来查看详情。路由器使用 id来显示正确的英雄的详情。

首先,在要导航的组件中导入以下成员。

Component import statements (excerpt)

import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';

接下来,注入当前路由(ActivatedRoute)服务:

Component (excerpt)

constructor(private route: ActivatedRoute) {}

配置这个类,让你有一个可观察对象 heroes$、一个用来保存英雄的 id号的 selectedId,以及 ngOnInit()中的英雄们,添加下面的代码来获取所选英雄的 id。这个代码片段假设你有一个英雄列表、一个英雄服务、一个能获取你的英雄的函数,以及用来渲染你的列表和细节的 HTML,就像在《英雄之旅》例子中一样。

Component 1 (excerpt)

heroes$: Observable<Hero[]>;
selectedId: number;
heroes = HEROES;

ngOnInit() {
  this.heroes$ = this.route.paramMap.pipe(
    switchMap(params => {
      this.selectedId = Number(params.get('id'));
      return this.service.getHeroes();
    })
  );
}

接下来,在要导航到的组件中,导入以下成员。

Component 2 (excerpt)

import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';

在组件类的构造函数中注入 ActivatedRouteRouter,这样在这个组件中就可以用它们了:

Component 2 (excerpt)

hero$: Observable<Hero>;

constructor(
  private route: ActivatedRoute,
  private router: Router  ) {}

ngOnInit() {
  const heroId = this.route.snapshot.paramMap.get('id');
  this.hero$ = this.service.getHero(heroId);
}

gotoItems(hero: Hero) {
  const heroId = hero ? hero.id : null;
  // Pass along the hero id if available
  // so that the HeroList component can select that item.
  this.router.navigate(['/heroes', { id: heroId }]);
}

# 惰性加载

你可以配置路由定义来实现惰性加载模块,这意味着 Angular 只会在需要时才加载这些模块,而不是在应用启动时就加载全部。另外,你可以在后台预加载一些应用部件来改善用户体验。

关于惰性加载和预加载的详情,请参阅专门的指南惰性加载 NgModule。

# 防止未经授权的访问

使用路由守卫来防止用户未经授权就导航到应用的某些部分。Angular 中提供了以下路由守卫:

  • canActivate

  • canActivateChild

  • canDeactivate

  • canMatch

  • resolve

  • canLoad

要想使用路由守卫,可以考虑使用无组件路由,因为这对于保护子路由很方便。

为你的守卫创建一项服务:

请在守卫函数里实现你要用到的守卫。下面的例子使用 canActivate来保护该路由。

guard (excerpt)

export const yourGuard: CanActivateFn = (
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot) => {
      // your  logic goes here
  }

在路由模块中,在 routes配置中使用相应的属性。这里的 canActivate会告诉路由器它要协调到这个特定路由的导航。

Routing module (excerpt)

{
  path: '/your-path',
  component: YourComponent,
  canActivate: [yourGuard],
}

关于此可工作范例的更多信息,请参阅路由导航中关于路由守卫的部分。

# 链接参数数组

链接参数数组保存路由导航时所需的成分:

  • 指向目标组件的那个路由的路径(path)

  • 必备路由参数和可选路由参数,它们将进入该路由的 URL

可以把 Router指令绑定到一个数组,就像这样:

src/app/app.component.ts (h-anchor)

<a [router]="['/heroes']">Heroes</a>

在指定路由参数时,使用如下的两元素数组:

src/app/heroes/hero-list/hero-list.component.html (nav-to-detail)

<a [router]="['/hero', hero.id]">
  <span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>

可以在对象中提供可选的路由参数,比如 { foo: 'foo' }

src/app/app.component.ts (cc-query-params)

<a [router]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>

这三个例子涵盖了你在单级路由的应用中所需的一切。不过,在你添加一个像危机中心一样的子路由时,你可以创建新链接数组。

下面这个最小化 Router例子是基于危机中心指定的默认子路由构建的。

src/app/app.component.ts (cc-anchor-w-default)

<a [router]="['/crisis-center']">Crisis Center</a>

查看以下内容:

  • 数组中的第一个条目标记出了父路由(/crisis-center)。

  • 这个父路由没有参数。

  • 没有默认的子路由,因此你得选取一个。

  • 你决定跳转到 CrisisListComponent,它的路由路径是'/',但你不用显式的添加它。

考虑以下路由器链接,它将从应用的根目录导航到巨龙危机(Dragon Crisis):

src/app/app.component.ts (Dragon-anchor)

<a [router]="['/crisis-center', 1]">Dragon Crisis</a>
  • 数组中的第一个条目标记出了父路由(/crisis-center)。

  • 这个父路由没有参数。

  • 数组中的第二个条目('/:id')用来标记出到指定危机的详情页的子路由。

  • 详细的子路由需要一个 id路由参数。

  • 你把巨龙危机的 id添加为该数组中的第二个条目(1)。

  • 最终生成的路径是 /crisis-center/1

你也可以把危机中心的路由单独重新定义为 AppComponent的模板:

src/app/app.component.ts (template)

template: `
  <h1 class="title">Angular Router</h1>
  <nav>
    <a [router]="['/crisis-center']">Crisis Center</a>
    <a [router]="['/crisis-center/1', { foo: 'foo' }]">Dragon Crisis</a>
    <a [router]="['/crisis-center/2']">Shark Crisis</a>
  </nav>
  <router-outlet></router-outlet>
`

总之,你可以用一级、两级或多级路由来写应用程序。链接参数数组提供了用来表示任意深度路由的链接参数数组以及任意合法的路由参数序列、必须的路由器参数以及可选的路由参数对象。

# LocationStrategy和浏览器的网址样式

当路由器导航到一个新的组件视图时,它会用该视图的 URL 来更新浏览器的当前地址以及历史。

现代 HTML 5 浏览器支持history.pushStateAPI, 这是一项可以改变浏览器的当前地址和历史,却又不会触发服务端页面请求的技术。 路由器可以合成出一个“自然的”URL,它看起来和那些需要进行页面加载的 URL 没什么区别。

下面是危机中心的 URL 在“HTML 5 pushState”风格下的样子:

```localhost:3002/crisis-center老旧的浏览器在当前地址的 URL 变化时总会往服务器发送页面请求……唯一的例外规则是:当这些变化位于“#”(被称为“hash”)后面时不会发送。通过把应用内的路由 URL 拼接在#`之后,路由器可以获得这条“例外规则”带来的优点。下面是到危机中心路由的“hash URL”。

```localhost:3002/src/#/crisis-center路由器通过两种LocationStrategy`提供者来支持所有这些风格:

提供者 详情
PathLocationStrategy 默认的 “HTML 5 pushState” 风格。The default "HTML5 pushState" style.
HashLocationStrategy “hash URL”风格。The "hash URL" style.

RouterModule.forRoot()函数把 LocationStrategy设置成了 PathLocationStrategy,使其成为了默认策略。你还可以在启动过程中改写(override)它,来切换到 HashLocationStrategy风格。

关于提供者和引导过程的更多信息,请参阅依赖注入。

# 选择路由策略

你必须在开发项目的早期就选择一种路由策略,因为一旦该应用进入了生产阶段,你网站的访问者就会使用并依赖应用的这些 URL 引用。

几乎所有的 Angular 项目都会使用默认的 HTML 5 风格。它生成的 URL 更易于被用户理解,它也为将来做服务端渲染预留了空间。

在服务器端渲染指定的页面,是一项可以在该应用首次加载时大幅提升响应速度的技术。那些原本需要十秒甚至更长时间加载的应用,可以预先在服务端渲染好,并在少于一秒的时间内完整渲染在用户的设备上。

只有当应用的 URL 看起来像是标准的 Web URL,中间没有 hash(#)时,这个选项才能生效。

# &lt;base href&gt;

路由器使用浏览器的 history.pushState API 进行导航。借助 pushState你自定义应用中的 URL 路径 localhost:4200/crisis-center,应用内的 URL 和服务器的 URL 没有区别。

现代的 HTML5 浏览器都支持 pushState,这也就是为什么很多人把这种 URL 形式称为 "HTML 5" 风格的 URL。

路由器默认使用 HTML5 风格的导航。 在 LocationStrategy 与浏览器 URL 风格 部分,你可以了解为何推荐使用 HTML5 风格的 URL,如何调整其行为,以及必要时如何切换到老式的 hash(#)风格。

你必须在应用的 index.html中添加一个 &lt;base href>元素 才能让 pushState路由正常工作。 浏览器要用 &lt;base href&gt;的值为引用 CSS、脚本和图片文件时使用的相对URL 添加前缀。

请把 &lt;base&gt;元素添加在 &lt;head&gt;标签的紧后面。如果应用的根目录是 app目录,那么就可以像这个应用程序一样,设置 index.html中的 href值。代码如下。

src/index.html (base-href)

<base href="/">

# HTML5 网址和 &lt;base href&gt;

后面的指南中会引用 URL 的不同部分。下图是这些部分所指内容的梗概:


foo://example.com:8042/over/there?name=ferret#nose
\_/   \______________/\_________/ \_________/ \__/
 |           |            |            |        |
scheme    authority      path        query   fragment

由于路由器默认使用 HTML5 pushState 风格,所以你必须用一个 &lt;base href&gt;来配置该策略(Strategy)。

配置该策略的首选方式是往 index.html&lt;head&gt;中添加一个 &lt;base href>element 标签。

src/index.html (base-href)

<base href="/">

如果没有该标记,浏览器就可能无法在“深度链接”进入应用时加载资源(图片,CSS,脚本)。

有些开发人员可能无法添加 &lt;base&gt;元素,这可能是因为它们没有访问 &lt;head&gt;index.html的权限。

它们仍然可以使用 HTML 5 格式的 URL,但要采取如下步骤进行补救:

  • 用适当的[APP_BASE_HREF][]值提供(provide)路由器。

  • 对所有 Web 资源(CSS、图片、脚本和模板 HTML 文件)使用根 URL(高优先度 URL)。

  • &lt;base href&gt;``path应该以“/”结尾,因为浏览器会忽略 path中最右边的“ /”后面的字符

  • 如果 &lt;base href&gt;包含 query部分,则只有页内链接的 path部分为空并且没有 query时,才会使用这里的 query。这意味着 &lt;base href&gt;中的 query部分只有在使用 HashLocationStrategy策略时才有用。

  • 如果页内链接是根 URL(高优先度 URL),则 &lt;base href&gt;不会使用。在这种方式下,APP_BASE_HREF的优先度将会导致所有由 Angular 创建的链接忽略 &lt;base href&gt;

  • &lt;base href&gt;中的片段(#后面的部分)永远不会被使用

有关如何使用 &lt;base href&gt;构建目标 URI 的更完整信息,请参阅 RFC 有关转换引用的部分。

# HashLocationStrategy

可以在根模块的 RouterModule.forRoot()的第二个参数中传入一个带有 useHash: true的对象,以回到基于 HashLocationStrategy的传统方式。

src/app/app.module.ts (hash URL strategy)

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [

];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(routes, { useHash: true })  // .../#/crisis-center/
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [

  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
Last Updated: 5/13/2023, 10:57:08 AM