一、前言

公司要实现用tabs标签页,看到需求的我,一脸愁眉,仔细阅读angular官网教程,发现可以使用路由复用策略(RouteReuseStrategy)来实现。

路由复用策略是用于解决这种事情:在移动端中用户通过关键词搜索商品,而死不死的这样的列表通常都会是自动下一页动作,此时用户好不容易滚动到第二页并找到想要看的商品时,路由至商品详情页,然后一个后退……用户懵逼了。

Angular路由与组件一开始就透过 RouterModule.forRoot 形成一种关系,当路由命中时利用 ComponentFactoryResolver 构建组件,这是路由的本质。

而每一个路由并不一定是一次性消费,Angular 利用 RouteReuseStrategy 贯穿路由状态并决定构建组件的方式;当然默认情况下(DefaultRouteReuseStrategy)像上面举的例子,一切都不进行任何处理。

二、原理

RouteReuseStrategy 我们称它为:路由复用策略;并不复杂,提供了几种办法通俗易懂的方法:

shouldDetach 是否允许复用路由 store 当路由离开时会触发,存储路由 shouldAttach 是否允许还原路由 retrieve 获取存储路由 shouldReuseRoute 进入路由触发,是否同一路由时复用路由 这看起来就像是一个时间轴关系,用一种白话文像是这样:把路由 /list 设置为允许复用(shouldDetach),然后将路由快照存在 store 当中;当 shouldReuseRoute 成立时即:再次遇到 /list 路由后表示需要复用路由,先判断 shouldAttach 是否允许还原,最后从 retrieve 拿到路由快照并构建组件。

当理解这一原理时,假如我们拿开头搜索列表返回的问题就变得非常容易解决。

三、实现

(一)创建策略

创建一个名为SimpleReuseStrategy的策略文件。因为代码里注释比较完整,就不再赘述。内容如下:

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;
        }
        //debugger;
        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;
        }
    }
}

(二)定义组件

我们利用angular官方的脚手架工具创建一个名叫content-tabs的组件。组件内容如下:

export class ContentTabsComponent {

    //路由列表
    menuList: Array<{ title: string, module: string, power: string, isSelect: boolean }> = [];

