【问题标题】:Component communication in Knockout.JSKnockout.JS 中的组件通信
【发布时间】:2023-03-05 06:12:01
【问题描述】:

我正在 KnockoutJS 中设计一个“中等”大小的应用程序,我想知道如何在组件之间发送事件。

在 KnockoutJS 中想象一个嵌套的组件层次结构:

Root Viewmodel -> A -> B -> C
               -> D

D 如何对来自 C 的消息作出反应?显而易见的 knockoutJS 方法是让 C 写入作为参数传递的 observable,并与 D 共享这个 observable,D 对这个 observable 的变化做出反应。

我不喜欢这种方法的是,A 和 B 需要了解消息,并且 A 和 B 通过其参数主动转发处理程序。使用正常的依赖注入方法,我可以将组件 C 和 D 直接相互连接起来,例如通过在 A 和 B 不知道的情况下将 D 注入 C。

所以我的问题:

  • 有没有办法手动连接根视图模型中的组件(例如,通过拦截组件创建)?

或改写:

  • 如何在不通过其父组件的情况下从主视图模型配置嵌套组件?

ko.components.register("aaa", {
    viewModel: function (params) { this.handler = params.handler;  },
    template: "<span>A</span> <bbb params='handler: handler'></bbb>"
});

ko.components.register("bbb", {
    viewModel: function (params) { this.handler = params.handler;  },
    template: "<span>B</span> <ccc params='handler: handler'></ccc>"
});

ko.components.register("ccc", {
    viewModel: function (params) { this.handler = params.handler;  },
    template: "<span>C</span> <button data-bind='click: handler'>OK</button>"
});

ko.components.register("ddd", {
    viewModel: function (params) { 
        var self = this; 
        this.text = ko.observable("No event received!"); 
        if (params.onClick)     params.onClick.subscribe(function () {
            self.text("Event Received!");
        });
    },
    template: "<span>D</span> <span data-bind='text:text'/>"
});


