(八)Knockout 组件 Components

概述 :组件和自定义元素

Components 是将UI代码组织成自包含的、可重用的块的一种强大而干净的方法。他们:

  • …可以表示单个控件/窗口小部件或应用程序的整个部分
  • …包含它们自己的视图,并且通常(可选地)包含它们自己的视图模型
  • …可以通过AMD或其他模块系统预加载,也可以(按需)异步加载
  • …可以接收参数,并选择性地将更改写回参数或调用回调
  • …可以组合在一起(嵌套)或从其他组件继承
  • …可以轻松打包,以便在项目间重用
  • …让您为配置和加载定义自己的约定/逻辑

这种模式对大型应用程序是有益的,因为它通过清晰的组织和封装简化了开发,并根据需要增量地加载应用程序代码和模板,从而帮助提高运行时性能

自定义元素是用于消费组件的可选但方便的语法。不需要使用占位符<div>来将绑定注入组件,您可以使用更多带有自定义元素名称的自描述性标记(例如,<voting-button> or <product-editor>))。淘汰赛会确保即使与IE 6等老浏览器兼容。

Example: A like/dislike widget

To get started, you can register a component using ko.components.register (technically, registration is optional, but it’s the easiest way to get started). A component definition specifies a viewModel and template. For example:
要开始,您可以使用ko.components.register注册一个组件(从技术上讲,注册是可选的,但这是最简单的方法)。组件定义指定视图模型和模板。例如

ko.components.register('like-widget', {
    viewModel: function(params) {
        // Data: value is either null, 'like', or 'dislike'
        this.chosenValue = params.value;
         
        // Behaviors
        this.like = function() { this.chosenValue('like'); }.bind(this);
        this.dislike = function() { this.chosenValue('dislike'); }.bind(this);
    },
    template:
        '<div class="like-or-dislike" data-bind="visible: !chosenValue()">
            <button data-bind="click: like">Like it</button>
            <button data-bind="click: dislike">Dislike it</button>
        </div>
        <div class="result" data-bind="visible: chosenValue">
            You <strong data-bind="text: chosenValue"></strong> it
        </div>'
});

通常,您会从外部文件加载视图模型和模板,而不是像这样内嵌声明它们。我们稍后再谈。

现在,要使用这个组件,您可以从应用程序中的任何其他视图引用它,或者使用component binding ,或者使用 custom element。下面是一个将它用作自定义元素的实时示例:

Source code: View

<ul data-bind="foreach: products">
    <li class="product">
        <strong data-bind="text: name"></strong>
        <like-widget params="value: userRating"></like-widget>
    </li>
</ul>

Source code: View model

function Product(name, rating) {
    this.name = name;
    this.userRating = ko.observable(rating || null);
}
 
function MyViewModel() {
    this.products = [
        new Product('Garlic bread'),
        new Product('Pain au chocolat'),
        new Product('Seagull spaghetti', 'like') // This one was already 'liked'
    ];
}
 
ko.applyBindings(new MyViewModel());

在本例中,组件在Product view model类上同时显示和编辑一个名为userRating的可观察属性。

Example: 根据需要,从外部文件加载 like/dislike 小部件

在大多数应用程序中,您都希望将组件视图模型和模板保存在外部文件中。如果将击倒配置为通过AMD模块加载器(如require.js)获取它们。然后,它们可以预先加载(可能是绑定/缩小),也可以根据需要增量加载。

下面是一个示例配置:

ko.components.register('like-or-dislike', {
    viewModel: { require: 'files/component-like-widget' },
    template: { require: 'text!files/component-like-widget.html' }
});

必要条件

为了让它发挥作用,文件files/component-like-widget.jsfiles/component-like-widget.html必须存在。检查它们(并在.html文件中查看源代码)——您将看到,这比在定义中内联代码更干净、更方便。

此外,您还需要引用一个合适的模块加载器库(如 require.js),或者实现一个知道如何获取文件的自定义组件加载器(custom component loader)。