    constructor(
        private router: Router,
        private activatedRoute: ActivatedRoute,
        private titleService: Title
    ) {
        this.getMenuList();
        //路由事件
        this.router.events.filter(event => event instanceof NavigationEnd)
            .map(() => this.activatedRoute)  // 将filter处理后的Observable再次处理
            .map(route => {
                while (route.firstChild) route = route.firstChild;//遍历路由表以便获取到每一个页面对应的路由信息
                return route;
            })
            .filter(route => route.outlet === 'primary')
            .mergeMap(route => route.data) //获取我们在路由表中为每个路由传入的data信息
            .subscribe((event) => {
                //路由data的标题
                let title = event['title'];
                SimpleReuseStrategy.handlers;
                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);
            });
    }

    getMenuList(): void {
        let menu = { title: "工作台", module: "/app/admin/home", power: "", isSelect: true };
        this.menuList.push(menu);
    }

    //关闭选项标签
    closeCurrent(isSelect: boolean) {
        //当前关闭的是第几个路由
        let index = this.menuList.findIndex(p => p.isSelect == true);
        let currentTab = this.menuList.find(p => p.isSelect == true);
        //如果只有一个不可以关闭
        if (this.menuList.length == 1) return;
        this.menuList = this.menuList.filter(p => p.isSelect == false);
        //删除复用
        let module = currentTab.module;
        SimpleReuseStrategy.deleteRouteSnapshot(module);
        //if (!isSelect) return;
        //显示上一个选中
        let menu = this.menuList[index - 1];
        if (!menu) {//如果上一个没有下一个选中
            menu = this.menuList[index];
        }
        //console.log(menu);
        //console.log(this.menuList);
        this.menuList.forEach(p => p.isSelect = p.module == menu.module);
        //显示当前路由信息
        this.router.navigate(['/' + menu.module]);
    }

    //关闭选项标签,用于关闭每个小选项卡
    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);
        //删除复用
        SimpleReuseStrategy.deleteRouteSnapshot([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]);
    }

    closeCloseOther(isSelect: boolean) {
        let currentTab = this.menuList.filter(p => p.isSelect == true);
        let otherTab = this.menuList.filter(p => p.isSelect == false);
        this.menuList = currentTab;
        if (this.menuList.length == 1) return;
        for (var i = 0; i < otherTab.length; i++) {
            SimpleReuseStrategy.deleteRouteSnapshot(otherTab[i].module);
        }
        this.router.navigate(['/' + currentTab[0].module]);
    }

    closeCloseAll() {
        if (this.menuList.length == 1) return;
        for (var i = 1; i < this.menuList.length; i++) {
            SimpleReuseStrategy.deleteRouteSnapshot(this.menuList[i].module);
        }
        let tempList = this.menuList[0];
        this.menuList.splice(0, this.menuList.length);
        this.menuList.push(tempList);
        this.router.navigate(['/' + this.menuList[0].module]);
    }

    tabReload() {
        //window.location.reload();
        let currentTab = this.menuList.find(p => p.isSelect == true);
        //router.renavigate()
        this.router.navigate(['/' + currentTab.module]);
    }
}
(三)content-tabs的html
<div class="content-tabs">
    <button class="roll-nav roll-left tabLeft" style="margin-left: 2px;border-left: 1px solid #ddd;">
        <i class="fa fa-backward"></i>
    </button>
    <nav class="page-tabs">
        <div class="page-tabs-content" *ngFor="let menu of menuList">
            <div class="menuTabs" [ngClass]="{'active':menu.isSelect}">
                <a routerLink="/{{ menu.module }}">
                    <i class="icon-home"></i> {{ menu.title }}
                </a>
                <i class="fa fa-times-circle" (click)="closeUrl(menu.module,menu.isSelect)"></i>
            </div>
        </div>
    </nav>
    <button class="roll-nav roll-right tabRight">
        <i class="fa fa-forward" style="margin-left: 3px;"></i>
    </button>
    <div class="btn-group roll-nav roll-right">
        <button class="dropdown tabClose" data-toggle="dropdown">
            {{l("tabOperat")}}<i class="fa fa-caret-down" style="padding-left: 3px;"></i>
        </button>
        <ul class="dropdown-menu dropdown-menu-right">
            <li><a class="tabReload" (click)="tabReload()">{{l("tabReload")}}</a></li>
            <li><a class="tabCloseCurrent" (click)="closeCurrent(true)">{{l("tabCloseCurrent")}}</a></li>
            <li><a class="tabCloseOther" (click)="closeCloseOther(false)">{{l("tabCloseOther")}}</a></li>
            <li><a class="tabCloseAll" (click)="closeCloseAll()">{{l("tabCloseAll")}}</a></li>
        </ul>
    </div>
    <a (click)="logout()" class="roll-nav roll-right tabExit"><i class="fa fa fa-sign-out"></i> {{l("Logout")}}</a>
</div>

样式文件就不放出来了,模仿H+的样式文件夹写的,修改了a标签存放i标签的小小样式。

最后,别忘了在根模块里添加提供器:

{ provide: RouteReuseStrategy, useClass: SimpleReuseStrategy }

至此,所有的代码就完成了。路由由angular路由配置而成。类似如下:

@NgModule({
    imports: [
        RouterModule.forChild([
            {
                path: '',
                children: [
                    { path: 'users', component: UsersComponent, data: { permission: 'Pages.Administration.Users', title: '用户', module: '/app/admin/users' } },
                    { path: 'roles', component: RolesComponent, data: { permission: 'Pages.Administration.Roles', title: '角色', module: '/app/admin/roles' } },
                    { path: 'auditLogs', component: AuditLogsComponent, data: { permission: 'Pages.Administration.AuditLogs', title: '审计日志', module: '/app/admin/auditLogs' } },
                ]
            }
        ])
    ],
    exports: [
        RouterModule
    ]
})

//other codes.... 完成的效果如下图: