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.后话
写这篇文章主要是想记录下自己在实现的过程遇到的问题,分享出来 ,希望能帮助到其他有类似需求的人,因为这方面的资料实在是太少了。
Angular实现多标签页效果(路由重用)的更多相关文章
- DevExpress navBarControl 和 xtraTabbedMdiManager实现浏览器标签页效果
一:navBarControl 属性设置 工具箱中的Navigation & Layout选项卡下找到NavBarControl,拖到窗体中 工具箱中添加2个imageCollection.分 ...
- jquery实战---标签页效果
在前面的博客中,小编主要简单的介绍了jquery的一些基本知识,今天这篇博文,小编继续来学习jquery的相关知识,今天我们来学习一个标签页的小例子,相关源码小编已经上传,有需要的小伙伴可以自己去下载 ...
- 实战Jquery(四)--标签页效果
这两天完毕了实战四五六的样例,实例四是标签页的实现方法,实例五是级联菜单下拉框,实例六是窗体效果,都是web层经常使用的效果.越到后面越发认为技术这东西,就是一种思路的展现,懂了要实现 ...
- JS实现标签页效果(配合css)不同标签下对应不同div
显示页面tab.jsp </ div ></ body > </ html > tab.css ul ,li { margin:0px; padding:0px ...
- js实现多标签页效果
点击导航按钮切换div的内容 html代码: <div class="tabs"> <ul id="tab"> <li>&l ...
- web前端中实现多标签页切换的效果
在这里,实现多标签页效果的方法有两个,一个是基于DOM的,另一个是基于jquery的,此次我写的是一个对于一个电话套餐的不同,显示不同的标签页 方法一: 首先,我们要把页面的大体框架和样式写出来,ht ...
- Ant Design Blazor 组件库的路由复用多标签页介绍
最近,在 Ant Design Blazor 组件库中实现多标签页组件的呼声日益高涨.于是,我利用周末时间,结合 Blazor 内置路由组件实现了基于 Tabs 组件的 ReuseTabs 组件. 前 ...
- IOS UIScrollView + UIButton 实现segemet页面和顶部标签页水平滚动效果
很长一段时间没有写博客了,最近在学习iOS开发,看了不少的代码,自己用UIScrollView和UIButton实现了水平滚动的效果,有点类似于今日头条的主界面框架,效果如下: 代码如下: MyScr ...
- 【Android进阶】使用Andbase快速开发框架实现常见侧滑栏和滑动标签页组合效果
最近闲来无事,在网上寻找源代码看,突然发现了一个国内技术牛人开发的快速开发框架Andbase,花了一天时间研究了下源码和怎么使用,现将开发常见的侧滑栏和滑动标签页组合效果的使用介绍个大家,希望可以减少 ...
随机推荐
- Error处理:/bin/bash^M: 坏的解释器: 没有该文件或目录(bad interpreter: No such file or directory)
在Linux下编译运行脚本的时候出现”/bin/bash^M: 坏的解释器: 没有那个文件或目录(bad interpreter: No such file or directory)“这样的错误. ...
- Hibernate 4.3 SessionFactory
Configuration configuration = new Configuration().configure(); //以下这两句就是4.3的新用法 StandardServiceRegis ...
- 498_Diagonal-Traverse
目录 498_Diagonal-Traverse Description Solution Java solution Python solution 1 Python solution 2 Pyth ...
- html制作chm格式开源文档
在主界面点击生成器,找到网页所在的文件夹. 然后用编译,还是找到网页文件夹.根据需要设置.TOC 那一项是目录,请根据需要修改. 特别要注意的是,预设那里,点击那个配置图标,会打开如下图的预设编辑器. ...
- [javaSE] IO流(递归查找指定文件)
递归方法,实现查找目录中以.java为后缀的文件路径,并存入文本文件中 定义一个静态方法fileToLine(),传入参数:File对象目录,List集合对象(List<File> 这样做 ...
- 提交表单时,post方式无法提交(一种情况)
tomcat6,设置文件上传不限制大小maxPostSize="0" 但是在tomcat7及以后版本,应设置为小于0,如maxPostSize="-1" 否则 ...
- SpringFramework中重定向
需求: 需要在两个@Controller之间跳转,实现重定向 解决: @PostMapping("/files/{path1}") public String upload(... ...
- Inside the C++ Object Model:构造语意(chapter5)
看到构造语意的其中一节“继承体系下的对象构造”(5.2节)的最后,看来原文,发现侯杰的翻译有问题,怪不得读起来不顺. What about when providing an argument for ...
- ADO.NET对象模型之间的关系
ADO.Net支持两种访问数据的模型:无连接模式和连接模式 无连接模式将数据下载到客户机器上,并在客户机上将数据封装到内存中, 可以向访问本地关系数据库一样访问内存中的数据(例如DataSet), 连 ...
- Heap memory compared to stack memory