Angular 如果更改单页应用程序可以使用路由,以便 2 个路由在拆分视图中都可见吗?

Can Angular Routing be used if single page application is changed so that 2 routes can be both visible w splitted view?

我有一个由 VS2017 Angular template 创建的示例应用程序,它是一个单页应用程序,在 app.module.ts

中定义了 3 个路由
RouterModule.forRoot([
  { path: '', component: HomeComponent, pathMatch: 'full' },
  { path: 'counter', component: CounterComponent },
  { path: 'fetch-data', component: FetchDataComponent },
])

并在 app.component.html

<body>
  <app-nav-menu></app-nav-menu>
  <div class="container">
    <router-outlet></router-outlet>
  </div>
</body>

导航在 nav-menu.component.html

中受到限制
<header>
  <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>
    <div class="container">
      <a class="navbar-brand" [routerLink]='["/"]'>my_new_app</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"
        [attr.aria-expanded]="isExpanded" (click)="toggle()">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>
        <ul class="navbar-nav flex-grow">
          <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'>
            <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a>
          </li>
          <li class="nav-item" [routerLinkActive]='["link-active"]'>
            <a class="nav-link text-dark" [routerLink]='["/counter"]'>Counter</a>
          </li>
          <li class="nav-item" [routerLinkActive]='["link-active"]'>
            <a class="nav-link text-dark" [routerLink]='["/fetch-data"]'>Fetch data</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</header>

选中计数器的正常情况如下所示(如果导航在侧面):

Home        | Counter | 
Counter (x) |         |
Fetch       |         |

在某些情况下,我需要有 2 个 "main level" 组件可见,这样就不会在路由器出口中为 1 个组件设置区域,而是将区域分成 2 部分,并且 2 条路由会以某种方式处于活动状态。

Home        | Counter | Fetch |
Counter (x) |         |       |
Fetch   (x) |         |       |

可以或应该使用 angular 路由来完成吗? 正常使用还是只有1条route active,router-outlet区域不分。

这可以完成,例如使用 ngIf 并使用(切换)按钮而不是路由器链接。但是,我正在开发一个非常大的应用程序,如果可能的话,我有兴趣使用路由。

链接的"duplicate"是关于第二个路由器插座的,与此无关。在这里,我想要实现的是主路由器出口同时激活 2 条路由并且内容被拆分。这可能是不可能的,但这就是想法,而不是一些侧边栏辅助导航。

我尝试过的一个选择是有 3 个分割区域,其中 1 个有路由器插座。其他 2 个具有计数器和获取数据组件作为它们的内容。当用作单页应用程序时,只有第一个拆分区域可见。

app.component.html

<body>
  <app-nav-menu></app-nav-menu>
  <div id="working" >
  <as-split direction="horizontal">
    <as-split-area>
      <router-outlet></router-outlet>
    </as-split-area>
    <as-split-area *ngIf="secondSplitAreaVisible">
      <app-counter-component></app-counter-component>
    </as-split-area>
    <as-split-area *ngIf="thirdSplitAreaVisible">
      <app-fetch-data></app-fetch-data>
    </as-split-area>
  </as-split>
  </div>
</body>

可以使用导航组件中的复选框将其他 2 个设置为可见,如下所示。请注意,在我的例子中,必须控制组件在 GUI 中只能显示一次。这是通过对路由使用 auth guards 并禁用上述复选框来完成的,以防止显示已在 router-outlet 中可见的组件的 aplit 区域。

导航-menu.component.html:

<header>
  <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>
    <div class="container">
      <a class="navbar-brand" [routerLink]='["/"]'>my_new_app</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"
              [attr.aria-expanded]="isExpanded" (click)="toggle()">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>
        <ul class="navbar-nav flex-grow">
          <li class="nav-item" [routerLinkActive]='["link-active"]'>
              <a class="nav-link text-dark" [routerLink]='["/home"]'><mat-checkbox [(ngModel)]="firstChecked" (change)="toggleTab('home')" [disabled]="firstDisabled"></mat-checkbox>Home</a>
          </li>
          <li class="nav-item" [routerLinkActive]='["link-active"]' [ngStyle]="{'border-bottom' : secondChecked || secondActive ? '2px solid' : '0px' }">
            <a class="nav-link text-dark" [routerLink]='["/counter"]'>
            <mat-checkbox [(ngModel)]="secondChecked" (change)="toggleTab('counter', secondChecked)" [disabled]="secondActive"></mat-checkbox>Counter</a>
          </li>
          <li class="nav-item" [routerLinkActive]='["link-active"]' [ngStyle]="{'border-bottom' : thirdChecked || thirdActive ? '2px solid' : '0px' }">
            <a class="nav-link text-dark" [routerLink]='["/fetch-data"]'><mat-checkbox [(ngModel)]="thirdChecked" (change)="toggleTab('fetch-data', thirdChecked)" [disabled]="thirdActive"></mat-checkbox>Fetch data</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</header>

app.module.ts 路由定义

