0%

angular-router

本文讲解一下如何在Angular项目中使用router

路由配置(NgModule based apps)

创建一个Angular项目

1
ng new angular-router

在创建过程中,Angular CLI会询问是否使用router,选择Yes

创建三个组件。

使用WebStorm或者VSCode打开上面创建的项目,呼出Terminal,执行以下命令。

1
2
3
ng generate component home
ng generate component product
ng generate component about

然后打开app-routing.module.ts文件,创建路由。注意,path不能以/开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {HomeComponent} from "./home/home.component";
import {ProductComponent} from "./product/product.component";
import {AboutComponent} from "./about/about.component";

// Add Routes here
const routes: Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

添加导航链接

打开app.component.html文件,添加导航链接。注意,routerLink的值要和path一致。如果是根路由,可以使用/。文档末尾的router-outlet是用来显示路由对应的组件的。当点击某一个链接的时候,Angular会找到对应的组件,并将其加载到router-outlet中。所以router-outlet相当于一个占位符,与html中的slot类似。

1
2
3
4
5
6
7
8
9
<h1>Angular Router Demo</h1>
<nav>
<ul>
<li><a routerLink="/home">Home</a></li>
<li><a routerLink="/product">Product</a></li>
<li><a routerLink="/about">About</a></li>
</ul>
</nav>
<router-outlet></router-outlet>

运行项目

Terminal中执行以下命令。打开浏览器,输入http://localhost:4200,就可以看到导航链接了。依次点击页面上的Home,Product,About链接,可以看到页面跳转了,并且浏览器中的地址也发生了变化。

1
ng serve

基于ROUTES的路由配置

ROUTES是Angular内置的一个Injection Token,在一些比较老的Angular app中,也有使用ROUTES来配置路由的,现举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@NgModule({
declarations: [
AppComponent,
ProductComponent,
ParentComponent,
ChildComponent,
HomeComponent,
AboutComponent,
],
imports: [
BrowserModule,
RouterModule.forRoot([]), // This line is must!
],
providers: [
{
provide: ROUTES,
useFactory: () => [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
],
multi: true, // This line is must!
},
],
bootstrap: [AppComponent]
})
export class AppModule {
}

useFactory后面跟的是一个工厂函数,这个函数我们直接返回一个路由数组。
使用这种方法后,我们就不需要路由文件app-routing.module.ts了,可以将它删除掉。

路由配置(Standalone apps)

基于standalone组件的app,路由配置稍有不同。

  1. main.ts文件中启动app时会传递appConfig进去,我们的路由配置就在这个appConfig中。
1
2
3
4
5
6
7
// main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
  1. app.config.ts文件中通过provideRouter函数提供路由。
1
2
3
4
5
6
7
8
9
10
// app.config.ts
import {provideRouter} from '@angular/router';
import {routes} from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
// ...
provideRouter(routes),
]
};
  1. app.routes.ts文件中定义路由。
1
2
3
4
5
6
// app.routes.ts
export const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
];

可见,路由的结构是十分清晰的。

添加404页面

如果我们在浏览器中随意输入一个不存在的路径,比如http://localhost:4200/abc,会发现页面是空白的。这是因为我们没有处理404页面。我们可以在app-routing.module.ts中添加一个**路径,用来匹配所有路径,然后将其重定向到一个404组件。

创建404页面组件

1
ng generate component not-found

修改404页面内容

打开not-found.component.html,输入以下内容。

1
<p>404: Not Found!</p>

添加404路由

1
2
3
4
5
6
7
// app-routing.module.ts
const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent},
{path: '**', component: NotFoundComponent}
];

此时,如果在浏览器里面输入一个不存在的路径,比如http://localhost:4200/abc,则会显示404: Not Found!

嵌套路由

如果我们想要在product组件中添加两个子路由,product-aproduct-a,可以在app-routing.module.ts中添加如下代码。

新建两个组件

