[Angular 基础] - routing 路由(上)
之前部分 Angular 笔记:
-
[Angular 基础] - 生命周期函数
-
[Angular 基础] - 自定义指令,深入学习 directive
-
[Angular 基础] - service 服务
终于到 routing 了……这部分的内容比我想象的要复杂很多,果然 Angular 的学习曲线不是开玩笑的 ¯\_(ツ)_/¯
基础页面布局
下面是一个简单的 wireframe,在没有实现路由时候的布局:
其中:
-
第一个模块对应的就是主页,一个非常简单的欢迎信息
-
第二个模块对应的是服务器管理
这里的实现是
edit
所属的模块与单独展示的server
平级 -
第二个模块对应的是用户信息展示
src/app/
├── home
├── servers
│ ├── edit-server
│ └── server
└── users└── user
结构如上所示
在没有实现路由功能的时候,可以结合前面学习的案例,采用 ngIf
+ services 去实现。
其主要逻辑是:
-
使用
ngIf
去判断当前应该渲染什么页面这个就需要在
app
层添加一个变量去控制当前展示的页面,实现一个 service 去管理对应的点击和更新事件 -
创建多个组件层级 services
如一个 service 去管理当前展示的 server,一个 service 去管理当前的 user
就像之前在案例项目 第一个 Angular 项目 - 添加服务 中实现的那样。不过这样的实现也有一点问题,比如实现会麻烦一些,或者无法根据网址访问对应的资源,如通过 domain/user_id
的方式访问对应的用户,有些验证方式也无法通过,如有些登录验证的方式是通过在 URL 后拼接一些 state id 的方式进行双向验证,这种多为第三方验证验证方式。
Angular 本身自带路由的实现
添加路由
创建一个新的 route module
这里的创建方式就是手动创建一个 TS 文件,文件名为 app-routing.module.ts
,实现方式如下:
const appRoutes: Routes = [{ path: '', component: HomeComponent },{path: 'users',component: UsersComponent,},{path: 'servers',component: ServersComponent,},
];@NgModule({imports: [RouterModule.forRoot(appRoutes)],exports: [RouterModule],
})
export class AppRoutingModule {}
随后在 app.module.ts
中导入 AppRoutingModule
:
@NgModule({declarations: [// ...],imports: [BrowserModule, FormsModule, AppRoutingModule],
})
export class AppModule {}
将路由单独拆分成一个 module 是为了代码的可读性,以及跟一下 SRP(Single Responsibility Principle),如果不拆分的话,直接将 appRoutes
定义在 app.module.ts
中,并且在 imports
中添加 RouterModule.forRoot(appRoutes)
也可以
这里代码的分析也比较简单,首先 Route
就是 Angular 定义好的类型:
上面这是 Angular 提供的最简单的配置,需要一个路径,一个组件,这两个是需要的最基础的配置。children 也 是可选项,代表着子组件(nested component),这里后面会说。
forRoot()
会创建一个新的,包含所有提供的鹿筋和指令的 ngModule,其语法为:
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterModule>
可以看到这是一个静态函数,换言之,这也是一个 singleton
使用 route
渲染对应 router
这里需要更新的是 V 层,抛除一些样式的上的内容,核心部分的代码如下:
<ul class="nav nav-tabs"><lirole="presentation"routerLinkActive="active"[routerLinkActiveOptions]="{ exact: true }"><a routerLink="/">Home</a></li><li role="presentation" routerLinkActive="active"><a [routerLink]="['/servers']">Servers</a></li><li role="presentation" routerLinkActive="active"><a [routerLink]="'/users'">Users</a></li>
</ul><router-outlet></router-outlet>
这里总共有这么几个需要注意的点:
-
routerLinkActive
routerLinkActive
也是一个指令,它会动态的添加指定的类名,当前情况下这个类名就是active
,展示效果如下:可以看到,随着 nav link 的变动,Angular 也会自动修改对应的类名——增添或是删除
active
-
routerLinkActiveOptions
这是比较经常搭配使用
routerLinkActive
的指令,比较常见的选项是[routerLinkActiveOptions]="{ exact: true }"
,这样可以保证浏览器的路径和路由提供的 URL 100% 一致时,才会增加对应的 active class如果不加的话,所有的路径都会 match
/
这个路径,因此就会出现两个 active tabs 的情况: -
routerLink
routerLink
取代了href
,通过href
进行定位的方式会导致整个页面重新刷新,从而丢失掉所有的状态——这点和 React 是一样的这是配置 path 的方法,我这里一共显示了 3 种写法
-
routerLink="/"
语法糖缩写,和下一种写法一致,具体在 [Angular 基础] - 自定义指令,深入学习 directive 有提到过
-
[routerLink]="'/users'"
这是在 path 比较简单的情况下使用,直接提供一个字符串即可
-
[routerLink]="['/servers']"
这是一个比较常见的用法,主要可以用来比较方便的接受静态数据
以
/users/user
为例,-
[routerLink]="'/users/user'"
会生成一个静态路径,即永远都是/users/user
如果想要生成动态路径,那么就需要使用
+
做拼接 -
[routerLink]="['/users', user]"
会生成一个动态路径,如user
是一个变量名,那么 Angular 就会获取对应的变量,并拼接出对应的路径也就是说,生成的路径名可能是
/users/user
,也有可能是/users/user1234
-
-
-
router-outlet
这就是一个 placeholder,当 Angular 完成渲染后,它会动态加载对应的组件
也就取代之前提到的用
ngIf
渲染的 template
编程式导航
这个情况为需要在组件内触发一些事件后进行重定向,如在登陆后重新导航到首页这种重定向操作
这里的案例为在 Home 页面通过点击事件定向到其他的页面,V 层修改如下:
<button class="btn btn-primary" (click)="onLoadServers()">To Server Page
</button>
VM 层实现:
export class HomeComponent implements OnInit {private servers: { id: number; name: string; status: string }[] = [];constructor(private router: Router, private route: ActivatedRoute) {}onLoadServers() {this.router.navigate(['servers'], {relativeTo: this.route,queryParams: { allowEdit: '1' },fragment: 'loading',});}
}
constructor 中的内容通过 dependency injection 实现,这部分具体可以查看 [Angular 基础] - service 服务 这篇笔记,这里不多赘述。这里的 Router
和 ActivatedRoute
都是 Angular 提供用于导航的 service
其中:
-
Router
是导航及历史记录的相关服务
对应的 React Hook 有
useHistory
/useNavigate
-
ActivatedRoute
顾名思义,这是对当前的 active route 进行的封装,可以通过这个 service 轻松获取当前的 path 以及包含的相关数据
对应的 React Hook 有
useLocation
,useParams
,useMatch
,useLoaderData
这里的点击事件触发的就是重定向到 servers
这个路径去,注意这里采用的是相对路径,Angular 的路由可以接受绝对路径,也可以接受相对路径,甚至还可以使用 ../
这样的相对路径。后面的参数则是定向的路由配置:
-
relativeTo: this.route
这里指的是导航的地址所参考的路径,如当前为
/
,那么路径拼接的就是/servers
。如果当前路径是/servers
,那么拼接的路径就是/servers/servers
使用相对路径时,一定要使用
relativeTo
,因为Router
不知道当前路径在哪里。当没有接收到relativeTo
时,Angular 会将所有的路径默认为绝对路径 -
queryParams: { allowEdit: '1' }
这就是添加 query parameter 的地方
-
fragment: 'loading'
fragment 为
#some_value
,一般用来定向到 HTML 页面中的某一个id
上去
定向效果为:
动态接受路径数据
上面一个 section 提到了相对路径和动态修改路径,这里继续实操一下,修改的是 servers component。
V 层修改如下
<div class="row"><div class="col-xs-12 col-sm-4"><div class="list-group"><a[routerLink]="['/servers', server.id]"[queryParams]="{ allowEdit: server.id === 3 ? '1' : '0' }"fragment="loading"class="list-group-item"*ngFor="let server of servers">{{ server.name }}</a></div></div>
</div>
VM 层不需要修改就此跳过,这个时候点击路径会发现没有任何的变化:
但是查看 HTML 元素又能发现,router-link
中是有值的。这是因为当前 Angular 的 routing 只针对 /servers
进行了处理,但是并没有对 /servers/id
进行处理,因此这里需要修改一下 app-routing module:
const appRoutes: Routes = [// 其余不变{path: 'servers/:id',component: ServerComponent,},
];
其中 :id
代表的是一个动态变量
这时候就能成功实现重定向:
这个时候的数据显示是不完整的,如果想在在 server component 中获取对应的 server 数据,则需要使用到 ActivatedRoute
这个 service,VM 层修改如下:
export class ServerComponent implements OnInit {server: { id: number; name: string; status: string };constructor(private serversService: ServersService,private route: ActivatedRoute,private router: Router) {}ngOnInit() {this.server = this.serversService.getServer(parseInt(this.route.snapshot.params.id));}
}
其中 serversService
只是用来获取当前 server 数据的一个 service,具体实现这里不会提及
实现后效果如下:
这里可以发现,数据已经可以正常渲染了
这里需要注意的是这个 snapshot
会获取当前路由的状态,其包含的数据如下:
这里获取的 id
对应的就是 path: 'servers/:id'
中的 :id
,也是对 routerLink 中的 ['/servers', server.id]
,之前的 section 提到过,使用数组传参数,数组中的值可以是字符串,也可以是变量,Angular 会自动拼接变量的值到路由中去。
同样,这里也可以注意到 navigation 中的 Servers
还是处于 active 的状态,这也是因为没有实现 exact: true
,Angular 在匹配字符串的时候,发现当前路径与 /servers
可以匹配,因此还是会添加 active
这一类名到对应的元素上
动态更新路由数据
现在更新一下 VM 层,更新如下:
<h5>{{ server?.name }}</h5>
<p>Server status is {{ server?.status }}</p><!-- <button class="btn btn-primary" (click)="onEdit()">Edit Server</button> --><div class=""><a [routerLink]="['/servers', 2]">Click me to server 2</a>
</div>
主要是新增加了一个超链接,然后完成重定向到 /servers/2
的实现,效果如下:
可以看到,路径是从 http://localhost:4200/servers/1?allowEdit=0#loading
变成了 http://localhost:4200/servers/2
,但是数据却没有任何的更新。
造成这个的原因是,对于 Angular 来说,当前的页面没有重新渲染——url 仍然是 /servers/:id
,因此当前组件不会重新经历一个 销毁 --> 新建
的过程,自然 ngOnInit
并没有重新被触发,数据自然也不会完成对应的更新。
想要解决这个问题,就需要 subscribe ActivatedRoute
的数据变化,在每次 ActivatedRoute
的数据更新时,也需要更新组件内的数据。
这里实现如下:
export class ServerComponent implements OnInit {ngOnInit() {this.server = this.serversService.getServer(parseInt(this.route.snapshot.params.id));console.log(this.route.snapshot);this.route.params.subscribe((params: Params) => {this.server = this.serversService.getServer(parseInt(params.id));});}
}
实现后效果如下:
这个实现会在每一次 this.route.params
产生变动时,更新 this.server
。另外从实践上来说,这里最好在 ngOnDestroy
里去 unsubscribe 去防止内存泄露,不过因为 ActivatedRoute
是 Angular 提供的 service,Angular 会在组件被销毁的时候自动 unsubscribe。
如果是自己实现的 service,那就 一定 要做好对应 unsubscribe 的处理
这里涉及到了 Observable,后面会有专门的部分复习回顾一下 Observable……虽然之前也有笔记写过 rxjs 的 Observable,大概了解过这个的用法