Knockout.JS 中的组件通信

Component communication in Knockout.JS

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

想象一下 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>

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

您可以使用 $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>

这里的弱点是你创建了一个难以 document/check 的 DOM 结构的固定依赖。我通常在使用 $parent...

后立即开始 运行 处理错误

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

通过这种方式,您可以创建 EventEmitter 的全局 ee 实例,例如在主 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 });

Is there a way to manually wire up the components inside the root viewmodel (e.g. by intercepting component creation)?

(emphasis mine)

淘汰机制

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

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

loadViewModel(name, viewModelConfig, callback)

The viewModelConfig value is simply the viewModel property from any componentConfig object. For example, it may be a constructor function, ...

方法

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

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

  1. 检索组件视图模型的原始构造函数(VM)
  2. 动态创建一个 factory 函数:
    • 将传递的 params 添加到绑定上下文
    • 使用 shared 参数构造 VM 实例
    • returns 新视图模型
  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);
  }
});

运行 示例

查看此链接 fiddle 以自己使用自定义加载程序。我包含了几个嵌套的组件结构,以表明不再需要手动传递 params

https://jsfiddle.net/sqLathu9/

我做的是当C创建D时,C传递一个参数作为对D的引用,D将自己赋值给这个参数。 param可以是一个observable,D的onDispose可以清空observable,让C知道D是否被disposed。