1
2
ng generate component product-a
ng generate component product-b

在product页面添加router-outlet

这个router-outlet用来显示ProductA和ProductB对应的的组件。

1
2
3
<!-- product.component.html -->
<p>product works!</p>
<router-outlet></router-outlet>

添加子路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes: Routes = [
{path: 'home', component: HomeComponent},
{
path: 'product',
// pathMatch: 'full', 注意这一句不能写!否则子路由无法匹配,会导致404!
component: ProductComponent,
children: [
{path: 'a', component: ProductAComponent},
{path: 'b', component: ProductBComponent}
]
},
{path: 'about', component: AboutComponent},
{path: 'about-me', component: AboutmeComponent},
{path: '**', component: NotFoundComponent}
];

此时,如果在浏览器中输入http://localhost:4200/product/a,会显示ProductA works!,输入http://localhost:4200/product/b,会显示ProductB works!。同时也会显示product works!,因为product.component.html中除了有<router-outlet></router-outlet>。还有<p>product works!</p>

严格匹配

上面的例子中,product的path不能使用严格匹配选项pathMath: 'full',否则子路由无法匹配,会导致404。这是为什么呢?
因为匹配子路由的时候,必须先匹配父级路由,而由于父级路由使用了pathMatch: 'full',所以无法匹配,父级路由无法匹配导致子路由的匹配无法进行,所以导致了404!

如果你非要使用pathMatch: 'full',可以如下定义路由表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes: Routes = [
{path: 'home', component: HomeComponent},
{
path: 'product',
pathMatch: 'full',
component: ProductComponent,
},
{
path: 'product/a', component: ProductAComponent
},
{path: 'product/b', component: ProductBComponent},
{path: 'about', component: AboutComponent},
{path: 'about-me', component: AboutmeComponent},
{path: '**', component: NotFoundComponent}
];

这样就可以使用pathMatch: 'full'了。但是这么做也有一个弊端,就是a和b从逻辑上讲不再是product子路由了。而是平级路由,只不过路径中包含了字符串product而已。这会导致product页面的内容无法显示。通常父级路由都会显示一些公共信息,差异化的信息放到子路由中。如果你使用了pathMatch: 'full',则会导致父级页面内容无法显示。请酌情使用此方法。

注意,如果你使用了嵌套路由,或者lazy load, 请谨慎使用pathMatch: 'full'。详情请看这里:detail

路由拆分

如果product中的子路由越来越多,都放到一个文件中会显得很臃肿。这时候我们可以将子路由拆分到单独的文件中。将子路由定义到product组件中。

基于NgModule的路由拆分

进入product文件夹,执行以下命令。

1
2
ng generate module product-routing --flat
ng generate module product --flat

--flat选项表示不创建文件夹。

product-routing.module.ts中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// product-routing.module.ts
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProductBComponent} from "../product-b/product-b.component";
import {ProductAComponent} from "../product-a/product-a.component";
import {ProductComponent} from "./product.component";
import {RouterModule, Routes} from "@angular/router";

const routes: Routes = [
{
path: '', // 注意这里不能写product,因为`app-routing.module.ts`中已经定义了product路由, 并且指定了path: 'product'
component: ProductComponent,
children: [
{path: 'a', component: ProductAComponent},
{path: 'b', component: ProductBComponent}
]
}
];

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes),
],
exports: [RouterModule]
})
export class ProductRoutingModule {
}

product.module.ts文件中添加如下代码,导入ProductRoutingModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProductComponent} from './product.component';
import {ProductRoutingModule} from './product-routing.module';
import {ProductAComponent} from '../product-a/product-a.component';
import {ProductBComponent} from '../product-b/product-b.component';

@NgModule({
declarations: [
ProductComponent,
ProductAComponent,
ProductBComponent
],
imports: [
CommonModule,
ProductRoutingModule
]
})
export class ProductModule {
}