使用组件

现在like-or-dislike可以像以前一样被消耗掉,使用component binding 或者 custom element

Source code: View

<ul data-bind="foreach: products">
    <li class="product">
        <strong data-bind="text: name"></strong>
        <like-or-dislike params="value: userRating"></like-or-dislike>
    </li>
</ul>
<button data-bind="click: addProduct">Add a product</button>

Source code: View model

function Product(name, rating) {
    this.name = name;
    this.userRating = ko.observable(rating || null);
}
 
function MyViewModel() {
    this.products = ko.observableArray(); // Start empty
}
 
MyViewModel.prototype.addProduct = function() {
    var name = 'Product ' + (this.products().length + 1);
    this.products.push(new Product(name));
};
 
ko.applyBindings(new MyViewModel());

如果在第一次单击“添加产品”之前打开浏览器开发人员工具的网络检查器,您将看到组件的 .js/.html文件在首次需要时按需提取,然后保留以供重复使用。

组件注册

要Knockout能够加载和实例化组件,必须使用ko.components.register注册它们,并提供如下所述的配置。

注意:作为一种替代方法,可以实现一个自定义组件加载器,它根据您自己的约定而不是显式配置来获取组件。

注册组件为一个 viewmodel/template 对

You can register a component as follows:

ko.components.register('some-component-name', {
    viewModel: <see below>,
    template: <see below>
});
  • 组件名可以是任何非空字符串。建议(但不是强制性的)使用小写的命令分隔字符串(例如 your-component-name),以便组件名称可以有效地用作自定义元素(例如<your-component-name>)。
  • viewModel 是可选的,并且可以采用下面描述的任何viewModel格式
  • template是必须的, 并且可以采用 下面描述的任何template 格式.

如果没有给出viewmodel,则将组件视为一个简单的HTML块,它将绑定到传递给组件的任何参数。

指定 viewmodel

视图模型可以用以下任何一种形式指定:

一个构造函数

function SomeComponentViewModel(params) {
    // 'params' is an object whose key/value pairs are the parameters
    // passed from the component binding or custom element.
    this.someProperty = params.something;
}
 
SomeComponentViewModel.prototype.doSomething = function() { ... };
 
ko.components.register('my-component', {
    viewModel: SomeComponentViewModel,
    template: ...
});

Knockout将为组件的每个实例调用构造函数一次,为每个实例生成单独的viewmodel对象。结果对象或其原型链上的属性(例如,上面示例中的somePropertydoSomething)可用于在组件的视图中绑定。

一个共享对象实例

如果希望组件的所有实例共享同一个viewmodel对象实例(通常不希望这样):

var sharedViewModelInstance = { ... };
 
ko.components.register('my-component', {
    viewModel: { instance: sharedViewModelInstance },
    template: ...
});

注意,需要指定 viewModel: { instance: object },而不仅仅是viewModel: object。这与下面的其他情况不同。

一个createViewModel工厂函数

如果希望在关联元素绑定到viewmodel之前对其运行任何设置逻辑,或者使用任意逻辑来决定实例化哪个viewmodel类:

ko.components.register('my-component', {
    viewModel: {
        createViewModel: function(params, componentInfo) {
            // - 'params' is an object whose key/value pairs are the parameters
            //   passed from the component binding or custom element
            // - 'componentInfo.element' is the element the component is being
            //   injected into. When createViewModel is called, the template has
            //   already been injected into this element, but isn't yet bound.
            // - 'componentInfo.templateNodes' is an array containing any DOM
            //   nodes that have been supplied to the component. See below.
 
            // Return the desired view model instance, e.g.:
            return new MyViewModel(params);
        }
    },
    template: ...
});

注意,通常,最好只通过custom bindings执行直接DOM操作,而不是从createViewModel内部对 componentInfo.element执行操作。这将导致更加模块化、可重用的代码。