ko.applyBindings({
    onClick: ko.observable()
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<p><aaa params='handler: onClick'> </aaa></p>
<p><ddd params='onClick: onClick'> </ddd></p>

【问题讨论】:

  • 您是否考虑过knockout postbox 插件中使用的模式?
  • 你如何在ccc 中定义如何从aaa 访问方法?通过命名约定?您可以创建某种共享参数组件加载器,但实现如何在ccc 中动态找到正确的参数仍然是一个挑战……无论如何,这就是您“中间组件创建”的方式:jsfiddle.net/r7zt4rf9
  • 完美 - 毕竟连接组件 ccc 和 ddd 的人需要了解两者的细节。我试图让组件加载器代码正确,但尝试使用默认组件加载器挂钩组件构造函数失败。通过这种方式,主视图模型可以获取任何创建的 ccc 或 ddd 组件的句柄。它的作用取决于应用程序。这将解决我的问题。如果您将该代码放入答案中,我会接受。
  • 我添加了一个答案,其中包括一些额外的解释。希望对您有所帮助!

标签: knockout.js custom-component


【解决方案1】:

有没有办法手动连接根视图模型中的组件(例如,通过拦截组件创建)?

(强调我的)

淘汰机制

您可以通过使用getConfigloadComponentloadTemplateloadViewModel 方法的任意组合将对象添加到ko.components.loaders 数组来注入custom component loaders

由于您希望在视图模型之间创建连接,因此这是我们需要定义的唯一方法。来自文档:

loadViewModel(name, viewModelConfig, callback)

viewModelConfig 值只是来自任何 componentConfig 对象的 viewModel 属性。比如可能是构造函数,...

方法

您的组件是直接引用构造函数定义的。我们将把这个构造函数包装在一个工厂函数中,该函数将传递给组件的params 替换为一个连接的sharedParams 对象,该对象包含传递给链上任何组件的所有内容。 这是否足够“安全”取决于您。一旦自定义加载程序就位,想出另一种连接aaaddd 的方式应该不会太难。

简而言之,我们的自定义加载器将:

  1. 检索组件视图模型的原始构造函数 (VM)
  2. 动态创建一个factory 函数:
    • 将传递的params 添加到绑定上下文中
    • 使用 shared 参数构造 VM 实例
    • 返回新的视图模型
  3. 调用新创建的factory函数

在代码中:

ko.components.loaders.unshift({
  loadViewModel: function loadViewModel(name, VM, callback) {
    const factory = function (params, componentInfo) {
      const bc = ko.contextFor(componentInfo.element);

      // Overwrite sharedParams
      bc.sharedParams = Object.assign(
        { },
        bc.sharedParams || {},
        params
      );

      return new VM(bc.sharedParams);
    };

    return callback(factory);
  }
});

运行示例

查看这个链接的小提琴来自己玩自定义加载器。我已经包含了几个嵌套组件结构,以表明不再需要手动传递 params

https://jsfiddle.net/sqLathu9/

【讨论】:

    【解决方案2】:

    我个人觉得很丑的一个解决方案,但可以被认为是这个问题的有效答案:

    您可以使用$root$parents[i]$parent 来引用组件绑定上下文中的任何层。我从您的示例中删除了一些params,以向您展示这是如何工作的。它表现出两种风味:

    1. params 属性内的bindingContext 获取依赖项(首选)
    2. 访问bindingContext里面组件模板(不推荐)

    ko.components.register("aaa", {
        viewModel: function (params) {  },
        template: "<span>A</span> <bbb></bbb>"
    });
    
    ko.components.register("bbb", {
        viewModel: function (params) {  },
        template: "<span>B</span> <ccc></ccc>"
    });
    
    ko.components.register("ccc", {
        viewModel: function (params) { },
        // This hides the dependency in the template. It's better to pass it in the DOM via `params`
        template: "<span>C</span> <button data-bind='click: $root.onClick'>OK</button>"
    });
    
    ko.components.register("ddd", {
        viewModel: function (params) { 
            var self = this; 
            this.text = ko.observable("No event received!"); 
            if (params.onClick)     params.onClick.subscribe(function () {
                self.text("Event Received!");
            });
        },
        template: "<span>D</span> <span data-bind='text:text'/>"
    });
    
    
    ko.applyBindings({
        onClick: ko.observable()
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    
    <p><aaa> </aaa></p>
    <!-- This approach isn't *that* ugly, since the dependency is made explicit in the DOM -->
    <p><ddd params='onClick: $root.onClick'> </ddd></p>

    这里的弱点是您创建了对难以记录/检查的 DOM 结构的固定依赖。我通常一使用$parent就会开始遇到错误...

    【讨论】:

    • 我认为这里最大的问题是,组件C需要知道消息的接收者,这首先违反了组件的思想。仅当 ROOT 视图模型中存在 onClick 属性时才使组件工作对于除非常小的项目外的所有项目来说都是一个糟糕的解决方案。
    【解决方案3】:

    在这种情况下,我认为最好使用 EventEmitter https://github.com/Olical/EventEmitter

    通过这种方式,您可以创建一个全局 ee 实例 EventEmitter,例如在您的主 html 上的脚本中

    window.ee = new EventEmitter()

    然后你可以在组件之间添加监听器或触发动作:

    对于组件 C 中的示例,您可以使用以下命令触发事件:window.ee.emitEvent('some-name-for-the-event', someArgumentsArray)

    在你的 D 组件中,只需为该事件添加一个侦听器,以便做出相应的反应,如 window.ee.addListener('some-name-for-the-event', function (params) { // your own code to handle the event });

    【讨论】:

    • 使用窗口不是一个好的解决方案(考虑单元测试场景),尽管我当然可以在任何地方粘贴一个全局变量。无论如何,我会将所有组件连接到同一个事件发射器。就像在邮箱插件中一样(它使用淘汰库上的全局变量)。问题是 - 有没有办法只连接两个组件而不在所有组件之间引入共享状态。为此,需要从主视图模型中获取嵌套子组件的句柄。但是怎么做呢?
    【解决方案4】:

    我所做的是当 D 由 C 创建时,C 将参数作为对 D 的引用传递,并且 D 将自己分配给该参数。参数可以是 observable,D 的 onDispose 可以清除 observable 以便 C 知道 D 是否被释放。

    【讨论】:

      猜你喜欢
      • 2014-10-27
      • 2016-01-24
      • 2016-08-07
      • 2019-05-10
      • 2010-09-22
      • 2019-06-18
      • 2018-02-05
      • 2011-01-16
      相关资源
      最近更新 更多