修改app-routing.module.ts
注意,这里必须使用loadChildren, 否则子路由无法显示。

1
2
3
4
5
// app-routing.module.ts
const routes: Routes = [
{path: 'product', loadChildren: () => import('./product/product.module').then(m => m.ProductModule)},
// ...
];

这样就可以将product中的子路由拆分到单独的文件中了。

基于Standalone组件的路由拆分

如果你的组件是standalone的,没有对应的module文件,那么可以使用如下方法拆分路由。注意loadChildren中加载的是路由文件,而不是module文件,也不是component文件。

1
2
// app.routers.ts
{path: 'product', loadChildren: () => import('./product/product.routers').then((routes) => routes.productRouters)},

在Product组件文件夹下新建路由文件product.routers.ts,添加如下代码。

1
2
3
4
5
6
7
8
9
// product.routers.ts
import {Routes} from "@angular/router";
import {ProductComponent} from "./product.component";
import {ProductDetailComponent} from "../product-detail/product-detail.component";

export const productRouters: Routes = [
{path: '', component: ProductComponent},
{path: 'product-detail', component: ProductDetailComponent},
]

注意这种方式下,点击product-detail链接时,会显示product-detail组件。product组件的内容不会显示。

如果要显示product组件的内容,需要将product-detail组件定义为product组件的子路由。如下所示。这种方式在导航时很方便,比如左侧是导航菜单,点击不同菜单显示不同的组件。对于整个页面来说,导航菜单一直显示,而右侧的内容会随着导航的变化而变化。

1
2
3
4
5
6
7
8
9
10
11
12
// product.routers.ts
import {Routes} from "@angular/router";
import {ProductComponent} from "./product.component";
import {ProductDetailComponent} from "../product-detail/product-detail.component";

export const productRouters: Routes = [
{
path: '', component: ProductComponent, children: [
{path: 'product-detail', component: ProductDetailComponent},
]
},
]

监听路由变化

如果我们想要在路由变化的时候做一些事情,比如打印路由信息,可以在app.component.ts中添加如下代码。此时会监听整个app所有路由的变化,如果只想监控某个组件的路由变化,那么可以在该组件中添监听代码。

1
2
3
4
5
6
7
// app.component.ts
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe((event) => {
console.log('Route:', event);
});
}

路由变化时会依次触发以下事件:

  • NavigationStart
  • RoutesRecognized
  • GuardsCheckStart
  • ChildActivationStart
  • ActivationStart
  • GuardsCheckEnd
  • ResolveStart
  • ResolveEnd
  • ActivationEnd
  • ChildActivationEnd
  • NavigationEnd

我们一般在NavigationEnd事件中做一些事情。

1
2
3
4
5
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
// do something
}
});

注意

即使使用了skipLocationChange选项,也可以通过以上方法获取完整的路由信息,因为skipLocationChange只是不改变浏览器地址栏的URL,但是路由信息还是会变化的。

获取路由信息

如果想要获取路由对应的url,路由中的参数等信息,可以在组件中注入ActivatedRoute服务。

使用ActivatedRoute.snapshot获取路由信息

1
2
// app.routers.ts
{ path: 'product/:id', component: ProductDetailComponent },
1
2
<!-- product.component.html -->
<p>This is product: {{id}}</p>
1
2
3
4
5
6
7
8
9
10
11
12
// product-detail.component.ts
export class ProductDetailComponent implements OnInit {
id: number = -1;
constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
const id = this.activatedRoute.snapshot.paramMap.get('id');
if (id) {
this.id = parseInt(id);
}
}
}

使用ActivatedRoute.params订阅路由变化

1
2
3
4
5
6
7
8
9
10
11
// product-detail.component.ts
export class ProductDetailComponent implements OnInit {
id: number = -1;
constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
this.activatedRoute.params.subscribe((params) => {
this.id = parseInt(params.id);
});
}
}