如果您想要构建一个接受任意标记以影响其输出的组件(例如,将提供的标记注入自身的网格、列表、对话框或选项卡集),componentInfo.templateNodes数组非常有用。有关完整示例,请参见将标记传递到组件

一个AMD模块,其值描述viewmodel

如果您的页面中已经有一个AMD加载器(如require.js,那么您可以使用它来获取一个viewmodel。有关如何工作的更多细节,请参见下面介绍如何通过AMD加载组件。例子:

ko.components.register('my-component', {
    viewModel: { require: 'some/module/name' },
    template: ...
});

返回的AMD模块对象可以是viewmodel允许的任何形式。因此,它可以是一个构造函数,例如。

// AMD module whose value is a component viewmodel constructor
define(['knockout'], function(ko) {
    function MyViewModel() {
        // ...
    }
 
    return MyViewModel;
});

或共享对象实例,例如。

// AMD module whose value is a shared component viewmodel instance
define(['knockout'], function(ko) {
    function MyViewModel() {
        // ...
    }
 
    return { instance: new MyViewModel() };
});

createViewModel函数,例如。

// AMD module whose value is a 'createViewModel' function
define(['knockout'], function(ko) {
    function myViewModelFactory(params, componentInfo) {
        // return something
    }
     
    return { createViewModel: myViewModelFactory };
});

或者,即使你不太可能想这样做,一个不同的AMD模块的引用,例如

// AMD module whose value is a reference to a different AMD module,
// which in turn can be in any of these formats
define(['knockout'], function(ko) {
    return { module: 'some/other/module' };
});

指定模板

模板可以用以下任何一种形式指定。最常用的是现有的元素idAMD模块

现有元素ID

For example, the following element:

<template id='my-component-template'>
    <h1 data-bind='text: title'></h1>
    <button data-bind='click: doSomething'>Click me right now</button>
</template>

… 可以通过指定其ID来用作组件的模板:

ko.components.register('my-component', {
    template: { element: 'my-component-template' },
    viewModel: ...
});

注意,只有指定元素中的节点才会被克隆到组件的每个实例中。容器元素(在本例中为<template>元素)将不被视为组件模板的一部分。

您不仅限于使用<template>元素,而且这些元素(在支持它们的浏览器上)也很方便,因为它们不会自己呈现。任何其他元素类型也可以。

现有元素实例

如果代码中有对DOM元素的引用,可以将其用作模板标记的容器:

var elemInstance = document.getElementById('my-component-template');
 
ko.components.register('my-component', {
    template: { element: elemInstance },
    viewModel: ...
});

同样,只有指定元素中的节点将被克隆,以用作组件的模板。

一串字符串标记

ko.components.register('my-component', {
    template: '<h1 data-bind="text: title"></h1>
               <button data-bind="click: doSomething">Clickety</button>',
    viewModel: ...
});

This is mainly useful when you’re fetching the markup from somewhere programmatically (e.g., AMD - see below), or as a build system output that packages components for distribution, since it’s not very convenient to manually edit HTML as a JavaScript string literal.
这主要是有用的,当你从某处以编程方式获取标记(例如,AMD -见下文),或者作为构建系统输出包组件分发,因为它不是很方便手工编辑HTML作为JavaScript字符串文字。

一组DOM节点

如果以编程方式构建配置,并且有一个DOM节点数组,则可以将它们用作组件模板:

var myNodes = [
    document.getElementById('first-node'),
    document.getElementById('second-node'),
    document.getElementById('third-node')
];
 
ko.components.register('my-component', {
    template: myNodes,
    viewModel: ...
});

在本例中,所有指定的节点(及其后代节点)都将被克隆并连接到要实例化的组件的每个副本中。

一个文档片段

如果您正在以编程方式构建配置,并且您有一个DocumentFragment对象,那么您可以将它用作组件模板:

ko.components.register('my-component', {
    template: someDocumentFragmentInstance,
    viewModel: ...
});

由于文档片段可以有多个顶级节点,因此整个文档片段(不仅仅是顶级节点的后代)被视为组件模板。

一个AMD模块,其值描述一个模板

如果您的页面中已经有一个AMD加载器(如require.js),那么您可以使用它来获取模板。有关如何工作的更多细节,请参见下面介绍如何通过AMD加载组件。例子

ko.components.register('my-component', {
    template: { require: 'some/template' },
    viewModel: ...
});

返回的AMD模块对象可以是viewmodel允许的任何形式。因此,它可以是一个标记字符串,例如使用[require.js的文本插件](require.js’s text plugin)获取:

ko.components.register('my-component', {
    template: { require: 'text!path/my-html-file.html' },
    viewModel: ...
});

...或这里描述的任何其他表单,尽管其他表单在通过AMD获取模板时非常有用。

指定其他组件选项

除了(或代替)templateviewModel之外,组件配置对象还可以具有任意其他属性。此配置对象可用于您可能正在使用的任何自定义组件加载程序

控制同步/异步加载

如果组件配置具有一个boolean synchronous属性,则Knockout将使用此属性确定是否允许同步加载和注入组件。默认值为false(即,必须是异步的)。例如:

ko.components.register('my-component', {
    viewModel: { ... anything ... },
    template: { ... anything ... },
    synchronous: true // Injects synchronously if possible, otherwise still async
});

为什么组件加载通常是异步的?

通常,Knockout确保了组件加载以及组件注入总是异步完成,因为有时它别无选择,只能异步完成(例如,因为它涉及到对服务器的请求)。即使可以同步注入特定的组件实例(例如,因为组件定义已经被加载),它也会这样做。这种始终异步的策略是一个一致性问题,是从其他现代异步JavaScript技术(如AMD)继承而来的一个公认的惯例。约定是一个安全的缺省值——它减轻了潜在的错误,在这些错误中,开发人员可能没有考虑到典型异步过程的可能性,有时同步完成,反之亦然。

为什么要启用同步加载?

如果要更改特定组件的策略,可以在该组件的配置中指定synchronous: true。然后它可能在第一次使用时异步加载,随后在所有后续使用中同步加载。如果您这样做,那么您需要在任何等待组件加载的代码中考虑这种可变行为。但是,如果您的组件总是可以同步加载和初始化,那么启用此选项将确保一致的同步行为。如果您在foreach绑定中使用组件,并且希望使用 afterAddafterRender 选项进行后处理,这可能很重要。

在Knockout 3.4.0之前,您可能需要使用同步加载来防止多个DOM在同时包含多个组件时发生重流(例如使用foreach绑定)。在Knockout 3.4.0中,组件使用Knockout的微指令(microtasks )来确保异步性,因此通常会执行同步加载。

Knockout如何通过AMD加载组件

当您通过require 声明加载视图模型或模板时,例如:

ko.components.register('my-component', {
    viewModel: { require: 'some/module/name' },
    template: { require: 'text!some-template.html' }
});

...所有Knockout都是调用require(['some/module/name'], callback)require(['text!some-template.html'], callback),并使用异步返回的对象作为视图模型和模板 定义。 所以,

  • 这并不严格依赖于require.js 或任何其他特定的模块加载器。任何提供amd风格的模块加载器都需要API。如果希望与API不同的模块加载器集成,可以实现自定义组件加载器。
  • Knockout并不以任何方式解释模块名——它只是将模块名传递给require()。因此,淘汰赛当然不知道或不关心从哪里加载模块文件。这取决于你的AMD加载器和你如何配置它。
  • Knockout不知道或关心你的AMD模块是否匿名。通常,我们发现将组件定义为匿名模块是最方便的,但这与KO完全无关。

AMD模块只按需加载

在实例化组件之前,Knockout不会调用require([moduleName], ...)。这是组件按需加载的方式,而不是预先加载。

例如,如果组件位于具有if binding(或另一个控制流绑定)的其他元素中,则在if条件为真之前,不会导致加载AMD模块。当然,如果AMD模块已经加载(例如,在一个预加载包中),那么require调用将不会触发任何额外的HTTP请求,因此您可以控制什么是预加载的,什么是按需加载的。

将组件注册为单个AMD模块

为了更好的封装,您可以将组件封装到一个自描述的AMD模块中。然后,您可以简单地引用组件:

ko.components.register('my-component', { require: 'some/module' });

请注意,没有指定视图 viewmodel/template 对。AMD模块本身可以使用上面列出的任何定义格式提供 viewmodel/template 对。例如,文件 some/module.js可以声明为::

// AMD module 'some/module.js' encapsulating the configuration for a component
define(['knockout'], function(ko) {
    function MyComponentViewModel(params) {
        this.personName = ko.observable(params.name);
    }
 
    return {
        viewModel: MyComponentViewModel,
        template: 'The name is <strong data-bind="text: personName"></strong>'
    };
});

推荐AMD的模块模式

在实践中最有用的是创建具有内联视图模型类的AMD模块,并显式地依赖于外部模板文件。

例如,如果以下内容在path/my-component.js的文件中,

// Recommended AMD module pattern for a Knockout component that:
//  - Can be referenced with just a single 'require' declaration
//  - Can be included in a bundle using the r.js optimizer
define(['knockout', 'text!./my-component.html'], function(ko, htmlString) {
    function MyComponentViewModel(params) {
        // Set up properties, etc.
    }
 
    // Use prototype to declare any public methods
    MyComponentViewModel.prototype.doSomething = function() { ... };
 
    // Return component definition
    return { viewModel: MyComponentViewModel, template: htmlString };
});

... 模板标记在文件path/my-component.html中,那么您有以下好处:

  • 应用程序可以简单地引用这一点,即ko.components.register('my-component', { require: 'path/my-component' });
  • 组件只需要两个文件——一个 viewmodel(path/my-component.js) 和一个 template (path/my-component.html) ,这是开发过程中非常自然的安排。
  • 由于在 define调用中显式地声明了对模板的依赖关系,这将自动与 r.js optimizer或类似的捆绑工具一起工作。因此,在构建步骤中,整个组件(viewmodel + template)可以简单地包含在一个bundle文件中。
    • Note: Since the r.js optimizer is very flexible, it has a lot of options and can take some time to set up. You may want to start from a ready-made example of Knockout components being optimized through r.js, in which case see Yeoman and the generator-ko generator. Blog post coming soon.
    • 注意:由于r.js optimizer非常灵活,它有很多选项,可能需要一些时间来设置。您可能想从一个通过r.js优化Knockout组件的现成示例开始。在这种情况下,请参阅e Yeomangenerator-ko 生成器。博客文章即将发布。

自定义元素

自定义元素提供了一种将组件注入视图的方便方法。

Introduction

自定义元素是 component binding 的语法替代(实际上,自定义元素在幕后使用组件绑定)。

例如,与其写这个:

<div data-bind='component: { name: "flight-deals", params: { from: "lhr", to: "sfo" } }'></div>

你可以写:

<flight-deals params='from: "lhr", to: "sfo"'></flight-deals>

这允许以一种非常现代的、类似于web组件的方式组织代码,同时保留对非常旧的浏览器的支持(请参阅自定义元素和IE 6到8)。

Example

这个例子声明了一个组件,然后将它的两个实例注入到一个视图中。参见下面的源代码。

Source code: View

<h4>First instance, without parameters</h4>
<message-editor></message-editor>
 
<h4>Second instance, passing parameters</h4>
<message-editor params='initialText: "Hello, world!"'></message-editor>

Source code: View model

ko.components.register('message-editor', {
    viewModel: function(params) {
        this.text = ko.observable(params.initialText || '');
    },
    template: 'Message: <input data-bind="value: text" /> '
            + '(length: <span data-bind="text: text().length"></span>)'
});
 
ko.applyBindings();

注意:在更实际的情况下,您通常会从外部文件加载组件视图模型和模板,而不是将它们硬编码到注册中。参见示例注册文档

传递参数

正如您在上面的示例中所看到的,您可以使用params属性向组件视图模型提供参数。params属性的内容被解释为JavaScript对象文本(就像data-bind 属性一样),因此可以传递任何类型的任意值。例子:

<unrealistic-component
    params='stringValue: "hello",
            numericValue: 123,
            boolValue: true,
            objectValue: { a: 1, b: 2 },
            dateValue: new Date(),
            someModelProperty: myModelValue,
            observableSubproperty: someObservable().subprop'>
</unrealistic-component>

父组件和子组件之间的通信

如果您在params属性中引用模型属性,那么您当然是在引用组件(父视图模型或主机视图模型)之外的视图模型上的属性,因为组件本身还没有实例化。在上面的例子中,myModelValue将是父视图模型上的一个属性,子组件viewmodel的构造函数将以 params.someModelProperty的形式接收它。

这就是如何将属性从父视图模型传递到子组件。如果属性本身是可观察的,那么父视图模型将能够观察并响应子组件插入到它们中的任何新值。

传递 observable 表达式

在下面的示例中,

<some-component
    params='simpleExpression: 1 + 1,
            simpleObservable: myObservable,
            observableExpression: myObservable() + 1'>
</some-component>

... 组件viewmodel的params参数将包含三个值:

  • simpleExpression

    • 这将是数值2。它将不是一个可观察值或计算值,因为不涉及可观察值。

      通常,如果参数的计算不涉及对可观察值的计算(在本例中,该值根本不涉及可观察值),则按字面意义传递该值。如果值是一个对象,那么子组件可以对它进行修改,但是由于它是不可观察的,所以父组件不会知道子组件已经这样做了。

  • simpleObservable

    • 这将是在父视图模型上声明为myObservableko.observable实例。它不是包装器——它实际上与父级引用的实例相同。因此,如果子视图模型写入这个可观察到的内容,父视图模型将接收到这个更改。

      一般来说,如果一个参数的评估不涉及评估一个可观测值(在这种情况下,可观测值只是简单地传递而没有评估它),那么该值就按字面意义传递。

  • observableExpression

    • 这个更棘手。表达式本身在计算时读取一个可观察值。可observable的值会随时间变化,所以表达式结果也会随时间变化。

      为了确保子组件能够对表达式值的更改做出反应,Knockout将自动将该参数升级为计算属性。因此,子组件将能够读取params.observableExpression()来获取当前值,或者使用params.observableExpression(...)等。

      通常,对于自定义元素,如果参数的计算涉及到计算一个可观察到的值,那么敲除将自动构造一个ko.computed值来给出表达式s的结果,并将其提供给组件。

总之,总的规则是:

  1. 如果一个参数的评估不涉及评估一个observable/computed,那么它就是字面上传递的。
  2. 如果参数的评估涉及评估一个或多个 observables/computeds,它将作为计算属性传递,以便您可以对参数值的变化做出反应。

将标记传递到组件

Sometimes you may want to create a component that receives markup and uses it as part of its output. For example, you may want to build a “container” UI element such as a grid, list, dialog, or tab set that can receive and bind arbitrary markup inside itself.

Consider a special list component that can be invoked as follows:

<my-special-list params="items: someArrayOfPeople">
    <!-- Look, I'm putting markup inside a custom element -->
    The person <em data-bind="text: name"></em>
    is <em data-bind="text: age"></em> years old.
</my-special-list>

By default, the DOM nodes inside <my-special-list> will be stripped out (without being bound to any viewmodel) and replaced by the component’s output. However, those DOM nodes aren’t lost: they are remembered, and are supplied to the component in two ways:

  • As an array, $componentTemplateNodes, available to any binding expression in the component’s template (i.e., as a binding context property). Usually this is the most convenient way to use the supplied markup. See the example below.
  • As an array, componentInfo.templateNodes, passed to its createViewModel function

The component can then choose to use the supplied DOM nodes as part of its output however it wishes, such as by using template: { nodes: $componentTemplateNodes } on any element in the component’s template.

For example, the my-special-list component’s template can reference $componentTemplateNodes so that its output includes the supplied markup. Here’s the complete working example:

Source code: View

<!-- This could be in a separate file -->
<template id="my-special-list-template">
    <h3>Here is a special list</h3>
 
    <ul data-bind="foreach: { data: myItems, as: 'myItem' }">
        <li>
            <h4>Here is another one of my special items</h4>
            <!-- ko template: { nodes: $componentTemplateNodes, data: myItem } --><!-- /ko -->
        </li>
    </ul>
</template>
 
<my-special-list params="items: someArrayOfPeople">
    <!-- Look, I'm putting markup inside a custom element -->
    The person <em data-bind="text: name"></em>
    is <em data-bind="text: age"></em> years old.
</my-special-list>

Source code: View model

ko.components.register('my-special-list', {
    template: { element: 'my-special-list-template' },
    viewModel: function(params) {
        this.myItems = params.items;
    }
});
 
ko.applyBindings({
    someArrayOfPeople: ko.observableArray([
        { name: 'Lewis', age: 56 },
        { name: 'Hathaway', age: 34 }
    ])
});

This “special list” example does nothing more than insert a heading above each list item. But the same technique can be used to create sophisticated grids, dialogs, tab sets, and so on, since all that is needed for such UI elements is common UI markup (e.g., to define the grid or dialog’s heading and borders) wrapped around arbitrary supplied markup.

This technique is also possible when using components without custom elements, i.e., passing markup when using the component binding directly.

控制自定义元素标记名称

By default, Knockout assumes that your custom element tag names correspond exactly to the names of components registered using ko.components.register. This convention-over-configuration strategy is ideal for most applications.

If you want to have different custom element tag names, you can override getComponentNameForNode to control this. For example,

ko.components.getComponentNameForNode = function(node) {
    var tagNameLower = node.tagName && node.tagName.toLowerCase();
 
    if (ko.components.isRegistered(tagNameLower)) {
        // If the element's name exactly matches a preregistered
        // component, use that component
        return tagNameLower;
    } else if (tagNameLower === "special-element") {
        // For the element <special-element>, use the component
        // "MySpecialComponent" (whether or not it was preregistered)
        return "MySpecialComponent";
    } else {
        // Treat anything else as not representing a component
        return null;
    }
}

You can use this technique if, for example, you want to control which subset of registered components may be used as custom elements.

注册自定义元素

If you are using the default component loader, and hence are registering your components using ko.components.register, then there is nothing extra you need to do. Components registered this way are immediately available for use as custom elements.

If you have implemented a custom component loader, and are not using ko.components.register, then you need to tell Knockout about any element names you wish to use as custom elements. To do this, simply call ko.components.register - you don’t need to specify any configuration, since your custom component loader won’t be using the configuration anyway. For example,

ko.components.register('my-custom-element', { /* No config needed */ });

Alternatively, you can override getComponentNameForNode to control dynamically which elements map to which component names, independently of preregistration.

Note: 将定制元素与常规绑定组合在一起

A custom element can have a regular data-bind attribute (in addition to any params attribute) if needed. For example,

<products-list params='category: chosenCategory'
               data-bind='visible: shouldShowProducts'>
</products-list>

However, it does not make sense to use bindings that would modify the element’s contents, such as the text or template bindings, since they would overwrite the template injected by your component.

Knockout will prevent the use of any bindings that use controlsDescendantBindings, because this also would clash with the component when trying to bind its viewmodel to the injected template. Therefore if you want to use a control flow binding such as if or foreach, then you must wrap it around your custom element rather than using it directly on the custom element, e.g.,:

<!-- ko if: someCondition -->
    <products-list></products-list>
<!-- /ko -->

或者:

<ul data-bind='foreach: allProducts'>
    <product-details params='product: $data'></product-details>
</ul>

Note: 自定义元素不能自己关闭

You must write <my-custom-element></my-custom-element>, and not <my-custom-element />. Otherwise, your custom element is not closed and subsequent elements will be parsed as child elements.

This is a limitation of the HTML specification and is outside the scope of what Knockout can control. HTML parsers, following the HTML specification, ignore any self-closing slashes (except on a small number of special “foreign elements”, which are hardcoded into the parser). HTML is not the same as XML.

Note: 自定义元素和Internet Explorer 6到8

Knockout tries hard to spare developers the pain of dealing with cross-browser compatiblity issues, especially those relating to older browsers! Even though custom elements provide a very modern style of web development, they still work on all commonly-encountered browsers:

  • HTML5-era browsers, which includes Internet Explorer 9 and later, automatically allow for custom elements with no difficulties.
  • Internet Explorer 6 to 8 also supports custom elements, but only if they are registered before the HTML parser encounters any of those elements.

IE 6-8’s HTML parser will discard any unrecognized elements. To ensure it doesn’t throw out your custom elements, you must do one of the following:

  • Ensure you call ko.components.register('your-component') before the HTML parser sees any <your-component> elements
  • Or, at least call document.createElement('your-component') before the HTML parser sees any <your-component> elements. You can ignore the result of the createElement call — all that matters is that you have called it.

For example, if you structure your page like this, then everything will be OK:

<!DOCTYPE html>
<html>
    <body>
        <script src='some-script-that-registers-components.js'></script>
 
        <my-custom-element></my-custom-element>
    </body>
</html>

If you’re working with AMD, then you might prefer a structure like this:

<!DOCTYPE html>
<html>
    <body>
        <script>
            // Since the components aren't registered until the AMD module
            // loads, which is asynchronous, the following prevents IE6-8's
            // parser from discarding the custom element
            document.createElement('my-custom-element');
        </script>
 
        <script src='require.js' data-main='app/startup'></script>
 
        <my-custom-element></my-custom-element>
    </body>
</html>

Or if you really don’t like the hackiness of the document.createElement call, then you could use a component binding for your top-level component instead of a custom element. As long as all other components are registered before your ko.applyBindings call, they can be used as custom elements on IE6-8 without futher trouble:

<!DOCTYPE html>
<html>
    <body>
        <!-- The startup module registers all other KO components before calling
             ko.applyBindings(), so they are OK as custom elements on IE6-8 -->
        <script src='require.js' data-main='app/startup'></script>
 
        <div data-bind='component: "my-custom-element"'></div>
    </body>
</html>

高级:访问$raw参数

Consider the following unusual case, in which useObservable1, observable1, and observable2 are all observables:

<some-component
    params='myExpr: useObservable1() ? observable1 : observable2'>
</some-component>

Since evaluating myExpr involves reading an observable (useObservable1), KO will supply the parameter to the component as a computed property.

However, the value of the computed property is itself an observable. This would seem to lead to an awkward scenario, where reading its current value would involve double-unwrapping (i.e., params.myExpr()(), where the first parentheses give the value of the expression, and the second give the value of the resulting observable instance).

This double-unwrapping would be ugly, inconvenient, and unexpected, so Knockout automatically sets up the generated computed property (params.myExpr) to unwrap its value for you. That is, the component can read params.myExpr() to get the value of whichever observable has been selected (observable1 or observable2), without the need for double-unwrapping.

In the unlikely event that you don’t want the automatic unwrapping, because you want to access the observable1/observable2 instances directly, you can read values from params.$raw. For example,

function MyComponentViewModel(params) {
    var currentObservableInstance = params.$raw.myExpr();
     
    // Now currentObservableInstance is either observable1 or observable2
    // and you would read its value with "currentObservableInstance()"
}

This should be a very unusual scenario, so normally you will not need to work with $raw.

原文地址:https://www.cnblogs.com/tangge/p/10857318.html