Skip to content

路由和导航

路由和导航

  • 多页面应用
    一个项目中有多个完整HTML文件,使用超链接跳转--销毁一棵DOM树,同步请求另一棵,得到之后再重建的DOM树;
    不足:DOM树要反复重建,间隔中客户端一片空白

  • 单页面应用
    称为SPA(Single Page Application),整个项目中有且只有一个”完整的”HTML文件,其它的页面都是异步加载的,当点击超链接时,只修改页面内容,不销毁一棵DOM树,也不重建一棵DOM树。
    单页面应用的优势:整个项目中客户端只需要下载一个HTML页面,创建一个完整的DOM树,页面跳转都是一个element替换另一个element,能够实现过场动画
    单页面应用的不足:不利于SEO访问优化。

NOTE

路径、路线、路由,有两部分:目标地址+处理过程
路由器,内部包含路由词典

常见的路由任务

生成一个支持路由的应用

使用 Angular CLI 生成一个带有应用路由的基础 Angular 应用。

添加用于路由的组件

要使用 Angular 路由器,应用需要至少有两个组件,以便它可以从一个导航到另一个。

bash
# 生成first和second组件
ng generate component first
ng generate component second

CLI 会自动附加 Component,因此如果你写 first-component,你的组件将是 FirstComponentComponent

导入这些新组件

要使用你的新组件,在文件顶部将它们导入。
基于独立应用的文件app.routes.ts,基于模块化路由的文件app-routing.module.ts,添加如下代码:

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

定义一个基本路由

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

  • 基于独立应用
    将路由导入 app.config.ts 并添加到 provideRouter 函数中。以下是使用 CLI 的默认 ApplicationConfig
    typescript
    export const appConfig: ApplicationConfig = {
      providers: [provideRouter(routes)]
    };
  • 基于模块化路由 在 app-routing.module.tsimports数组中添加RouterModule.forRoot(routes)
    typescript
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule],
    })
    export class AppRoutingModule { }

Angular CLI 会为你执行此步骤。但是,如果你手动创建应用或处理现有的非 CLI 应用,请验证导入和配置是否正确。

  1. 为你的路由设置一个 Routes 数组
    app.route.tsapp-routing.module.ts 中创建一个 Routes 数组。

    • 基于独立应用app.route.ts

      typescript
      import { Routes } from '@angular/router';
      export const routes: Routes = [];
    • 基于模块化路由app-routing.module.ts

      typescript
      import { Routes } from '@angular/router';
      const routes: Routes = [];
  2. 在你的 Routes 数组中定义你的路由

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

    typescript
    const routes: Routes = [
      { path: 'first-component', component: FirstComponent },
      { path: 'second-component', component: SecondComponent },
    ];

    path 是一个字符串,它定义了路由的 URL 路径。component 是一个组件类,它定义了 Angular 应该为相应路径使用的组件。

  3. 将你的路由添加到应用中

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

    html
    <!-- app.component.html -->
    <h1>Angular Router App</h1>
    <nav>
      <ul>
        <li><a routerLink="/first-component" routerLinkActive="active" ariaCurrentWhenActive="page">First Component</a></li>
        <li><a routerLink="/second-component" routerLinkActive="active" ariaCurrentWhenActive="page">Second Component</a></li>
      </ul>
    </nav>
    <!-- The routed views render in the <router-outlet>-->
    <router-outlet></router-outlet>

    还需要将 RouterLinkRouterLinkActiveRouterOutlet 添加到 AppComponentimports 数组中。

    typescript
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'routing-app';
    }
路由顺序

路由的顺序很重要,因为 Router 使用先匹配先赢策略来匹配路由,因此更具体的路由应放在不太具体的路由上方。 优先列出具有静态路径的路由,然后是空路径路由(匹配默认路由)。 通配符路由 放在最后,因为它匹配每个 URL,且 Router 仅在没有其他路由匹配时才选择它。

获取路由信息

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

