Angular实现多标签页效果(路由重用)
1.需求
做了几年的MES系统,从ASP.NET WebForm至MVC,系统决定了用户界面必须为标签页方式实现,因为用户在进行一项操作的时候很有可能会进行其它的操作,比如查询之类的。如果按MVC的方式每个页面都去刷新界面的话用户体验就太差了,所以一直以来都是用的多标签页方式,在WebForm或者MVC框架中都是使用的iframe来实现的,网上找了一个H+的图,就是类似的效果。
2.寻找解决方案
虽然用iframe效果是实现了,但是iframe这种缺点也很明显:
1.加载页面所有的js,css都要全部再加载一遍(虽然有缓存)
2.与主页面交互麻烦,比如弹出一个Dialog,在iframe里面弹不美观,在外层主页面弹获取数据比较麻烦
说了这么多废话大家发现都没说到Angular的内容,不要着急,现在进入主题。最近在看MVVM前端框架,在目前流行的几个框架里我选择了Angular(别问我原因,我能说一整天....),发现MVVM框架来实现上面的效果应该不错。优点嘛就是能解决上面这些个缺点:)
在把官方的教程写了一遍,看了两天的教学视频后开始动手写实现代码,UI方面很简单,因为用的Metronic,所以标签页的样式我借鉴了H+,具体UI的样式和HTML之类的代码我就不放出来了,这个挺简单的,主要是Angular路由的处理。开始的想法是用子路由,页面中多个router-outlet加name来实现,但是深入了解了路由后发现其实是进了死胡同,因为根本实现不了,点击导航跳转页面路由肯定是会变更的,相当是跳转到一个新的页面了,于是在网上找了找有没有相关的解决方案。在找了很久以后终于在园里子发现了一篇文章:
http://www.cnblogs.com/lslgg/p/7700888.html 这里要特别感谢下:smiles 提供给的思路。就是利用路由的重用策略来实现 。
Angular路由重用网上资料也挺多的,因为接触Angular不久,所以没想到这块,具体原理我就不说了,大家可以查资料。简单的来说就是在路由跳转的时候可以记录下路由当时的快照,然后将快照存放起来,等你下次重新打开这个路由的时候再从快照里取出来显示原来的界面,当然其中的逻辑是自己写的,想怎么写都行。
3.代码实现
撸起袖子就是干,smiles大神已经把路由重用的代码写好了,我直接复制了下来,然后另外写了一个标签页管理的组件来实现多标签页管理,这里代码我也先不发了,因为大多是从smiles大神的博客里复制过来的,大家要看代码可以点我上面发的链接。我就贴一点主要的代码:
import { RouteReuseStrategy, DefaultUrlSerializer, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router'; export class SimpleReuseStrategy implements RouteReuseStrategy { public static handlers: { [key: string]: DetachedRouteHandle } = {} /** 表示对所有路由允许复用 如果你有路由不想利用可以在这加一些业务逻辑判断 */ public shouldDetach(route: ActivatedRouteSnapshot): boolean { return true; } /** 当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象 */ public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { SimpleReuseStrategy.handlers[route.routeConfig.path] = handle } /** 若 path 在缓存中有的都认为允许还原路由 */ public shouldAttach(route: ActivatedRouteSnapshot): boolean { return !!route.routeConfig && !!SimpleReuseStrategy.handlers[route.routeConfig.path] } /** 从缓存中获取快照,若无则返回nul */ public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (!route.routeConfig) { return null } return SimpleReuseStrategy.handlers[route.routeConfig.path] } /** 进入路由触发,判断是否同一路由 */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return future.routeConfig === curr.routeConfig } }
export class AppComponent { //路由列表 menuList: Array<{ title: string, module: string, power: string,isSelect:boolean }>=[]; constructor(private router: Router, private activatedRoute: ActivatedRoute, private titleService: Title) { //路由事件 this.router.events.filter(event => event instanceof NavigationEnd) .map(() => this.activatedRoute) .map(route => { while (route.firstChild) route = route.firstChild; return route; }) .filter(route => route.outlet === 'primary') .mergeMap(route => route.data) .subscribe((event) => { //路由data的标题 let title = event['title']; this.menuList.forEach(p => p.isSelect=false); var menu = { title: title, module: event["module"], power: event["power"], isSelect:true}; this.titleService.setTitle(title); let exitMenu=this.menuList.find(info=>info.title==title); if(exitMenu){//如果存在不添加,当前表示选中 this.menuList.forEach(p => p.isSelect=p.title==title); return ; } this.menuList.push(menu); }); } //关闭选项标签 closeUrl(module:string,isSelect:boolean){ //当前关闭的是第几个路由 let index=this.menuList.findIndex(p=>p.module==module); //如果只有一个不可以关闭 if(this.menuList.length==1) return ; this.menuList=this.menuList.filter(p=>p.module!=module); //删除复用 delete SimpleReuseStrategy.handlers[module]; if(!isSelect) return; //显示上一个选中 let menu=this.menuList[index-1]; if(!menu) {//如果上一个没有下一个选中 menu=this.menuList[index+1]; } // console.log(menu); // console.log(this.menuList); this.menuList.forEach(p => p.isSelect=p.module==menu.module ); //显示当前路由信息 this.router.navigate(['/'+menu.module]); } }
在我将所有代码嵌入到我写的项目中的时候发现,效果实现了,跟我之前想的一模一样。这里再次感谢下大神
4.遇到问题
首先,我发现大神写的路由存储用的key是用的路由的path属性,而且要在路由配置里写好:
就是data属性的module属性。这样虽然没什么问题,但是路由多的话要写的内容很多,而且按path去判断会出现问题,因为有主路由和子路由存在的话,path的值取出来都是子路由的path,很有可能不同的主路由会存在相同名称的子路由。所以我稍微改动了下代码:
在路由重用中加了一个方法:
private getRouteUrl(route: ActivatedRouteSnapshot){ return route['_routerState'].url.replace(/\//g,'_') }
获取路由的从主路由开始的路径,相当于location.pathname,然后把其中的 "/"字符换成了下划线。存储路由和判断路由都是用的这个方法的返回值来判断。比如说:
SimpleReuseStrategy.handlers[this.getRouteUrl(route)] = handle
问题解决了,然而我并没有高兴太久,因为我又遇到了一个问题:有些页面虽然是用的同一个路由,但是有可能是参数不一样,比如说:/detail/1 或者 /detail/2来显示详情界面。
于是开始查找问题,发现是路由重用组件导致的。我们来看下判断路由是否为同一路由的代码:
/** 进入路由触发,判断是否同一路由 */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return future.routeConfig === curr.routeConfig }
这里来判断是否是同一路由是用的 ActivatedRouteSnapshot的routeConfig对象,这个就是配置的路由,详情页面肯定是用的一个路由,只是参数不一样,但是这里直接判断.routeConfig显然是有问题的,具有不同的参数也会认为是同一个路由,导致会将之前的路由拿出来复用,其实并不是一个页面。然后我稍微修改了下这个判断:
/** 进入路由触发,判断是否同一路由 */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return future.routeConfig===curr.routeConfig && JSON.stringify(future.params)==JSON.stringify(curr.params); }
加了参数的判断在里面,这里问题解决。
然而..没多久又出现问题了,刚打开一个新的标签页,然后你并没有切换标签页直接点击标签页上的X把这个标签页又给干掉了,然后你再打开发现还原来的快照,关掉并没有成功清除掉快照。话不多说继续找问题。
发现导致这个问题是因为,路由快照是在离开这个路由的时候才会被记录,打开新的标签页而且没有切换标签的情况下,快照并没有记录,然而在关闭标签页的事件里删除快照显然就有问题了,因为这个时候你快照还没生成,怎么能删除呢,而且标签一关闭跳到其它标签页的时候,这里又触发了快照的保存。
想了一下解决方案,用了一个临时变量记录了下这种情况下待删除的路由,最终的路由复用代码:
import { RouteReuseStrategy, DefaultUrlSerializer, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router'; export class SimpleReuseStrategy implements RouteReuseStrategy { public static handlers: { [key: string]: DetachedRouteHandle } = {} private static waitDelete:string /** 表示对所有路由允许复用 如果你有路由不想利用可以在这加一些业务逻辑判断 */ public shouldDetach(route: ActivatedRouteSnapshot): boolean { return true; } /** 当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象 */ public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { if(SimpleReuseStrategy.waitDelete && SimpleReuseStrategy.waitDelete==this.getRouteUrl(route)){ //如果待删除是当前路由则不存储快照 SimpleReuseStrategy.waitDelete=null return; } SimpleReuseStrategy.handlers[this.getRouteUrl(route)] = handle } /** 若 path 在缓存中有的都认为允许还原路由 */ public shouldAttach(route: ActivatedRouteSnapshot): boolean { return !!SimpleReuseStrategy.handlers[this.getRouteUrl(route)] } /** 从缓存中获取快照,若无则返回nul */ public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (!route.routeConfig) { return null } return SimpleReuseStrategy.handlers[this.getRouteUrl(route)] } /** 进入路由触发,判断是否同一路由 */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return future.routeConfig===curr.routeConfig && JSON.stringify(future.params)==JSON.stringify(curr.params); } private getRouteUrl(route: ActivatedRouteSnapshot){ return route['_routerState'].url.replace(/\//g,'_') }
public static deleteRouteSnapshot(name:string):void{ if(SimpleReuseStrategy.handlers[name]){ delete SimpleReuseStrategy.handlers[name]; }else{ SimpleReuseStrategy.waitDelete=name; } } }
至此,整个功能的实现就完成了。经过多次测试也再也没有发现其实问题(如果有人发现有其它问题,还请发站内信给我)
5.后话
写这篇文章主要是想记录下自己在实现的过程遇到的问题,分享出来 ,希望能帮助到其他有类似需求的人,因为这方面的资料实在是太少了。