angular2 学习笔记 ( Router 路由 )
更新 : 2024-08-21
请看新版本:
Angular 18+ 高级教程 – Routing 路由 (原理篇)
更新 : 2019-11-21
我们经常喜欢用 empty string 来做 default router path
比如 2 个 tabs
<nav mat-tab-nav-bar> <a mat-tab-link [routerLink]="['./']" routerLinkActive [routerLinkActiveOptions]="{exact: true}" #rla1="routerLinkActive" [active]="rla1.isActive"> Test A {{ rla1.isActive }} </a> <a mat-tab-link [routerLink]="['test-b']" routerLinkActive #rla2="routerLinkActive" [active]="rla2.isActive"> Test B {{ rla2.isActive }} </a> </nav>
要留意第一个如果忘记放 exact: true, 那么第二个被匹配到的时候它也会 active 哦,所以使用 empty path + router active 时要记得了
注意 : router link 配 empty string 还要有 relation 写法是
<a routerLink=“./” >go</a>
更新: 2019-08-05
之前都没有什么用到 matrix url, 因为项目难度不大,用 queryparams 挺好的,但是越来月复杂后开始感觉到 matrix 的好了.
要改动 matrix 比较难, 没有类似 'merge' 啊等等的东西.
常用的方式是
<a [routerLink]="['../',{}]" >gogo</a>
不过要留意哦, root path 就不可以用这招了,root path should not have matrix.
然后说到 ../ 我就气!
https://github.com/angular/angular/issues/13011 这个 issue 几年前我就 like 了. 然后我也不记得那时候怎样闪掉了.
今天又给我与到... 我根本就不记得有过这种事情了... n 年前... 尽然没有 fix !!! 什么鬼嘛
看了几眼原来是有 fix 的.. 只是 fix 的不全面而已
relativeLinkResolution?: 'legacy' | 'corrected' 就是用来解决这类问题的 (问题不只一种)
幸好我这次遇到的可以用这个 fix. 希望不要再有问题了。
Angular 的 Router 是真的很强, 但是也很多坑 ! 一堆问题没人理.. 唉..
更新 : 2018-7-25
Base href 的奇妙
默认情况下, ng 会为每个项目加上一个 base href="/"
<base href="/">
依据规范, base href 的结尾必须是 slash "/" 比如 "/en/", "/cn/"
看看下面的情况
可以获取图 <base href="/"> <img src="/images/stickman.gif" width="24" height="39"> 可以获取图 <base href="/"> <img src="images/stickman.gif" width="24" height="39"> 不可以获取图 <base href="/images/"> <img src="/stickman.gif" width="24" height="39"> 可以获取图 <base href="/images/"> <img src="stickman.gif" width="24" height="39">
是不是有点奇葩... 反正 best pratice 就是一定要写 base href, 如果没有特别的就放 "/"
所有路径都不要使用 slash "/" 作为开始, 这样就安全了.
要在 ng 里获取 base href 的值可以注入服务 PlatformLocation.getBaseHrefFromDOM()
更新 : 2017-12-19
今天遇到一个 关于 router empty path + relative 的 bug
https://github.com/angular/angular/issues/18059
https://github.com/angular/angular/issues/13011
https://github.com/angular/angular/issues/17957
也可能这个不是 bug 是 ng 的设计思路.
总只能结果就是如果你想在 component 内使用 relative routerLink 比如 "../" or "./" or "child-path"
那么请你确保你这个 component 所在的 router 一定要有 parent path. 如果所有 parent path 都是 empty string 那么你就 gg.com 了.
1 imports: [RouterModule.forRoot([ 2 { path: '', pathMatch: 'full', redirectTo: '/products' }, 3 { 4 path: '', 5 children: [ 6 { 7 path: '', 8 component: HomeComponent, 9 children: [{ 10 path: '', 11 component: CategoryComponent, // <a routerLink="products" >category go</a> 《--result is products/(products) .... 12 children: [ 13 { 14 path: 'products', 15 component: ProductComponent 16 } 17 ] 18 }] 19 } 20 ] 21 } 22 ])],
下面这个就 ok
imports: [RouterModule.forRoot([ { path: '', pathMatch: 'full', redirectTo: 'dada/products' }, { path: '', children: [ { path: 'dada', // 要有 parent path component: HomeComponent, children: [{ path: '', component: CategoryComponent, // <a routerLink="products" >category go</a> OK ! children: [ { path: 'products', component: ProductComponent } ] }] } ] } ])],
更新 : 2017-11-04
lazy load vs common chunk
by default ng 会把共用的模块放入 common chunk 里头, 确保即使在 lazy load 的情况下, 模块都不会被加载 2 次 (不重复)
但是这样的设置并不一定就最好的,因为 lazy load 的目的本来就是最小化的加载丫.
这只是一个平衡的问题. ng 视乎只给了 2 个极端的做法, 要嘛使用 common chunk 要嘛完全不要 common chunk
ng build --prod --no-common-chunk
更新 : 2017-10-18
angular 的 router 有一个原则, 如果你触发一个 <a href> 或则调用 router.navigate(...) 但是最终它发现 url 没变动,那么什么不会发生, route event 统统没有运行.
还有另一个是当 url change 时 angular 不会轻易 rebuild component, 如果它的 path 依然是激活的 angular 会保留它哦.
更新 : 2017-08-04
今天我才发现其实 Preload 不像我说的那样, 我们可以在 preload 方法中把 load 这个方法保持起来.
export class AppCustomPreloader implements PreloadingStrategy { loaders: Function[] = []; preload(route: Route, load: Function): Observable<any> { this.loaders.push(load); return Observable.of(null); } }
然后在任何一个 component 注入 AppCustomePreloader 并调用 load 就可以了.
ng 是很聪明的,如果 load() 运行时发现其模块已经加载了, 那么它并不会报错. 所以鼓励大家去使用它 .
更新 : 2017-04-05
this.router.navigate(['../../'], { relativeTo: this.activatedRoute, queryParamsHandling: 'preserve' });
<a routerLink="../../" queryParamsHandling="preserve">
queryParamsHandling and preserveFragment 可以在移动 router 时保留当前的 queryParams and fragment 很方便哦。
queryParamsHandling 不只是可以 preserve, 还可以 merge 哦
更新 : 2017-03-27
matcher
如果我想自己写 url 匹配, 我们可以通过 matcher
export function matcher(segments: UrlSegment[], group: UrlSegmentGroup, route: Route) { //判断 return { consumed: segments.slice(1), //如果你的 route 还有 child 的话,这里要注意,只放入你所匹配到的范围,后面的交给 child 去判断. posParams: { Id: segments[1] } // 传入 params, url matrix 等等 } } @NgModule({ imports: [RouterModule.forChild([ { matcher : matcher, //帮发放放进来, 这里不要使用匿名方法或则箭头函数哦, aot 不过 component : FirstComponent, children : [] } ])], exports: [RouterModule], }) export class DebugRoutingModule { }
更新 2017-03-05
preloading module
lazy load 的好处是 first load 很快, 因为很多 module 都没有 load 嘛, 但是后续的操作就会变成卡卡的, 因为后来要 load 嘛.
2.1 开始 ng 支持 preloading 的概念. 就是通过 lazyload 让你 firstload 很快之后, 立马去预先加载其它的 module.
比如你的 first page 是一个登入界面, 用户就可以很快看到页面,让后乘着客户在输入密码时,你偷偷去加载后续会用到的模块。这样客户接下来的操作就不会卡卡的了.
这听上去不错哦.
要注意的是, ng 并不太智能, 它不会等到 browser idle 的时候才去加载. 它会马上去加载.
如果你的首页有很多图片或者视屏, ng 不会等待这些图片视屏加载完了才去加载其它模块, 它会马上去加载. 这可能会造成一些麻烦 (因项目而定, 自己做平衡哦)
@Injectable() export class MyPreloadingStrategy implements PreloadingStrategy { constructor(private route : ActivatedRoute, private router : Router) { //可以注入 router route 任何 service 来帮助我们判断也不要 preload } preload(route: Route, load: () => Observable<any>): Observable<any> {
// ng 会把每一个 lazyload 的 module 丢进来这个函数, 问问你是否要 preload, 如果要, 你就返回 load() 不要 preload 的话就返回 Observable.of(null); return (true) ? load() : Observable.of(null); } } @NgModule({ imports: [RouterModule.forRoot([ { path: 'home', loadChildren: "app/+home/home.module#HomeModule" }, { path: "about", loadChildren: "app/+about/about.module#AboutModule" }, { path: "contact", loadChildren: "app/+contact/contact.module#ContactModule" }, { path: "", redirectTo: "home", pathMatch: "full" } ], { preloadingStrategy: MyPreloadingStrategy })], // { preloadingStrategy: PreloadAllModules } <--ng 自带的
exports: [RouterModule], providers: [MyPreloadingStrategy] }) export class AppRoutingModule { }
只要在 forRoot 里添加 preloadingStrategy 就可以了. 上面我用了一个自定义的处理, 如果你想简单的表示全部都预加载的话,可以使用 ng 提供的 PreloadAllModules
更新 2017-01-29
提醒 :
路由是有顺序的, 在用 import 特性模块时, 位置要留意.
例如, 如果你 app-routing 最后是处理 404
但是在 app-module 却把 routing 限于特性模块 IdentityModule, 那么 IdentityModule 的 routing 就进不去了。因为已经被匹配掉了.
2016-08-26
参考 :
https://angular.cn/docs/ts/latest/guide/router.html#!#can-activate-guard
https://angular.cn/docs/ts/latest/api/ -@angular/router 部分
ng 路由的概念和游览器类似, 和 ui-router 也类似, 下面会把具体功能逐一解释
1. html5 和 hash #
ng 默认模式是 html5, 在开发阶段我们喜欢使用 hash 模式, 这样可以不用部署服务器.
要从 html5 转换成 hash 只要做一个小设定 :
(update:用 angular cli 开发的话,不需要 hash 模式了.)
2.child
和 ui-view 一样 ng 也支持嵌套
就是一个路由的组件模板内又有另一个路由组件
const appRoutes: Routes = [ { path: "", redirectTo: "home", pathMatch: "full" }, { path: "home", component: TopViewComponent, //view 内也有 <router-outlet> children: [ { path: "" //如果没有设置一个空路由的话, "/home" 会报错, 一定要 "/home/detail" 才行. }, { path: "detail", component: FirstChildViewComponent } ] } ];
3. 获取 params ( params 是 Matrix Url 和 :Id , 要拿 search 的话用 queryParams )
class TestComponent implements OnInit, OnDestroy { //home/xx private sub : Subscription; constructor(private route: ActivatedRoute) { } ngOnInit() { //监听变化 this.sub = this.route.params.subscribe(params => { console.log(params); //{ id : "xx" } }); //如果只是要 get 一次 value, 用快照 console.log(this.route.snapshot.params); //{ id : "xx" } } ngOnDestroy() { this.sub.unsubscribe(); //记得要取消订阅, 防止内存泄露 (更新 : 其实 ActivatedRoute 可以不需要 unsubscribe,这一个 ng 会智能处理,不过养成取消订阅的习惯也是很好的) } }
4. 导航
导航有个重要概念, 类似于游览器, 当我们表示导航时 ../path, /path, path 它是配合当下的区域而做出相对反应的. 记得是 path+区域 哦.
<a [routerLink]="['data',{ key : 'value' }]" [queryParams]="{ name : 'keatkeat' }" fragment="someWhere" >go child</a>
export class TopViewComponent { constructor(private router: Router, private route: ActivatedRoute) { console.clear(); } click(): void { this.router.navigate( ["data", { key: "value" }], //data 是 child path, {key : "value"} 是 Matrix Url (矩阵 URL) 长这样 /data;key=value { relativeTo: this.route, //表示从当前route开始, 这个只有在 path not start with / 的情况下需要放. //一般的 queryParams, 这里只能 override 整个对象, 如果你只是想添加一个的话,你必须自己实现保留之前的全部. queryParams: { 'name': "keatkeat" // ng 会对值调用 toString + encode 才放入 url 中, 解析时会 decode, 然后我们自己把 str convert to 我们要的值 }, //#坐标 fragment: "someWhere",
replaceUrl : true //有时候我们希望 replace history 而不是 push history } ); //redirect by url let redirectUrl = this.route.snapshot.queryParams["redirectUrl"]; if (redirectUrl != undefined) { this.router.navigateByUrl(redirectUrl); } else { this.router.navigate(["/admin"]); } } }
大概长这样, 最终个结果 : /home/data;key=value?name=keatkeat#someWhere
通过指令 routerLink , 或则使用代码都可以 (写法有一点点的不同, 看上面对比一下吧)
导航使用的是数组, ["path1","path2",{ matrix : "xx" },"path3"], 你也不一定要一个 path 一个格子, ['/debug/a/b','c'] 也是 work 的
和游览器类似
"/path" 表示从 root 开始
"../path" 表示从当前route往上(parent)
"path" 表示从当前往下(child)
这里有个关键概念, 在不同的 component 获取到的 this.route 是不同的, 组件和 route 是配合使用的
比如上面 click() 方法,如果你放在另一个组件,结果就不同了,this.route 会随着组件和改变.
没有 route name 的概念 (之前好像是有,不知道是不是改了..没找到../.\), 就是用 path 来操作.
matrix url 和 params 获取的手法是一样的. 他的好处就是不需要把所有子孙层页面的参数都放到 params 中,放到 matrix url 才对嘛.
提醒 : { path : "product?name&age" } 注册 route 的时候不要定义 queryParam. ?name&age 删掉 (ui-router need, ng not)
5. 拦截进出
通常进入时需要认证身份,出去时需要提醒保存资料.
ng 为我们提供了拦截点
{ path: ":id", component: TestComponent, data: { title : "test" }, canActivate: [BlockIn], //进入 canDeactivate : [BlockOut] //出去 }
BlockIn, BlockOut 分别是2个服务
@Injectable() export class BlockIn implements CanActivate { constructor( private currentRoute: ActivatedRoute, private router: Router, ) { } canActivate(nextRoute: ActivatedRouteSnapshot, nextState: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { //this.currentRoute vs nextRoute some logiz let nextUrl = nextState.url; let currentUrl = this.router.url; return new Promise<Boolean>((resolve, reject) => { setTimeout(() => { resolve(true); },5000); }); } }
实现了 CanActivate 接口方法, 里头我们可以获取到即将进入的路由, 这样就足够让我们做验证了, 途中如果要跳转页是可以得哦..
@Injectable() export class BlockOut implements CanDeactivate<TestComponent> { canDeactivate( component: TestComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | Promise<boolean> | boolean { console.log("leave"); return true; } }
CanDeactivate 还多了一个 Component, 让我们在切出的时候可以调用 component 内容来做检查或者保存资料等等, 很方便呢.
还有一个叫 CanActivateChild, 依据子层来决定能否访问... 它和 CanActivate 的区别是
canActivate(nextRoute: ActivatedRouteSnapshot) vs canActivateChild(childRoute: ActivatedRouteSnapshot)
有点像 dom 事件的 event.target vs event.currenttarget 的概念.
6. resolve
@Injectable() export class DataResolve implements Resolve<String> { constructor(private router: Router) { } resolve(route: ActivatedRouteSnapshot): Observable<any> | Promise<any> | any { console.log("masuk"); if ("xx" === "xx") { return "xx"; } else { this.router.navigate(['/someWhere']); //随时可以跳转哦 } } }
{ path: "home", component: TopViewComponent, resolve: { resolveData: DataResolve }, }
注册在 route 里
providers: [appRoutingProviders, Title, BlockIn, BlockOut, DataResolve],
还要记得注册服务哦
ngOnInit() { console.log("here"); console.log(this.route.snapshot.data); }
在 onInit 里面就可以使用啦.
提醒 : 和 ui-router 不同的时, ng 的 resolve 和 data 是不会渗透进子路由的,但是我们在子路由里调用 this.route.parent.... 来获取我们想要的资料.
7. set web browser title
这个和 router 没有直接关系,只是我们通常会在还 route 时改动 browser title 所以记入在这里吧
ng 提供了一个 service 来处理这个title
import { BrowserModule, Title } from '@angular/platform-browser'; providers: [appRoutingProviders, Title, BlockIn, BlockOut, DataResolve] constructor(private route: ActivatedRoute, private titleService: Title) { this.titleService.setTitle("data"); }
注册 & 注入 service 就可以用了
8.auxiliary routes / multi view
参考 :
const appRoutes: Routes = [ { path: "home", children: [ { path: "detail", }, { path: "popup", outlet : "popup" } ] }, { path: "chat", outlet : "chat" } ];
结构提醒 :
根层是 /home(chat:chat) 而不是 /(home//chat:chat)
子层是 /home/(detail//popup:popup) 而不是 /home/detail(popup:popup)
要留意哦.
// create /team/33/(user/11//aux:chat) router.createUrlTree(['/team', 33, {outlets: {primary: 'user/11', right: 'chat'}}]); // remove the right secondary node router.createUrlTree(['/team', 33, {outlets: {primary: 'user/11', right: null}}]);
生成的方式.
9.异步加载特性模块
首先特性模块和主模块的 routing 设置不同
一个用 .forRoot, 一个用 .forChild 方法
export const routing: ModuleWithProviders = RouterModule.forChild(routes);
要异步加载特性模块的话,非常简单.
在主路由填入 loadChildren 属性,值是模块的路径#类的名字
const appRoutes: Routes = [ { path: "", redirectTo: "/home", pathMatch: "full" }, { path: "home", component: HomeComponent }, { path: "product", loadChildren: "app/product/product.module#ProductModule" //ProductModule 是类的名字, 如果是用 export default 的话,这里可以不需要表明 } ];
在特性模块的路由, 把 path 设置成空
const routes: Routes = [ { path: "", component: ProductComponent } ];
这样就可以啦.
小记:
1.Router : 用于 redirect 操作
2.ActivateRoute : 用于获取 data, params 等等
3.Route : 就是我们每次注册时写的资料咯, 里面有 data, path 哦