【问题标题】:Passing Values Between Non-Parent/Child Relationship Components in Angular 2在 Angular 2 中的非父/子关系组件之间传递值
【发布时间】:2017-01-10 23:27:42
【问题描述】:

据我了解,在 Angular 2 中,如果您想在不相关的组件(即不共享路由因此不共享父子关系的组件)之间传递值,您可以通过共享服务。

这就是我在 Angular2 应用程序中设置的内容。我正在检查 url 中是否存在特定系列的字符,如果存在则返回 true。

  isRoomRoute(routeUrl) {
      if ((routeUrl.includes('staff') || routeUrl.includes('contractors'))) {
          console.log('This url: ' + routeUrl + ' is a roomRoute');
          return true;
      } else {
          console.log('This url: ' + routeUrl + ' is NOT a room route');
          return false;
      }
  }

在根app.component的构造函数中,我订阅了路由事件:

constructor(private routeService: RouteService,
            private router: Router)  {
    this.router.events.subscribe((route) => {
    let routeUrl = route.url;
    this.routeService.sendRoute(routeUrl);
    this.routeService.isRoomRoute(routeUrl);
    });
}

... 然后使用那些提供的 url 来检查 url 是否包含特定的字符串。每次路线更改时都会对其进行评估。

所以这一切都按预期工作。

但是,我在将该检查的结果传递给不同的、不相关的(非父子)组件时遇到了问题。

即使我在 app.component 和不相关的 (room.component) 组件中都使用了共享服务 (routeService),但在一个组件中有效的功能在另一个组件中无效。据我了解,这里检查的“真实性”应该足以返回一个真实的陈述。

但在次要的、不相关的组件中,当我调用该函数时,我得到一个“未定义”错误,如下所示:

  isRoomRoute() {
       if (this.routeService.isRoomRoute(this.routeUrl)) {
           return true;
       }
     }

所以这就是我卡住的地方。基本上,关于 url 是否包含某个字符串的评估已经发生。现在我只需要将检查的布尔结果传递给次要的、不相关的组件。我怎样才能在 Angular 2 中做到最好?

【问题讨论】:

  • 服务不仅适用于不相关的组件,请参阅angular.io/docs/ts/latest/cookbook/…
  • 我明白,但这不是我要说的。我正在解决一个特定的用例,因为它涉及在非父子组件之间传递值。
  • 您可以将服务用于任何相关或无关的事情。在一个小型应用程序中,甚至可以只使用一个服务来处理整个应用程序的所有事件发射。我得到一些人可能会说不。
  • 好的,但同样,这与我的问题无关。我知道您可以将服务用于许多事情。我没有详尽无遗。我是说在不相关的组件之间传递信息时,您使用服务来执行此操作。
  • 其实一般不是这样的。共享服务将像单例一样运行。换句话说,两个组件将共享同一个服务实例。

标签: javascript angular typescript


【解决方案1】:

您的理解是正确的,injectable 共享服务是多个不相关组件之间的常用通信方式。

这是这样一个用例的演练。

首先,根据您的情况,我们将监听AppComponent 中的Router 事件,获取活动路由,并将其传递给RouteService,以便服务可以对其进行操作,和/或将其提供给其他组件。

AppComponent 应该是这样的:

export class AppComponent {

    constructor(private _router: Router,
                private _routeService: RouteService) {

        this._router.events.subscribe(event => {
            if (event instanceof NavigationEnd) {
                let url = event.urlAfterRedirects;
                this._routeService.onActiveRouteChanged(url);
            }
        });
    }

}

说到服务,这里我们将引入BehaviorSubject作为代理,这样使用服务的组件就可以订阅服务数据的变化。有关BehaviorSubject 和其他主题的更多信息,请访问:Delegation: EventEmitter or Observable in Angular2

这是我们共享的RouteService 的实现(组件需要use the single instance of the service,因此请确保您已在根级别提供它):

@Injectable()
export class RouteService {

    isRoomRouteSource: BehaviorSubject<boolean> = new BehaviorSubject(false);

    constructor() { }

    onActiveRouteChanged(url: string): void {
        let isRoomRoute = this._isRoomRoute(url);
        this.isRoomRouteSource.next(isRoomRoute);
        // do other stuff needed when route changes
    }

    private _isRoomRoute(url: string): boolean {
        return url.includes('staff') || url.includes('contractors');
    }
}

另一个组件使用该服务并订阅我们的BehaviorSubject更改的示例:

export class AnotherComponent {

    isCurrentRouteRoomRoute: boolean;

    constructor(private _routeService: RouteService) {
        this._routeService.isRoomRouteSource.subscribe((isRoomRoute: boolean) => {
            this.isCurrentRouteRoomRoute = isRoomRoute;
            // prints whenever active route changes
            console.log('Current route is room route: ', isRoomRoute);
        });
     }

}

如果订阅isRoomRouteSource 更改不是必需的,假设我们只需要存储的最后一个值,那么:

export class AnotherComponent {

    isCurrentRouteRoomRoute: boolean;

