我应该如何在 Ember 个组件之间共享数据?

How should I share data between Ember components?

我的 Ember 应用有一条路线,其中包含 2 个不同的组件和一个带有 index.hbs 模板的控制器。

这是它的样子:

1) 用户可以 select 从过滤器组件的下拉列表中选择多个过滤器

2) DataGrid 是一个独立于过滤器的组件

3) 用户可以通过选中框

select DataGrid 中的多行

4) 创建自定义报告按钮触发 "sendAction" 到路由的控制器

此数据不是特定于模型的...它只是我可以制作自定义报告之前所需的临时数据。

Ember 最佳实践是 "Data Down / Actions Up",根据我的阅读,您不应该尝试从控制器访问组件。

但是,问题是 控制器中的 createCustomReport 方法需要访问过滤器组件中 selected 的所有过滤器以及所有在网格组件中检查的行。

我的第一直觉是在组件本身上设置属性 - 让它保持自己的状态 - 然后从控制器获取对组件的引用,以便在将其传递给报告函数之前获取其状态。

但显然这是一个禁忌。


这是我当前的解决方案:

每次我 select 过滤器时,都会有一个 sendAction 从组件冒泡到控制器并在控制器上设置自定义 属性。

此外,每次我 select 从网格中选择一个复选框时,另一个 sendAction 会转到组件,然后冒泡到控制器并在控制器上为 [=86] 设置自定义 属性 =]编辑网格行。

然后,当我单击 "createCustomReport" 时,在控制器中触发的方法可以访问我之前设置的属性 - 因为它们现在都在控制器上。

所以看起来像这样:

import Ember from 'ember';

export default Ember.Controller.extend({

    myFirstFilter: undefined,
    mySecondFilter: undefined,

    actions: {
        createCustomReport() {
            // do something with all those component properties you've been setting
        },

        // These are triggered by the sendAction on the respective component
        firstFilterMethod(myProperty1) {                
            this.set('myFirstFilter', myProperty1.name);
        },

        secondFilterMethod(myProperty2) {               
            this.set('mySecondFilter', myProperty2.name);
        },

        ... etc...


    }
});

这是我的问题

我不是直接从控制器访问组件,而是使用 "Actions Up" 原则,我在控制器上设置视图特定的属性。

来自 Sencha ExtJS 背景,其中控制器引用了他们的视图,我觉得这很奇怪。

通过不获取对组件的引用,我应该将我的控制器与其视图分离...但是由于我设置的所有属性通常都在视图上,因此控制器最终成为 与视图的耦合 比我仅获取对组件的引用还要多。

这在 Ember 中被认为是 "best practice" 还是有更好的方法让我获取所有这些独立组件的数据以触发 createCustomReport 方法?

好吧,我想我已经设法解决了我自己的问题并开始使用 Ember 做事方式。

我找到了 2 种不同的解决方案,每一种都有其优点。此外,我还创建了 2 个关于如何解决状态传播和共享组件数据的 Ember Twiddle 迷你教程。

两种解决方案都完全符合 Ember 2.6 的处理方式:无需控制器

第一个正在使用 Ember 服务。


我建立了一个简单的电影列表,可以在这里查看: https://ember-twiddle.com/c91e98cd255a556311417ac603ab0315

通过关注文件中的评论并查看上面的 Ember Twiddle,您所有关于如何实现它的问题都应该得到解答。

由于服务是单例,我可以将它注入到我的组件和路由中,它的唯一目的是维护其关联组件的数据。

组件如下所示:

import Ember from 'ember';

export default Ember.Component.extend({
  movieService: Ember.inject.service('movie-displayer-service'),
  currentSelectedMovie: '',

  didInsertElement: function() {
    // When the component is inserted into the DOM tree, use the model to set
    // the 'currentSelectedMovie' property.
    this.set('currentSelectedMovie', this.get('model').currentSelectedMovie);   
  },

  actions: {
    selectMovie: function(movie) {
      // Instead of saving state in the component itself, let's
      // save it in a service that can be consumed anywhere
      // in the application.

     this.get('movieService').setupCurrentSelectedMovie(movie);

     // When the movie changes, we can override the 'currentSelectedMovie' property
     // that is being populated with the 
     this.set('currentSelectedMovie', movie);   

    }
  }
});

服务如下所示:

import Ember from 'ember';

export default Ember.Service.extend({
  currentSelectedMovie: undefined,

  setupCurrentSelectedMovie: function(movie) {
   this.set('currentSelectedMovie', movie); 
  },

  showSelectedMovie: function() {
    if (this.get('currentSelectedMovie')) {
        alert("The current selected movie of the movie-displayer component is:  \n" + this.get('currentSelectedMovie'));
    } else {
        alert('Please Select a Movie First');
    }
  }
});

这是组件的车把文件:

<div class="movie-list-container">
    <h4 class="movie-list-title">Movie List</h4>

  <ul>
    {{#each model.movies as |movie|}}

        {{!--   'eq' is a helper function that I made
                    to compare two values.  You can check it out in
              the 'helpers' folder.
      --}}
        <li class="{{if (eq movie currentSelectedMovie) "selected" "not-selected"}}" {{action 'selectMovie' movie}}>{{movie}}</li>
    {{/each}}
  </ul>

</div>

路线如下:

import Ember from 'ember';

export default Ember.Route.extend({
  movieService: Ember.inject.service('movie-displayer-service'),

  model: function() {
    return {
        currentSelectedMovie: this.get('movieService').currentSelectedMovie,

      movies: ['Captain America: Civil War', 'Guardians of the Galaxy', 'Ant Man']
    }
  },

  actions: {
    showServiceState: function() {
        this.get('movieService').showSelectedMovie();
    }
  }
});

服务解决方案的优点:

作为单例,我可以在应用程序的任何位置访问该组件的数据。

服务解决方案的缺点:

我必须将它注入到我想要使用它的每个文件中 - 从而在我进行时创建依赖项。另一种解决方案是使用 Ember Initializer class,它会在应用程序启动时自动将其注入路由、控制器或组件。当然,这意味着它会进入 它所注入的每个 实例,这可能是过大的杀伤力。


第二种解决方案在没有服务的情况下将状态从组件发送到路由器


第二个 Ember Twiddle 是一个简单的餐厅列表,展示了如何在不需要服务的情况下传播状态:

https://ember-twiddle.com/dffc679fb96434ba6698161ba7617d15

组件的车把文件如下:

<div class="restaurant-list-container">
  <ul>
    {{#each model as |restaurant|}}
      <li class="{{if (eq currentlySelectedRestaurant restaurant ) 'selected' 'not-selected' }}" {{action 'selectRestaurant' restaurant}}>{{restaurant}}</li>
    {{/each}}
  </ul>

</div>

这是路由文件:

import Ember from 'ember';

export default Ember.Route.extend({  
  // Properties Here
    currentlySelectedRestaurant: 'Please Select a Restaurant',

  model: function() {
    return ['Taco Bell', 'McDonalds', 'Dennys']
  },

  actions: {
    setupRestaurantState : function(restaurant) {
        this.set('currentlySelectedRestaurant', restaurant);
    },

    getComponentState: function() {
     alert(this.get('currentlySelectedRestaurant'));
    }
  }
});

这里是组件文件:

import Ember from 'ember';

export default Ember.Component.extend({

  currentlySelectedRestaurant: undefined,

  actions: {
    selectRestaurant: function(restaurant) {

      // The 'sendAction' method is where the magic happens.
      // A method called 'stateSetter' references a function
      // that lives either on the controller or the route.
      // This was setup when the component was instantiated in the
      // fancy-restaurants.hbs file.
      this.sendAction('stateSetter', restaurant);
      this.set('currentlySelectedRestaurant', restaurant);
    }
  }
});

注意路由包含未定义的状态属性:'currentlySelectedRestaurant'。

这很容易是一个具有多个属性的对象或一个数组。

您也可以使用像 "componentState" 这样的通用名称,并存储您选择从任何组件发送的任何内容:例如在过滤列表中选中的选项或从网格中选择的项目。

不使用服务的优点:

做起来更容易。只需在您的组件中使用 sendAction() 来冒泡到路由器。并且没有创建额外的文件或任何依赖项。

不使用服务的缺点

由于模型数据从路由级别向下流动,如果您更改路由,您将无法访问状态。


每个解决方案都是可行的,所以我会留给您找出最有效的解决方案。

此外,我还没有将此标记为答案,因为其他人可能有更好的解决方案,如果能得到一些反馈会很好。