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.后話
寫這篇文章主要是想記錄下自己在實現的過程遇到的問題,分享出來 ,希望能幫助到其他有類似需求的人,因為這方面的資料實在是太少了。