snapshop和params的区别:

  • snapshot只会获取一次路由信息,它是静态的,获取之后就不会再变化了。
  • params会订阅路由变化,当路由变化时,会重新获取路由信息。

params和queryParams的区别:

  • params: 获取路由中的参数,比如/product/1中的1
  • queryParams: 获取路由中的查询参数,比如/product?id=1&name=zdd中的id, name
  • matrixParameters: 获取路由中的矩阵参数,比如/product;id=1;name=zdd中的id, name

queryParams和matrixParameters的区别

  • queryParams: 使用?开头,使用&分隔每组参数,比如/product?id=1&name=zdd
  • matrixParameters: 使用;开头,使用;分隔每组参数,比如/product;id=1;name=zdd
  • 两者都以=分隔键值对。

路由守卫

待整理

路由传递数据

定义Router时发送数据

1
2
3
4
5
6
// app.routers.ts
{
path: 'product',
component: ProductComponent,
data: {category: 'digital'} // send data to component
},

组件中接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
const {category} = this.activatedRoute.snapshot.data;
console.log(category);

// or

this.activatedRoute.data.subscribe((data) => {
const {category} = data;
console.log(category);
})
}
}
1
this.router.navigate(['/product'], {state: {category: 'digital'}});

组件中接收数据

After Angular 16, must call this on constructor or you will get null, see here for detail

1
2
3
4
5
6
7
8
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private router: Router) {
const state = this.router.getCurrentNavigation()?.extras.state;
const {category} = state || {category: 'none'};
console.log(category);
}
}

定义routerLink时发送数据

1
<a [routerLink]="['/product']" [state]="{category: 'digital'}">Product</a>

组件中接收数据

1
2
3
4
5
6
7
8
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private router: Router) {
const state = this.router.getCurrentNavigation()?.extras.state;
const {category} = state || {category: 'none'};
console.log(category);
}
}

保持Fragment不丢失

Url中#后面紧跟的内容为Fragment,比如http://localhost:4200/book#b123中的b123就是Fragment

首先在Component中定义Fragment变量

1
2
3
export class AppComponent implements OnInit {
bookId = 'b123';
}

在routerLink中添加Fragment

1
<a [routerLink]="['book', {}]" [fragment]="bookId">Book</a>

在navigate中添加Fragment

1
this.router.navigate(['book'], {fragment: this.bookId});

激活后路由为:http://localhost:4200/book#b123

保持Fragment不变

当我们从一个带Fragment的路由跳转到其他路由时(比如辅助路由),Fragment会丢失。如果我们想要保持Fragment不变,可以在navigate中添加preserveFragment选项。

1
this.router.navigate([{outlets: {primary: 'book/book1/book2', detail: 'book-detail'}}, {preserveFragment: true}]);

此时路由变为:http://localhost:4200/book(detail:book-detail//list:book-list)#b123 - 注意Fragment在url末尾。

注意关闭辅助路由时,也要添加此选项,否则Fragment仍会丢失。

1
this.router.navigate([{outlets: {list: null}}], {preserveFragment: true}); 

此时路由变为:http://localhost:4200/book(detail:book-detail)#b123

1
this.router.navigate([{outlets: {detail: null}}], {preserveFragment: true});

此时路由变为:http://localhost:4200/book#b123

路由三要素

  1. 路由对应的路径及组件 - Routes配置文件
  2. 路由显示的位置 - Outlet
  3. 路由的触发方式
    • 点击触发,比如routerLink, button等。
    • 编程触发,比如navigatenavigateByUrl等。
    • 动态加载,比如动态创建组件并加载。

待商榷:

  1. 没有module时如何拆分router文件? - 可以直接使用路由module文件,不需要组件对应的module文件。目前standalone组件无法做到路由文件的拆分,必须有module支持。

https://www.angulararchitects.io/en/blog/routing-and-lazy-loading-with-standalone-components/