Angular2:使用NG-ZORRO的tabs结合路由复用策略实现动态tab

1.需求,使用路由懒加载的方式实现动态tab页,点击左侧菜单右侧新建一个tab,

2.新建一个项目: $ ng new angular-tab

按照ng-zorro官网的步骤导入 ng-zorro

  • 安装:$ npm install ng-zorro-antd --save
  • 在app.module.ts里导入
    image.png
  • 在.angular-cli.json里导入样式
    image.png

3.新建两个组件,header,sidebar

image.png
  • header.component.html
<div class="header">
</div>

-header.component.css

.header{
  height: 50px;
  width: 100%;
  background: lightskyblue;
}

header组件比较简单,就是一个div设定了高度

  • sidebar.component.html
<div class="sidebar">
  <ul nz-menu [nzMode]="'inline'" style="width: 240px;">
    <li nz-submenu>
      <span title><i class="anticon anticon-appstore"></i>系统管理</span>
      <ul>
        <li nz-menu-item>页面1</li>
        <li nz-menu-item>页面2</li>
        <li nz-menu-item>页面3</li>
      </ul>
    </li>
  </ul>
</div>
  • sidebar.component.css, 浮动一下,不然右边内容上不来
.sidebar{
  float: left;
  width: 240px;
}

siderbar里用到了ng-zorro组件库里的menu组件

  • app.component.html
<app-header></app-header>
<app-sidebar></app-sidebar>
<div class="content">
  123
</div>
  • app.component.css
.content{
  margin-left: 240px;
}

在app.component.html里添加两个组件

现在页面的效果

image.png

4.在编写tab之前,先添加几个tab要用到的页面

因为路由懒加载的方式是加载的模块,所以文件结构是这样的


image.png
  • page1.module.ts
import {NgModule} from '@angular/core';
import {Page1Component} from './page1.component';
import {CommonModule} from '@angular/common';
import {ContentComponent} from './content/content.component';
@NgModule({
  imports: [
    CommonModule,
    Page1RouteModule
  ],
  declarations: [
    Page1Component,
    ContentComponent
  ]
})
export class Page1Module {
}

说明一下:

  • page1.module.ts 因为路由懒加载是加载的模块,所以这个是给路由懒加载使用的,其中声明了两个组件,Page1Component和ContentComponent,其中Page1Component是路由进来显示的组件,具体看下文的page1-route.module.ts文件说明
  • page1-route.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {Page1Component} from './page1.component';
export const ROUTES: Routes = [
  {
    path: '', // 当访问 /page1的时候显示Page1Component组件
    component: Page1Component
  }
]
@NgModule({
  imports: [
    RouterModule.forChild(ROUTES)
  ],
  exports: [
    RouterModule
  ]
})
export class Page1RouteModule {
}

说明一下:
可以看到Page1RouteModule里设置了路由,当访问/page1 这个url,会加载Page1Component组件到页面上

  • page1.component.html
<app-page1-content></app-page1-content>
  • content.component.html
<p>
  page1的content组件
</p>

以同样的目录结构建立page2,page3


image.png

添加路由

  • 在src目录下建立app-route.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