    constructor(private _routeService: RouteService) {
        this.isCurrentRouteRoomRoute = this._routeService.isRoomRouteSource.getValue(); // returns last value stored
        console.log('Current route is room route: ', this.isCurrentRouteRoomRoute);
     }

}

希望这有帮助!

【讨论】:

    【解决方案2】:

    仅查看您的代码,这里似乎有些地方不正确。

      isRoomRoute() {
           if (this.routeService.isRoomRoute(this.routeUrl)) {
               return true;
           }
         }
    在我看来,上面代码中的this.routeUrl 可能是未定义的,除非它在其他地方定义并在之前定义。您可以做的是在路由事件的服务中设置一个属性,然后在 isRoomRoute 中读取该属性。

    @Injectable()
    class routeService {
      constructor(private router: Router) {
        // subscribe to event
        router.subscribe((url) => {
          this.routeUrl = url;
          // other things?  sendRoute??
        });
    
      }
    
      // Other methods for this class
      isRoomRoute() {
        return this.routeUrl && (this.routeUrl.includes('staff') || this.routeUrl.includes('contractors'));
      }
    }
    
    // Usage later where this service has been injected
    @Component({
     // ... other properties
     providers: [routeService]
    })
    class someComponent {
      constructor(private routeService: routeService) {}
      someMethod() {
        this.routeService.isRoomRoute();  // Check if we are in a room route.
      }
    }

    在这种情况下,我不确定为什么不能在调用 isRoomRoute 时简单地获取 URL 并解析它,而不是在路由事件上设置一些东西。

    【讨论】:

    • 是的,问题肯定是我在 room.component 中调用“this.routeUrl”时未定义。考虑到我可以在同一组件中控制台记录 routeUrl 的正确值,这对我来说仍然很奇怪。问题:在你的回答中,'someStr' 代表什么?
    • 实际上,根据您上面的内容,条件是:(routeUrl.includes('staff') || routeUrl.includes('contractors')。我的答案刚刚改变。
    • 至于为什么this.routeUrl 未定义,我只是没有在您显示的代码中的任何地方看到它定义。我需要更多地说明原因。另一个组件没有我看到的 this.routeUrl = something ,所以它是未定义的。您还必须担心 js this 语义无论何时何地您调用的东西都会引用 this
    【解决方案3】:

    (被要求发布我在 cmets 中谈论的示例):

    我认为实际的 Angular 事件/数据流是这样的:

    • “Interested”从组件的 EventEmitter 听到发出的事件(因为它有对它的引用,并且订阅了该引用)。
    • 某物通过 EventEmitter 发出偶数,任何引用它并订阅它的东西都会听到它。

    他们都是用 EventEmitters 来做的。所以父母可以挂钩到孩子的事件发射器,而孩子可以挂钩到父母的。每个人都可以加入服务。而且,一切都是“应用程序”的孩子。所以最后(虽然服务是要走的路),你可以构建一个完整的组件通信系统,只需将每个组件挂钩到“应用程序”的事件发射器。

    此示例是解决菜单栏按钮通信问题的一种方法:单击一个按钮时,您希望他们都知道它(因此您可以突出显示选定的背景并取消突出显示其余部分,无论如何)。按钮组件是对等的,仅因为它们具有更高级别的父级而相关。

    所以在父组件中(可以像 MenuBarComponent 一样受限,也可以像“app”一样宽泛):

    <ul>
      <li *ngFor="let btn of buttons">
        // Bind the parent's broadcast listener, to the child Input. 
        <my-child [broadcastListener]="broadcasterParent">
        </my-child>
      </li>
    </ul>
    

    在这里,父母通过 Input 向孩子提供对其(父母的)EventEmitter 的引用(广播等只是典型的名称)。因此,当父级从其 EventEmitter 发出事件,或任何子级使用对该发射器的引用来发出事件时,所有通过 Input 引用并订阅该引用的组件都会听到它。

    上面那个模板背后的父代码(注意我使用的是 ES6,很确定你会明白的,我只是使用构造函数来保持简短):

    import { Component, EventEmitter } from '@angular/core';
    ...
    constructor  ) {
      // this is the instance given to the child as Input
      this.broadcasterParent = new EventEmitter ( );
    }
    

    在孩子身上:

    import { Component } from '@angular/core';
    ...
      constructor ( ) {
        // This is almost certainly better used in OnInit.
        // It is how the child subscribes to the parent's event emitter.
        this.broadcastListener.subscribe( ( b ) => this.onBroadcast ( b ) );
      }
    
      onButtonClick ( evt ) {
        // Any component anywhere with a ref to this emitter will hear it
        this.broadcastListener.emit ( evt );
      }
    
      onBroadcast ( evt ) {
        // This will be the data that was sent by whatever button was clicked
        // via the subscription to the Input event emitter (parent, app, etc).
        console.log ( evt );        
    }
    
    ChildComponent.annotations = [
      new Component ( {
          ... // TS would use an attribute for this
          inputs: [ 'broadcastListener' ]
          template: `
            <div click)="onButtonClick ( $event )">
               ...
            </div>
          `
      })
    ];
    

    服务实际上或多或少做同样的事情,但服务是“浮动的”并通过注入访问,而不是固定在层次结构中并通过输入访问(您可以使其更健壮等)。

    所以任何一个按钮被点击,都会发出一个他们都会听到的事件,因为他们都订阅了同一个事件发射器(无论是在服务中还是在其他任何地方)。

    【讨论】:

    • 谢谢,蒂姆。我会仔细研究一下以适用于我的情况。
    • 我仍然不清楚当两个组件没有父子关系时这将如何工作。最重要的是,我需要将“routeUrl”的值传递给非子组件,以便它可以自行评估该路由是否匹配某些参数......或者,我只需要传递布尔结果在服务中找到,在非子组件上。
    • 那么我如何将布尔结果从服务传递给调用该服务的组件?
    • 某处的某个组件,即订阅了其他东西的 EventEmitter(在您的情况下为服务),使用服务的 EventEmitter(或包装它的服务函数)来发出事件:mySvc。 emitEvent ( someValue ),类似于上面的 buttonClick。任何其他将 Service 注入其中并订阅了该发射器的组件将听到该发射的事件并从发起者那里接收数据。这里没有父子关系。只有两个组件具有相同的注入服务来操纵事件发射器。
    • 如果您仍然需要创建一个说明这一点的 CLI 项目,我将有一些时间,我会做一些事情,例如创建一些看起来与层次结构无关的组件(即使一切都是最终是“app”的子节点),并使用服务通过发出的事件传递数据。同样,我在上面展示的重点是,组件如何获取对事件发射器的引用并不重要;它可以从服务中获取,也可以通过输入获取。之后他们的用法是一样的。
    【解决方案4】:

    说我会这样做,所以这里的想法与我的第一个答案相同,只是使用“浮动”服务(所以没有父/子的东西)。虽然很幼稚,但这几乎是它的症结所在。

    首先,创建服务 EmitService。

    import { Injectable, EventEmitter } from '@angular/core';
    
    @Injectable()
    export class EmitService {
    
      private _emitter;
    
      constructor() {
        this._emitter = new EventEmitter ( );
      }
    
      subscribeToServiceEmitter ( callback ) {
        return this._emitter.subscribe ( b => callback ( b ) );
      }
    
      emitThisData ( data ) {
        this._emitter.emit ( data );
      }
    }
    

    创建两个组件,它们可以在应用程序的任何位置。这里是 CompOneComponent,复制它来创建 CompTwoComponent:

    import { Component, OnInit, OnDestroy } from '@angular/core';
    // the CLI puts components in their own folders, adjust this import
    // depending on your app structure...
    import { EmitService } from '../emit.service';
    
    @Component({
      selector: 'app-comp-one',
      templateUrl: './comp-one.component.html',
      styleUrls: ['./comp-one.component.css']
    })
    export class CompOneComponent implements OnInit, OnDestroy {
    
      private _sub;
    
      constructor( private _emitSvc : EmitService ) {}
    
      ngOnInit() {
        // A subscription returns an object you can use to unsubscribe
        this._sub = this._emitSvc.subscribeToServiceEmitter ( this.onEmittedEvent );
    
        // Watch the browser, you'll see the traces. 
        // Give CompTwoComponent a different interval, like 5000
        setTimeout( () => {
          this._emitSvc.emitThisData ( 'DATA FROM COMPONENT_1' );
        }, 2000 );
    
      }
    
      onEmittedEvent ( data ) {
        console.log ( `Component One Received ${data}` );
      };
    
      ngOnDestroy ( ) {
        // Clean up or the emitter's callback reference
        this._sub.unsubscribe ( );
      }
    }
    

    将其全部添加到您的应用中;这里的组件都是顶级的,但它们不是必须的,它们可以存在于任何地方:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';
    
    import { AppComponent } from './app.component';
    import { CompOneComponent } from './comp-one/comp-one.component';
    import { CompTwoComponent } from './comp-two/comp-two.component';
    
    import { EmitService } from './emit.service';
    
    @NgModule({
      declarations: [
        AppComponent,
        CompOneComponent,
        CompTwoComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
        HttpModule
      ],
      exports: [ ],
      providers: [ EmitService ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }
    

    就是这样。现在两个组件都可以访问服务(并订阅了它的 EventEmitter),因此它们可以告诉它发出事件,并接收任何其他组件触发的任何事件。创建一个 CLI 应用程序,添加这些东西,运行它,你会看到 console.logs 像你期望的那样触发(注意发出事件的组件也会听到它,你可以用几种不同的方式过滤掉它)。您可以将 EmitService 注入的任何东西都可以使用它,而不管它在“哪里”。

    【讨论】:

      猜你喜欢
      • 2018-06-18
      • 2016-07-20
      • 2017-09-04
      • 1970-01-01
      • 1970-01-01
      • 2016-04-29
      • 2017-08-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多