RouterModule.forRoot([
  { path: 'home', component: HomeComponent, canActivate: [AuthGuard]},
  { path: 'counter', component: CounterComponent, canActivate: [AuthGuard] },
  { path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthGuard]},
  { path: '', redirectTo: '/home', pathMatch: 'full' }

和授权守卫:

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  subscription;
  outletUrl: string;
  secondSplitAreaVisible: boolean = false;
  thirdSplitAreaVisible: boolean = false;

  constructor(
    private router: Router,
    private ngRedux: NgRedux<IAppState>,
    private actions: TabActions) {
      this.subscription = ngRedux.select<string>('outletUrl')
        .subscribe(newUrl => this.outletUrl = newUrl);    // <- New
        this.subscription = ngRedux.select<boolean>('secondOpen') // <- New
        .subscribe(newSecondVisible => this.secondSplitAreaVisible = newSecondVisible);    // <- New
        this.subscription = ngRedux.select<boolean>('thirdOpen') // <- New
        .subscribe(newThirdVisible => this.thirdSplitAreaVisible = newThirdVisible);    // <- New
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (state.url === '/counter' && this.secondSplitAreaVisible) {
      return false;
    }
    if (state.url === '/fetch-data' && this.thirdSplitAreaVisible) {
      return false;
    }
    return true;
  }
}

上面使用 redux 来管理状态变化。该部分也在下面,以防万一有人感兴趣:

导航-menu.component.ts

@Component({
  selector: 'app-nav-menu',
  templateUrl: './nav-menu.component.html',
  styleUrls: ['./nav-menu.component.css']
})
export class NavMenuComponent {
  firstChecked: boolean = false;
  secondChecked: boolean = false;
  thirdChecked: boolean = false;

  firstDisabled: boolean = true;
  secondActive: boolean = false;
  thirdActive: boolean = false;

  constructor(
    private ngRedux: NgRedux<IAppState>,
    private actions: TabActions,
    private router: Router) {
    router.events.subscribe((event) => {
      if (event instanceof NavigationEnd) {
        this.ngRedux.dispatch(this.actions.setOutletActiveRoute(event.url));
        if (event.url.includes('counter')) {
          this.secondActive = true;
          this.thirdActive = false;
          this.firstChecked = false;  
        }
        else if (event.url.includes('fetch')) {
          this.thirdActive = true;
          this.secondActive = false;
          this.firstChecked = false;          
        }
        else {
          // home
          this.secondActive = false;
          this.thirdActive = false;
          this.firstChecked = true;
        }
      }
    });
  }

  isExpanded = false;

  collapse() {
    this.isExpanded = false;
  }

  toggle() {
    this.isExpanded = !this.isExpanded;
  }

  toggleTab(name: string, isChecked : boolean) { 
    this.ngRedux.dispatch(this.actions.toggleSplitArea({ splitArea : name, isVisible: isChecked}));
  }
}

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
  title = 'app';
  secondSplitAreaVisible: boolean = false;
  thirdSplitAreaVisible: boolean = false;

  subscription;

  constructor(
    private ngRedux: NgRedux<IAppState>,
    private actions: TabActions) {
      this.subscription = ngRedux.select<boolean>('secondOpen')
        .subscribe(newSecondVisible => {
          this.secondSplitAreaVisible = newSecondVisible;
        });    
        this.subscription = ngRedux.select<boolean>('thirdOpen')
        .subscribe(newThirdVisible => {
          this.thirdSplitAreaVisible = newThirdVisible;
        }); 
  }

  ngOnDestroy() {                 
    this.subscription.unsubscribe();
  } 
}

app.actions.ts

@Injectable()
export class TabActions {
  static TOGGLE_SPLIT_AREA = 'TOGGLE_SPLIT_AREA';
  static SET_OUTLET_ACTIVE_ROUTE = 'SET_OUTLET_ACTIVE_ROUTE';

  toggleSplitArea(splitAreaToggle: SplitAreaToggle): SplitAreaToggleAction {
    return { 
        type: TabActions.TOGGLE_SPLIT_AREA, 
        splitAreaToggle 
    };
  }

  setOutletActiveRoute(url: string) : SetOutletActiveRouteAction {
    return { 
        type: TabActions.SET_OUTLET_ACTIVE_ROUTE,
        url
    };
  }
}

store.ts

export interface IAppState { 
    outletUrl : string;
    secondOpen : boolean;
    thirdOpen : boolean;
};

export const INITIAL_STATE: IAppState = {
    outletUrl: 'home',
    secondOpen : false,
    thirdOpen : false
};

export function rootReducer(lastState: IAppState, action: Action): IAppState {
    switch(action.type) {
        case TabActions.SET_OUTLET_ACTIVE_ROUTE: {
            const setRouteAction = action as SetOutletActiveRouteAction;
            const newState: IAppState = {
                ...lastState,
                outletUrl: setRouteAction.url
            }
            return newState;
        }
        case TabActions.TOGGLE_SPLIT_AREA: {
            const splitToggleAction = action as SplitAreaToggleAction;
            console.log('rootreducer splitareatoggle:' + splitToggleAction.splitAreaToggle.splitArea);
            if (splitToggleAction.splitAreaToggle.splitArea === 'counter') {
                const newState: IAppState = {
                    ...lastState,
                    secondOpen: splitToggleAction.splitAreaToggle.isVisible
                }
                return newState;
            }
            else {
                const newState: IAppState = {
                    ...lastState,
                    thirdOpen: splitToggleAction.splitAreaToggle.isVisible
                }
                return newState;
            }
        }
        default : {
            return lastState;
        }
    }
}