export const ROUTES: Routes = [
  {
    path: 'page1',
    loadChildren: './pages/page1/page1.module#Page1Module'
  },
  {
    path: 'page2',
    loadChildren: './pages/page2/page2.module#Page2Module'
  },
  {
    path: 'page3',
    loadChildren: './pages/page3/page3.module#Page3Module'
  }
]
@NgModule({
  imports: [ // 因为是根路由,所以使用forRoot
    RouterModule.forRoot( ROUTES )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRouterModule {
}

说明一下:前面提到的page1.module.ts在这里派上了用场,路由懒加载的方式声明路由


image.png

将根路由添加到app.module.ts中
修改app.component.html

<app-header></app-header>
<app-sidebar></app-sidebar>
<div class="content">
  <router-outlet></router-outlet>
</div>

启动项目访问 http://localhost:4200/page1

image.png

一、实现 RouteReuseStrategy 接口自定义一个路由复用策略

  • 在service目录下新建SimpleReuseStrategy.ts文件
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(url: string): void {
    const key = url.replace(/\//g, '_');
    if (SimpleReuseStrategy.handlers[key]) {
      delete SimpleReuseStrategy.handlers[key];
    } else {
      SimpleReuseStrategy.waitDelete = key;
    }
  }
}

二、策略注册到app.module模块当中:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {NgZorroAntdModule} from 'ng-zorro-antd';
import {SidebarComponent} from './layout/sidebar/sidebar.component';
import {HeaderComponent} from './layout/header/header.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppRouterModule} from './app-router.module';
import {SimpleReuseStrategy} from './service/SimpleReuseStrategy';
import {RouteReuseStrategy} from '@angular/router';
@NgModule({
  declarations: [
    AppComponent,
    SidebarComponent,
    HeaderComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRouterModule,
    NgZorroAntdModule.forRoot()
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: SimpleReuseStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

四、新建一个tab组件并且注册到app.module中,

image.png
  • tab.component.html,tab页的具体使用参照ng-zorro官网,这里拷贝了一段官网的示例
<nz-tabset [nzType]="'card'" [nzSelectedIndex]="index">
  <nz-tab *ngFor="let tab of tabs" [nzTitle]="titleTemplate">
    <ng-template #titleTemplate>
      <div>{{ tab }}<i class="anticon anticon-close" (click)="closeTab(tab)"></i></div>
    </ng-template>
    Content of {{ tab }}
  </nz-tab>
</nz-tabset>
  • tab.component.ts
import {Component} from '@angular/core';
@Component({
  selector: 'app-tab',
  templateUrl: './tab.component.html',
  styleUrls: ['./tab.component.css']
})
export class TabComponent {
  index = 0;
  tabs = [ 'Tab 1', 'Tab 2' ];
  closeTab(tab: string): void {
    this.tabs.splice(this.tabs.indexOf(tab), 1);
  }
}
  • app.component.html
<app-header></app-header>
<app-sidebar></app-sidebar>
<div class="content">
  <app-tab></app-tab>
</div>

现在页面的样子


image.png

五、编写tab代码

  • tab.component.ts
import {Component} from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {Title} from '@angular/platform-browser';
import {SimpleReuseStrategy} from '../../service/SimpleReuseStrategy';

import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

@Component({
  selector: 'app-tab',
  templateUrl: './tab.component.html',
  styleUrls: ['./tab.component.css']
})
export class TabComponent {
  // 路由列表
  menuList = [];
  // 当前选择的tab index
  currentIndex = -1;
  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的标题
        const menu = {...event};
        menu.url = this.router.url
        const url = menu.url;
        this.titleService.setTitle(menu.title); // 设置网页标题
        const exitMenu = this.menuList.find(info => info.url === url);
        if (!exitMenu) {// 如果不存在那么不添加,
          this.menuList.push(menu);
        }
        this.currentIndex = this.menuList.findIndex(p => p.url === url);
      });
  }

  // 关闭选项标签
  closeUrl(url: string) {
    // 当前关闭的是第几个路由
    const index = this.menuList.findIndex(p => p.url === url);
    // 如果只有一个不可以关闭
    if (this.menuList.length === 1) {
      return;
    }
    this.menuList.splice(index, 1);
    // 删除复用
    // delete SimpleReuseStrategy.handlers[module];
    SimpleReuseStrategy.deleteRouteSnapshot(url)
    // 如果当前删除的对象是当前选中的,那么需要跳转
    if (this.currentIndex === index) {
      // 显示上一个选中
      let menu = this.menuList[index - 1];
      if (!menu) {// 如果上一个没有下一个选中
        menu = this.menuList[index];
      }
      // 跳转路由
      this.router.navigate([menu.url]);    }
  }
  /**
   * tab发生改变
   */
  nzSelectChange($event) {
    this.currentIndex = $event.index;
    const menu = this.menuList[this.currentIndex];
    // 跳转路由
    this.router.navigate([menu.url]);
  }

}
  • tab.component.html
<nz-tabset style="margin-left: -1px;" [nzAnimated]="true"
           [nzSelectedIndex]="currentIndex"
           [nzShowPagination]="true"
           (nzSelectChange)="nzSelectChange($event)"
           [nzType]="'card'">
  <nz-tab *ngFor="let menu of menuList" [nzTitle]="nzTabHeading">
    <ng-template #nzTabHeading>
      <div>
        {{menu.title}}
        <i *ngIf="menu.isRemove" (click)="closeUrl(menu.url)" class="anticon anticon-cross" ></i>
      </div>
    </ng-template>
  </nz-tab>
</nz-tabset>
<div class="tab-content">
  <!--路由的内容会被显示在这里-->
  <ng-content></ng-content>
</div>
  • app.component.html
<app-header></app-header>
<app-sidebar></app-sidebar>
<div class="content">
  <app-tab>
    <router-outlet></router-outlet>
  </app-tab>
</div>
  • app-route.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
export const ROUTES: Routes = [
  {
    path: 'page1',
    loadChildren: './pages/page1/page1.module#Page1Module',
    data: {
      title:' 页面1',
      isRemove: true
    }
  },
  {
    path: 'page2',
    loadChildren: './pages/page2/page2.module#Page2Module',
    data: {
      title: '页面2',
      isRemove: true
    }
  },
  {
    path: 'page3',
    loadChildren: './pages/page3/page3.module#Page3Module',
    data: {
      title: '页面2',
      isRemove: true
    }
  }
]
@NgModule({
  imports: [ // 因为是根路由,所以使用forRoot
    RouterModule.forRoot( ROUTES )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRouterModule {
}

六、现在编写sidebar页面,和tab联动起来

  • sidebar.component.html
<div class="sidebar">
  <ul nz-menu [nzMode]="'inline'" style="width: 240px;">
    <li nz-submenu>
      <span title><i class="anticon anticon-appstore"></i>系统管理</span>
      <ul>
        <li nz-menu-item (click)="tabs('page1')">页面1</li>
        <li nz-menu-item (click)="tabs('page2')">页面2</li>
        <li nz-menu-item (click)="tabs('page3')">页面3</li>
      </ul>
    </li>
  </ul>
</div>
  • sidebar.component.ts
import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
@Component({
  selector: 'app-sidebar',
  templateUrl: './sidebar.component.html',
  styleUrls: ['./sidebar.component.css']
})
export class SidebarComponent implements OnInit {
  constructor(private router: Router) { }
  ngOnInit() {
  }
  /**
   * 路由方式添加tab
   * @param data
   */
  tabs(data) {
    this.router.navigate([data]);
  }
}

现在页面

image.png

参考文章

https://www.cnblogs.com/lovesangel/p/7853364.html
http://www.cnblogs.com/lslgg/p/7700888.html

NG-ZORRO官网

https://ng.ant.design/docs/getting-started/zh

项目地址

https://github.com/Ariesssssssss/angular-tab

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容