使用路由将此类信息传递给你的应用组件。 为此,你可以使用 withComponentInputBinding 特性与 provideRouterRouterModule.forRootbindToComponentInputs 选项。

要从路由中获取信息:

  1. 添加withComponentInputBinding或者bindToComponentInputs 独立应用中,将 withComponentInputBinding 特性添加到 provideRouter 方法中。

    typescript
    providers: [
      provideRouter(appRoutes, withComponentInputBinding()),
    ]

    路由模块中,将bindToComponentInputs添加到RouterModule.forRoot方法中。

    typescript
    @NgModule({
      imports: [RouterModule.forRoot(routes, { bindToComponentInputs: true })],
    })
  2. 给组件添加一个 Input
    更新组件以具有与该参数名称匹配的 Input

    typescript
    @Input()
    set id(heroId: string) {
      this.hero$ = this.service.getHero(heroId);
    }

    注意:你可以将所有路由数据(包括键值对)绑定到组件输入:静态或已求解的路由数据、路径参数、矩阵参数和查询参数。 如果你想使用父组件的路由信息,需要设置路由器的 paramsInheritanceStrategy 选项: withRouterConfig({paramsInheritanceStrategy: 'always'})RouterModule.forRoot(routes, { paramsInheritanceStrategy: 'always' })

设置通配符路由

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

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

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

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

显示 404 页面

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

typescript
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。

typescript
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('')。

有时候,重定向并不是一个简单的静态重定向。 redirectTo 属性也可以是一个包含更复杂逻辑的函数,该函数返回一个字符串或 UrlTree

typescript
const routes: Routes = [
  { path: "first-component", component: FirstComponent },
  {
    path: "old-user-page",
    redirectTo: ({ queryParams }) => {
      const errorHandler = inject(ErrorHandler);
      const userIdParam = queryParams['userId'];
      if (userIdParam !== undefined) {
        return `/user/${userIdParam}`;
      } else {
        errorHandler.handleError(new Error('Attempted navigation to user page without user ID.'));
        return `/not-found`;
      }
    },
  },
  { path: "user/:userId", component: OtherComponent },
];

嵌套路由

随着你的应用变得更加复杂,你可能希望创建相对于某个组件(而不是根组件)的路由。这些类型的嵌套路由称为子路由。这意味着你需要在应用中添加第二个 <router-outlet>,因为它是 AppComponent 中的 <router-outlet> 之外的另一个。

在这个示例中,有两个额外的子组件, child-achild-b。这里, FirstComponent 有它自己的 <nav> 和一个第二个 <router-outlet>,这是在 AppComponent 之外的另一个。

html
<h2>First Component</h2>
<nav>
  <ul>
    <li><a routerLink="child-a">Child A</a></li>
    <li><a routerLink="child-b">Child B</a></li>
  </ul>
</nav>
<router-outlet></router-outlet>

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

typescript
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属性设置文档标题。

typescript
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属性遵循与静态路由 data和实现 ResolveFn的动态值相同的规则。

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

