本文讲解一下如何在Angular
项目中使用router
。
路由配置(NgModule based apps)
创建一个Angular项目
1 | ng new angular-router |
在创建过程中,Angular CLI
会询问是否使用router,选择Yes
。
创建三个组件。
使用WebStorm
或者VSCode
打开上面创建的项目,呼出Terminal
,执行以下命令。
1 | ng generate component home |
然后打开app-routing.module.ts
文件,创建路由。注意,path不能以/
开头。
1 | // app-routing.module.ts |
添加导航链接
打开app.component.html
文件,添加导航链接。注意,routerLink
的值要和path
一致。如果是根路由,可以使用/
。文档末尾的router-outlet
是用来显示路由对应的组件的。当点击某一个链接的时候,Angular会找到对应的组件,并将其加载到router-outlet
中。所以router-outlet
相当于一个占位符,与html中的slot
类似。
1 | <h1>Angular Router Demo</h1> |
运行项目
在Terminal
中执行以下命令。打开浏览器,输入http://localhost:4200
,就可以看到导航链接了。依次点击页面上的Home,Product,About链接,可以看到页面跳转了,并且浏览器中的地址也发生了变化。
1 | ng serve |
基于ROUTES
的路由配置
ROUTES
是Angular内置的一个Injection Token,在一些比较老的Angular app中,也有使用ROUTES
来配置路由的,现举例如下:
1 | ({ |
useFactory
后面跟的是一个工厂函数,这个函数我们直接返回一个路由数组。
使用这种方法后,我们就不需要路由文件app-routing.module.ts
了,可以将它删除掉。
路由配置(Standalone apps)
基于standalone
组件的app,路由配置稍有不同。
- 在
main.ts
文件中启动app时会传递appConfig
进去,我们的路由配置就在这个appConfig
中。
1 | // main.ts |
- 在
app.config.ts
文件中通过provideRouter
函数提供路由。
1 | // app.config.ts |
- 在
app.routes.ts
文件中定义路由。
1 | // app.routes.ts |
可见,路由的结构是十分清晰的。
添加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 | // app-routing.module.ts |
此时,如果在浏览器里面输入一个不存在的路径,比如http://localhost:4200/abc
,则会显示404: Not Found!
。
嵌套路由
如果我们想要在product
组件中添加两个子路由,product-a
和product-a
,可以在app-routing.module.ts
中添加如下代码。
新建两个组件
1 | ng generate component product-a |
在product页面添加router-outlet
这个router-outlet
用来显示ProductA和ProductB对应的的组件。
1 | <!-- product.component.html --> |
添加子路由
1 | const routes: Routes = [ |
此时,如果在浏览器中输入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 | const routes: Routes = [ |
这样就可以使用pathMatch: 'full'
了。但是这么做也有一个弊端,就是a和b从逻辑上讲不再是product子路由了。而是平级路由,只不过路径中包含了字符串product
而已。这会导致product页面的内容无法显示。通常父级路由都会显示一些公共信息,差异化的信息放到子路由中。如果你使用了pathMatch: 'full'
,则会导致父级页面内容无法显示。请酌情使用此方法。
注意,如果你使用了嵌套路由,或者lazy load, 请谨慎使用pathMatch: 'full'
。详情请看这里:detail
路由拆分
如果product中的子路由越来越多,都放到一个文件中会显得很臃肿。这时候我们可以将子路由拆分到单独的文件中。将子路由定义到product组件中。
基于NgModule的路由拆分
进入product
文件夹,执行以下命令。
1 | ng generate module product-routing --flat |
--flat
选项表示不创建文件夹。
在product-routing.module.ts
中添加如下代码。
1 | // product-routing.module.ts |
product.module.ts文件中添加如下代码,导入ProductRoutingModule
。
1 | import {NgModule} from '@angular/core'; |
修改app-routing.module.ts
注意,这里必须使用loadChildren, 否则子路由无法显示。
1 | // app-routing.module.ts |
这样就可以将product中的子路由拆分到单独的文件中了。
基于Standalone组件的路由拆分
如果你的组件是standalone的,没有对应的module文件,那么可以使用如下方法拆分路由。注意loadChildren
中加载的是路由文件
,而不是module文件,也不是component文件。
1 | // app.routers.ts |
在Product组件文件夹下新建路由文件product.routers.ts
,添加如下代码。
1 | // product.routers.ts |
注意这种方式下,点击product-detail链接时,会显示product-detail组件。product
组件的内容不会显示。
如果要显示product
组件的内容,需要将product-detail
组件定义为product
组件的子路由。如下所示。这种方式在导航时很方便,比如左侧是导航菜单,点击不同菜单显示不同的组件。对于整个页面来说,导航菜单一直显示,而右侧的内容会随着导航的变化而变化。
1 | // product.routers.ts |
监听路由变化
如果我们想要在路由变化的时候做一些事情,比如打印路由信息,可以在app.component.ts
中添加如下代码。此时会监听整个app所有路由的变化,如果只想监控某个组件的路由变化,那么可以在该组件中添监听代码。
1 | // app.component.ts |
路由变化时会依次触发以下事件:
- NavigationStart
- RoutesRecognized
- GuardsCheckStart
- ChildActivationStart
- ActivationStart
- GuardsCheckEnd
- ResolveStart
- ResolveEnd
- ActivationEnd
- ChildActivationEnd
- NavigationEnd
我们一般在NavigationEnd
事件中做一些事情。
1 | router.events.subscribe((event) => { |
注意:
即使使用了skipLocationChange
选项,也可以通过以上方法获取完整的路由信息,因为skipLocationChange
只是不改变浏览器地址栏的URL,但是路由信息还是会变化的。
获取路由信息
如果想要获取路由对应的url,路由中的参数等信息,可以在组件中注入ActivatedRoute服务。
使用ActivatedRoute.snapshot
获取路由信息
1 | // app.routers.ts |
1 | <!-- product.component.html --> |
1 | // product-detail.component.ts |
使用ActivatedRoute.params
订阅路由变化
1 | // product-detail.component.ts |
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 | // app.routers.ts |
组件中接收数据
1 | // product.component.ts |
navigation
时发送数据
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 | // product.component.ts |
定义routerLink
时发送数据
1 | <a [routerLink]="['/product']" [state]="{category: 'digital'}">Product</a> |
组件中接收数据
1 | // product.component.ts |
保持Fragment不丢失
Url中#
后面紧跟的内容为Fragment
,比如http://localhost:4200/book#b123
中的b123
就是Fragment
。
首先在Component中定义Fragment变量
1 | export class AppComponent implements OnInit { |
在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
路由三要素
- 路由对应的路径及组件 - Routes配置文件
- 路由显示的位置 - Outlet
- 路由的触发方式
- 点击触发,比如
routerLink
,button
等。 - 编程触发,比如
navigate
,navigateByUrl
等。 - 动态加载,比如动态创建组件并加载。
- 点击触发,比如
待商榷:
- 没有module时如何拆分router文件? - 可以直接使用路由module文件,不需要组件对应的module文件。目前standalone组件无法做到路由文件的拆分,必须有module支持。
https://www.angulararchitects.io/en/blog/routing-and-lazy-loading-with-standalone-components/