typescript
@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}`);
    }
  }
}
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    {provide: TitleStrategy, useClass: TemplatePageTitleStrategy},
  ]
};

使用相对路径

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

html
<h2>First Component</h2>
<nav>
  <ul>
    <li><a routerLink="../second-component">Relative Route to second component</a></li>
  </ul>
</nav>
<router-outlet></router-outlet>

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

指定相对路由

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

然后在你的导航方法中使用 relativeTo。 在链接参数数组之后,这里包含 items,添加一个对象,其中 relativeTo属性设置为 ActivatedRoute,即 this.route

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

navigate()参数配置路由器使用当前路由作为基础,以便附加 items

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

访问查询参数和片段

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

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

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

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

typescript
constructor(private route: ActivatedRoute) {}

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

typescript
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();
    })
  );
}

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

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

在组件类的构造函数中注入 ActivatedRouteRouter,以便它们对该组件可用:

typescript
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 只会在需要时才加载这些模块,而不是在应用启动时就加载全部。另外,你可以在后台预加载一些应用部件来改善用户体验。

有关惰性加载和预加载的更多信息,请参阅专门的指南 惰性加载。

防止未经授权的访问

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

  • CanActivate
  • CanActivateChild
  • CanDeactivate
  • CanMatch
  • resolve
  • CanLoad

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

为你的守卫创建一个文件:

bash
ng generate guard your-guard

请在守卫文件里添加你要用到的守卫函数。下面的例子使用 canActivateFn 来保护该路由。

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

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

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

链接参数数组

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

  • 指向目标组件的那个路由的路径(path
  • 必备路由参数和可选路由参数,它们将进入该路由的 URL

RouterLink指令绑定到这样的数组:

html
<a [routerLink]="['/heroes']">Heroes</a>

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

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

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

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

这三个示例涵盖了具有一级路由的应用的需求。然而,通过子路由器,例如在crisis-center,你可以创建新的链接数组可能性。

以下最小 RouterLink示例基于为crisis-center指定的默认子路由。

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

查看以下内容:

  • 数组中的第一个条目标记出了父路由(/crisis-center)。
  • 这个父路由没有参数。
  • 没有默认的子路由,因此你得选取一个。
  • 你正在导航到路由路径为 /CrisisListComponent,但你不需要显式添加斜杠。

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

html
<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>
  • 数组中的第一个条目标记出了父路由(/crisis-center)。
  • 这个父路由没有参数。
  • 第二条目标识特定危机的子路由详情( /:id)。
  • 详细的子路由需要一个 id 路由参数。
  • 你将龙危机的 id添加为数组中的第二条目( 1)。
  • 最终生成的路径是 /crisis-center/1

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

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

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

LocationStrategy 和浏览器 URL 样式

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

现代 HTML5 浏览器支持 history.pushState,这是一种在不触发服务器页面请求的情况下更改浏览器位置和历史记录的技术。 路由器可以组合一个“自然”的URL,这个URL和需要加载页面的URL没有区别。

下面是危机中心的 URL 在“HTML 5 pushState”风格下的样子:
localhost:3002/crisis-center

旧版浏览器在位置 URL 更改时向服务器发送页面请求,除非更改发生在「#」之后(称为「哈希」)。路由器可以通过使用哈希组成应用内路由 URL 来利用此例外。以下是一个路由到危机中心的「哈希 URL」。 localhost:3002/src/#/crisis-center

路由器支持两种样式,有两个 LocationStrategy提供者:

提供者详情
PathLocationStrategy默认的「HTML5 pushState」样式。
HashLocationStrategy「哈希 URL」样式。

RouterModule.forRoot() 函数将 LocationStrategy 设置为 PathLocationStrategy,这使其成为默认策略。 你也可以选择在引导过程中切换到 HashLocationStrategy

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

选择路由策略

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

几乎所有的 Angular 项目都应使用默认的 HTML5 样式。它生成的 URL 更易于用户理解,并保留了服务器端渲染的选项。

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

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

<base href>

路由器使用浏览器的 history.pushState 进行导航。 pushState 允许你自定义应用内的 URL 路径;例如, localhost:4200/crisis-center。 应用内的 URL 可以和服务器的 URL 一样。

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

提示:HTML5 样式导航是路由器的默认样式。 在 LocationStrategy 和浏览器 URL 样式部分,了解为什么 HTML5 样式更可取,如何调整其行为,以及在必要时如何切换到较旧的哈希( #)样式。

你必须向应用的 index.html 添加一个 <base href> 元素,以使 pushState 路由正常工作。 浏览器使用 <base href> 的值来为引用 CSS 文件、脚本和图像的相对 URL 添加前缀。

<head>标签之后添加 <base>元素。如果 app文件夹是应用的根目录,就像此应用一样,请在 index.html中设置如下所示的 href值。

html
<base href="/">

HTML5 网址和 <base href>

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

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

虽然路由器默认使用 HTML5 pushState 样式,但你必须通过 <base href> 配置该策略。

配置策略的首选方法是在 index.html<head> 中添加一个 <base href> 元素 标签。

html
<base href="/">

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

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

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

  1. 为路由器提供适当的 APP_BASE_HREF 值。
  2. 为所有 Web 资源使用根 URL(带有 authority的 URL):CSS、图像、脚本和模板 HTML 文件。
    • <base href> path 应该以“/”结尾,因为浏览器会忽略 path 中最右边的“ / ”后面的字符

    • 如果 <base href> 包含 query 部分,则只有在页面中的链接的 path 为空且没有 query 时才会使用 query。 这意味着只有在使用 HashLocationStrategy 时, <base href> 中的 query 才会包含在内。

    • 如果页面中的链接为根 URL(具有 authority),则不使用 <base href>。 这样,带有权限的 APP_BASE_HREF 将导致 Angular 创建的所有链接忽略 <base href> 值。

    • <base href> 中的片段(#后面的部分)永远不会被使用

HashLocationStrategy

通过在 AppModuleRouterModule.forRoot() 的第二个参数中提供 useHash: true,来使用 HashLocationStrategy

typescript
providers: [
  provideRouter(appRoutes, withHashLocation())
]

在使用 RouterModule.forRoot 时,通过在第二个参数中配置 useHash: true 来实现: RouterModule.forRoot(routes, {useHash: true})

在单页面应用中使用 Angular 路由

  1. 创建整个应用需要的路由组件

    bash
    ng generate component crisis-center
    ng generate component hero-list

    编辑crisis-list.component.html 并用如下 HTML 替换占位符内容。

    html
    <h3>CRISIS CENTER</h3>
    <p>Get your crisis here</p>

    编辑heroes-list.component.html 并用如下 HTML 替换占位符内容。

    html
    <h3>HEROES</h3>
    <p>Get your heroes here</p>

    编辑app.component.html 并用如下 HTML 替换其内容。

    html
    <h1>Angular Router Sample</h1>
    ...
    <app-crisis-list></app-crisis-list>
    <app-heroes-list></app-heroes-list>
  2. 定义你的各个路由
    定义两个路由:

    • 路由 /crisis-center 用来打开 crisis-center 组件。

    • 路由 /heroes-list 用来打开 heroes-list 组件。
      路由定义是一个 JavaScript 对象。每个路由通常都有两个属性。第一个属性 path 是一个字符串,它指定路由的 URL 路径。第二个属性 component 是组件类,它指定应用要为该路由显示哪个组件。

    • 独立应用
      创建并打开app.routes.ts文件,并添加以下代码:

      typescript
      import {Routes} from '@angular/router';
      
      // 为组件添加路由配置
      export const routes: Routes = [
        {path: 'crisis-list', component: CrisisListComponent},
        {path: 'heroes-list', component: HeroesListComponent},
      ];

      这个路由列表是一个 JavaScript 对象数组,每个对象定义了一个路由的属性。

    • 基于路由模块的应用
      打开app-routing.module.ts文件,修改routes的值:

      typescript
      import {CrisisListComponent} from './crisis-list/crisis-list.component';
      import {HeroesListComponent} from './heroes-list/heroes-list.component';
      
      const routes = [
        {path: 'crisis-list', component: CrisisListComponent},
        {path: 'heroes-list', component: HeroesListComponent},
      ];
      
      @ngModule({
        imports: [RouterModule.forRoot(routes)],
      })

      只在根模块 AppRoutingModule 中调用 RouterModule.forRoot()(如果在 AppModule 中注册应用的顶级路由,那就在 AppModule 中调用)。 在其它模块中,你就必须调用RouterModule.forChild方法来注册附属路由。

      在路由词典中,路由地址必须是唯一的;路由地址不能以/开头或者结尾,但中间可以包含;
      路由词典中可以指定一个默认首页地址:{path: '', component: ...}
      路由配置的顺序很重要。 路由器会接受第一个匹配上导航所要求的路径的那个路由。
      当所有路由都在同一个 AppRoutingModule 时,你要把默认路由和通配符路由放在最后(这里是在 /heroes 路由后面), 这样路由器才有机会匹配到 /heroes 路由,否则它就会先遇到并匹配上该通配符路由,并导航到“页面未找到”路由。
      这些路由不再位于单一文件中。他们分布在两个不同的模块中:AppRoutingModuleHeroesRoutingModule
      每个路由模块都会根据导入的顺序把自己的路由配置追加进去。 如果你先列出了 AppRoutingModule,那么通配符路由就会被注册在“英雄管理”路由之前。 通配符路由(它匹配任意URL)将会拦截住每一个到“英雄管理”路由的导航,因此事实上屏蔽了所有“英雄管理”路由。

  3. @angular/router 导入 provideRouter
    路由允许你根据 URL 路径显示应用的特定视图。要将此功能添加到你的示例应用中,你需要更新 app.config.ts 文件以使用路由提供者函数 provideRouter。你需要从 @angular/router 导入这个提供者函数。

    • 独立应用
      编辑app.config.ts文件,并添加以下代码:
      typescript
      import { provideRouter } from '@angular/router';
      import { routes } from './app.routes';
      更新 appConfig 中的提供者:
      typescript
      providers: [provideRouter(routes)]
    • 基于 NgModule 的应用
      对于基于 NgModule 的应用,需要将 provideRouter 放在 AppModuleproviders 列表中,或放在应用中传递给 bootstrapModule 的任何模块中。
      typescript
      import { provideRouter } from '@angular/router';
      
      @NgModule({
       providers: [provideRouter(routes)],
      })
    • 基于路由模块的应用
      不需要提供provideRouter
  4. 使用 router-outlet 更新你的组件
    此刻,你已经为应用定义了两个路由。但是,你的应用仍然在你的 app.component.html 模板中硬编码着 crisis-listheroes-list 组件。为了让你的路由正常工作,需要更新模板,以便根据 URL 路径动态加载一个组件。

    要实现此功能,你需要在模板文件中添加 router-outlet 指令。

    编辑app.component.html文件

    html
    // 删除下面两行代码
    <app-crisis-list></app-crisis-list>
    <app-heroes-list></app-heroes-list>
    
    // 添加代码
    <router-outlet></router-outlet>
    • 独立应用
      app.component.ts 中的 AppComponent 的导入列表中添加 RouterOutlet

      typescript
      imports: [RouterOutlet]
    • 基于路由模块的应用
      不需要imports [RouterOutlet]

    在浏览器中查看更新后的应用。你应该只看到应用标题。要查看 crisis-list 组件,就要把 crisis-list 添加到浏览器地址栏的路径末尾。比如:
    http://localhost:4200/crisis-list
    注意,crisis-list 组件会显示出来。Angular 正在使用你定义的路由来动态加载组件。你可以用同样的方法加载 heroes-list 组件:
    http://localhost:4200/heroes-list

  5. 导航控制

    • 使用 UI 元素控制导航
      目前,你的应用支持两种路由。但是目前使用这些路由的唯一方法是让用户在浏览器的地址栏中手动输入路径。在本节中,你要添加两个链接,用户可以单击它们在 heroes-listcrisis-list 组件之间导航。你还会添加一些 CSS 样式。虽然这些样式不是必需的,但它们可以让你更容易的识别出当前显示的组件的链接。你将在下一节中添加此功能。
      编辑app.component.html 文件,在标题下方添加以下 HTML。

      html
      <nav>
        <a class="button" routerLink="/crisis-list">Crisis Center</a> |
        <a class="button" routerLink="/heroes-list">Heroes</a>
      </nav>

      这个 HTML 使用了一个 Angular 指令 routerLink。这个指令将你定义的路由连接到模板文件。

      • 基于独立应用
        app.component.ts 中的 AppComponent 的导入列表中添加 RouterLink 指令。

        typescript
        import { RouterLink } from '@angular/router';
        @Component({
         imports: [RouterLink],
        })
      • 基于路由模块的应用
        不需要imports: [RouterLink]
        如果你在浏览器中查看应用,你会看到这两个链接。单击某个链接时,会出现相应的组件。

    • 使用脚本跳转
      路由器(Router)一个提供导航和操纵 URL 能力的 NgModule

      html
      <button (click)="jump()">跳转到CRISIS CENTER</button>
      typescript
      import { Router } from '@angular/router';
      export class HomePageComponent {
        private router: Router = inject(Router);
        jump() {
          this.router.navigateByUrl('/crisis-list');
        }
      }
  6. 标出活动路由
    虽然用户可以使用你在上一节中添加的链接来导航你的应用,但他们没有直接的方法来识别当前的活动路由。 使用 Angular 的 routerLinkActive 指令添加此功能。
    编辑app.component.html 文件,更新<a>以包含routerLinkActive指令。

    html
    <nav>
       <a class="button"
         routerLink="/crisis-list"
         routerLinkActive="activebutton"
         ariaCurrentWhenActive="page">
         Crisis Center
       </a> |
       <a class="button"
         routerLink="/heroes-list"
         routerLinkActive="activebutton"
         ariaCurrentWhenActive="page">
         Heroes
       </a>
    </nav>
    • 基于独立应用
      RouterLinkActive 指令添加到 app.component.tsAppComponentimports 列表中。
      typescript
      import { RouterLinkActive } from '@angular/router';
      @Component({
       imports: [RouterLinkActive],
      })
    • 基于路由模块的应用
      不需要imports: [RouterLinkActive],

    再次查看你的应用。 当你点击某个按钮时,该按钮的样式会自动更新,从而向用户标识活动组件。 通过添加 routerLinkActive 指令,你告知你的应用将特定的 CSS 类应用到活动路由。 在本教程中,该 CSS 类是 activebutton,但你可以使用任何你想要的类。

    请注意,我们还为 routerLinkActiveariaCurrentWhenActive 指定了一个值。这确保了视力障碍用户(可能无法感知所应用的不同样式)也可以识别活动按钮。有关详细信息,请参阅无障碍最佳实践 活动链接识别部分。

  7. 重定向 添加一个重定向路由来把用户导向 /heroes-list 组件。

    编辑app.routes.ts 文件,更新routes部分。

    typescript
    {path: '', redirectTo: '/heroes-list', pathMatch: 'full'},

    注意这个新路由使用一个空字符串作为路径。 此外,它用两个新属性替换了 component 属性:

    属性详情
    redirectTo此属性指示 Angular 从空路径重定向到 heroes-list 路径。
    pathMatch此属性指示 Angular 匹配 URL 的程度。在本教程中,你应该将此属性设置为 full。当路径为空字符串时,推荐使用这种策略。有关此属性的更多信息,请参阅 Route API 文档。

    现在,当你打开应用时,它会默认显示 heroes-list 组件。

  8. 添加 404 页面
    用户可以尝试访问你尚未定义的路由。为了解决这个问题,最佳做法是显示一个 404 页面。在本节中,你将创建一个 404 页面,并更新路由配置,以便为任何未指定的路由显示该页面。

    创建新组件PageNotFound,编辑模板内容为:

    html
    <h2>Page Not Found</h2>
    <p>We couldn't find that page! Not even with x-ray vision.</p>

    打开 app.routes.ts 文件,并将以下路由添加到 routes 列表中:

    typescript
    {path: '**', component: PageNotFoundComponent}

    新路由使用路径 **。 这是 Angular 识别通配符路由的方式。 任何不匹配你配置中现有路由的路由都将使用此路由。

    重要提示: 注意,通配符路由放在数组的末尾。 路由的顺序很重要,因为 Angular 按顺序应用路由,并使用找到的第一个匹配项。

    尝试导航到你的应用中的非存在路由,例如 http://localhost:4200/powers。 此路由与 app.routes.ts 文件中定义的任何内容都不匹配。 但是,因为你定义了一个通配符路由,应用会自动显示你的 PageNotFound 组件。

  9. 路由参数
    在路由词典中定义路由地址时,其中可以包含可变的参数:

    typescript
    {path: 'hero/:id', component: HeroDetailComponent}

    路由参数用冒号 : 标记。
    路由参数允许你从路由中获取数据。 例如,如果你的 URL 是 /hero/11,那么 11 就是一个路由参数。
    路由器会从 URL 中提取参数,并将其传递给组件。

    typescript
    import { ActivatedRoute } from '@angular/router';
    
    export class DetailPageComponent implements OnInit {
      private route: ActivatedRoute = inject(ActivatedRoute);
    
      ngOnInit() {
        this.route.params.subscribe(params => {
          console.log('detail-page params:');
          console.log(params);
          console.log(params['id']);
        });
      }
    
    }

    注意:在Angular 16之前,只能用ActivatedRoute来获取数据,从Angular 16开始数据可以自动绑定到 @Input 输入参数当中。可以通过 bindToComponentInputs 激活这个有趣的新功能。RouterModule.forRoot(routes, { bindToComponentInputs: true })

  10. 审查路由器配置
    通过注入它(Router)并检查它的 config 属性,你可以随时审查路由器的当前配置。 例如,把 AppModule 修改为这样,并在浏览器的控制台窗口中查看最终的路由配置。

    typescript
    export class AppModule {
      // Diagnostic only: inspect router configuration
      constructor(router: Router) {
        // Use a custom replacer to display function names in the route configs
        const replacer = (key, value) => (typeof value === 'function') ? value.name : value;
    
        console.log('Routes: ', JSON.stringify(router.config, replacer, 2));
      }
    }

附录:LocationStrategy 以及浏览器 URL 样式

当路由器导航到一个新的组件视图时,它会用该视图的 URL 来更新浏览器的当前地址以及历史。 严格来说,这个 URL 其实是本地的,浏览器不会把该 URL 发给服务器,并且不会重新加载此页面。

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

下面是crisis-center的 URL 在“HTML 5 pushState”风格下的样子:
localhost:3002/crisis-center/

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

路由器通过两种 LocationStrategy 提供商来支持所有这些风格:

  • PathLocationStrategy - 默认的策略,支持“HTML 5 pushState”风格。

  • HashLocationStrategy - 支持“hash URL”风格。

哪种策略更好?

你必须选择一种策略,并且在项目的早期就这么干。一旦该应用进入了生产阶段,要改起来可就不容易了,因为外面已经有了大量对应用 URL 的引用。

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

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

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

除非你有强烈的理由不得不使用 hash 路由,否则就应该坚决使用默认的 HTML 5 路由风格。

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

<base href> 标签

路由器使用浏览器的 history.pushState API 进行导航。借助 pushState 你可以让应用中的 URL 路径看上去就像你期望的那样,比如 localhost:4200/crisis-center,应用内的 URL 和服务器的 URL 没有区别。

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

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

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

HTML 5 URL 与<base href>

由于路由器默认使用“HTML 5 pushState”风格,所以你必须用一个base href来配置该策略(Strategy)。

配置该策略的首选方式是往 index.html 的 <head> 中添加一个<base href> element标签<base href="/">

如果没有此标签,当通过“深链接”进入该应用时,浏览器就不能加载资源(图片、CSS、脚本)。如果有人把应用的链接粘贴进浏览器的地址栏或从邮件中点击应用的链接时,这种问题就发生。

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

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

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

  • 对所有 Web 资源使用绝对地址:CSS、图片、脚本、模板 HTML。

HashLocationStrategy 策略

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

typescript
@NgModule({
  RouterModule.forRoot(routes, { useHash: true })
})

路由器参考

激活路由

路由的路径和参数可以通过注入名为 ActivatedRoute 的路由服务获得。它提供了大量有用的信息,包括:

属性详情
url一个路由路径的 Observable,表示为路由路径的各部分的字符串数组。
data一个包含为该路由提供的 data 对象的 Observable。还包含来自解析守卫的任何解析值。
params一个包含特定路由的必需和可选参数的 Observable
paramMap一个包含特定路由的必需和可选参数的 映射 的 Observable。该映射支持从同一参数中检索单个值和多个值。
queryParamMap一个包含所有路由可用查询参数的 映射 的 Observable。该映射支持从查询参数中检索单个值和多个值。
queryParams一个包含所有路由可用查询参数的 Observable
fragment所有路由可用的 URL 片段的 Observable
outlet用于渲染路由的 RouterOutlet 的名称。对于未命名的出口,出口名称是 primary
routeConfig用于包含原始路径的路由的路由配置。
parent当此路由是子路由时,该路由的父级 ActivatedRoute
firstChild包含此路由的子路由列表中的第一个 ActivatedRoute
children包含当前路由下激活的所有子路由。

路由器事件

在每次导航期间, Router通过 Router.events属性发出导航事件。 这些事件显示在下表中。

路由器事件详情
NavigationStart当导航开始时触发。
RouteConfigLoadStartRouter延迟加载路由配置之前触发。
RouteConfigLoadEnd在路由已被延迟加载后触发。
RoutesRecognized当路由器解析URL并识别路由时触发。
GuardsCheckStart当路由器开始路由守卫阶段时触发。
ChildActivationStart当路由器开始激活路由的子级时触发。
ActivationStart当路由器开始激活路由时触发。
GuardsCheckEnd当路由器成功完成路由守卫阶段时触发。
ResolveStart当路由器开始路由解析阶段时触发。
ResolveEnd当路由器成功完成路由解析阶段时触发。
ChildActivationEnd当路由器成功完成激活路由的子级时触发。
ActivationEnd当路由器成功完成激活路由时触发。
NavigationEnd当导航成功结束时触发。
NavigationCancel当导航被取消时触发。这可能发生在导航期间路由守卫返回false,或通过返回 UrlTreeRedirectCommand进行重定向时。
NavigationError由于意外错误导致导航失败时触发。
Scroll表示滚动事件。

当你启用 withDebugTracing 功能时,Angular 会将这些事件记录到控制台中。

路由器术语

以下是关键的 Router 术语及其含义:

路由器部分详情
Router显示当前 URL 对应的应用程序组件。管理从一个组件到下一个组件的导航。
provideRouter为浏览应用视图提供必要的服务提供者。
RouterModule一个单独的 NgModule,为浏览应用视图提供必要的服务提供者和指令。
Routes定义一组路由,每个路由将 URL 路径映射到一个组件。
Route根据 URL 模式定义路由器如何导航到组件。大多数路由包含路径和组件类型。
RouterOutlet指示路由器显示视图的指令( <router-outlet>)。
RouterLink用于将可点击的 HTML 元素绑定到路由的指令。单击带有绑定到 字符串 或 链接参数数组 的 routerLink 指令的元素会触发导航。
RouterLinkActive用于在相关的 routerLink 处于活动状态/非活动状态时向 HTML 元素添加/移除类。它还可以为更好的无障碍性设置活动链接的 aria-current
ActivatedRoute为每个路由组件提供的服务,包含路由特定信息,如路由参数、静态数据、解析数据、全局查询参数和全局片段。
RouterState路由器的当前状态,包括当前激活路由的树,以及用于遍历路由树的便捷方法。
链接参数数组路由器将解释为路由指令的数组。你可以将该数组绑定到 RouterLink 或将数组作为参数传递给 Router.navigate 方法。
路由组件一个包含 RouterOutlet 的 Angular 组件,根据路由导航显示视图。