Magento 2 and konockoutjs

本篇将通过实验来介绍 ko 的模版绑定、自定义绑定和组件绑定,然后在此基础上介绍 magento 2 对 ko 的扩展。最后通过一个例子来介绍如何在 magento 2 中使用 ko 绑定。

准备

ThankIT_HelloWorld 模块的基础上,我们来创建一个页面,用来做实验。

File: app\code\ThankIT\HelloWorld\Controller\Knockoutjs\Index.php

<?php
namespace ThankIT\HelloWorld\Controller\Knockoutjs;

class Index extends \Magento\Framework\App\Action\Action
{

    protected $resultPageFactory;

    /**
     * Constructor
     *
     * @param \Magento\Framework\App\Action\Context  $context
     * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
     */
    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory
    ) {
        $this->resultPageFactory = $resultPageFactory;
        parent::__construct($context);
    }

    /**
     * Execute view action
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        return $this->resultPageFactory->create();
    }
}

File:app\code\ThankIT\HelloWorld\view\frontend\layout\helloworld_knockoutjs_index.xml

<?xml version="1.0" ?>
<page layout="1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="ThankIT\HelloWorld\Block\Knockoutjs\Index" name="knockoutjs.index" template="ThankIT_HelloWorld::knockoutjs/index.phtml"/>
        </referenceContainer>
    </body>
</page>

File:app\code\ThankIT\HelloWorld\Block\Knockoutjs\Index.php

<?php
namespace ThankIT\HelloWorld\Block\Knockoutjs;

use Magento\Framework\View\Element\Template;

class Index extends Template
{

}

File:app\code\ThankIT\HelloWorld\view\frontend\templates\knockoutjs\index.phtml

<p>First name: <strong data-bind="text:firstName">todo</strong></p>
<p>Last name: <strong data-bind="text:lastName">todo</strong></p>

注意:Magento 2 的 js 框架部分变化比较频繁,2.0 和 2.1 之间有较大差异,本文使用的是 2.1.7

然后我们清空缓存,刷新页面,打开调试面板,会看到:
ko

假设我们在普通的页面中,我们尚未应用 ko.applyBindings(obj),那么 data-bind 应该被浏览器忽略,我们是不会看到这样的错误的。

所以 Magento 2 页面中自动调用了 ko.applyBindings(obj)

调用 ko.applyBindings() 部分的实现位于 Magento_Ui/js/lib/knockout/bootstrap 具体的文件位置位于 vendor\magento\module-ui\view\base\web\js\lib\knockout\bootstrap.js,文件内容如下:

/**
 * Copyright © 2013-2017 Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
/** Loads all available knockout bindings, sets custom template engine, initializes knockout on page */

define([
    'ko',
    './template/engine',
    'knockoutjs/knockout-es5',
    './bindings/bootstrap',
    './extender/observable_array',
    './extender/bound-nodes',
    'domReady!'
], function (ko, templateEngine) {
    'use strict';

    ko.uid = 0;

    ko.setTemplateEngine(templateEngine);
    ko.applyBindings();
});

至于他是怎样实现页面加载后,自动调用 ko.applyBindings() 的,此处先挖坑,留着以后填。

这里先明白,Magento 2 对 ko 做了自定义,他会让 ko 自动进行初始化。

template binding

我们先来看一个 ko 的模版绑定,看看 ko 原来的用法是怎样的。

<!DOCTYPE html>
<html>
<head>
    <title>ko</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
</head>
<body>
<h2>Participants</h2>
Here are the participants:
<div data-bind="template: { name: 'person-template', data: buyer }"></div>
<div data-bind="template: { name: 'person-template', data: seller }"></div>
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>
<script type="text/javascript">
     function MyViewModel() {
         this.buyer = { name: 'Franklin', credits: 250 };
         this.seller = { name: 'Mario', credits: 5800 };
     }
     ko.applyBindings(new MyViewModel());
</script>
</body>
</html>

效果:
ko 模版绑定

下面我们来看看 Magento 2 对 ko 模版引擎的改造后的用法。
将 phtml 改成以下内容:
File:app\code\ThankIT\HelloWorld\view\frontend\templates\knockoutjs\index.phtml

<div data-bind="template:'ThankIT_HelloWorld/hello'"></div>

然后我们创建以下文件:
File:app\code\ThankIT\HelloWorld\view\frontend\web\template\hello.html

<p data-bind="style:{color:'red'}">Hello World</p>

然后我们刷新页面,效果如下:
m2 模版引擎

Magento 2 对 ko 模版引擎的改造让我们不需要用 <script type="text/html"> 来声明模版,把模版也从 phtml 文件中分离到独立的 html 文件中,有利于模版的复用。

ko 自定义绑定

我们先来看看 ko 的自定义绑定是怎样的:

<!DOCTYPE html>
<html>
<head>
    <title>ko</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<label><input type="checkbox" data-bind="checked: giftWrap" /> Gift wrap</label>
<div data-bind="slideVisible: giftWrap, slideDuration:600, test:'just for test'">You have selected the option</div>
<script type="text/javascript">
    var viewModel = {
        giftWrap: ko.observable(true)
    };
    ko.bindingHandlers.slideVisible = {
    update: function(element, valueAccessor, allBindings) {
        // console.log(element);
        // First get the latest data that we're bound to
        var value = valueAccessor();
        // console.log(value);
        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);
        // console.log(valueUnwrapped);
        // console.log(allBindings);
        // Grab some more data from another binding property
        var duration = allBindings.get('slideDuration') || 400; // 400ms is default duration unless otherwise specified
        console.log(allBindings.get('test'));
        // Now manipulate the DOM element
        if (valueUnwrapped == true)
            $(element).slideDown(duration); // Make the element visible
        else
            $(element).slideUp(duration);   // Make the element invisible
        }
    };
    ko.applyBindings(viewModel);
</script>
</body>
</html>

可参考官方文档 Creating custom bindings

效果如下:
ko 自定义绑定

要点是:

ko.bindingHandlers.yourBindingName = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called when the binding is first applied to an element
        // Set up any initial state, event handlers, etc. here
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called once when the binding is first applied to an element,
        // and again whenever any observables/computeds that are accessed change
        // Update the DOM element based on the supplied values here.
    }
};

一旦注册了自定义绑定,就可以这样使用:

<div data-bind="yourBindingName: someValue"> </div>

ko component 绑定

<!DOCTYPE html>
<html>
<head>
    <title>ko</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<h4>First instance, without parameters</h4>
<div data-bind='component: "message-editor"'></div>

<h4>Second instance, passing parameters</h4>
<div data-bind='component: {
    name: "message-editor",
    params: { initialText: "Hello, world!" }
}'></div>


<script type="text/javascript">
    ko.components.register('message-editor', {
        viewModel: function(params) {
        /**
         *   几乎所有语言中 || 和 && 都遵循“短路”原理,
         *   如 && 中第一个表达式为假就不会去处理第二个表达式,而 || 正好相反。
         *   js 也遵循上述原则。
         *   当 || 时,找到为 true 的分项就停止处理,并返回该分项的值,否则执行完,并返回最后分项的值。
         *   当 && 时,找到为 false 的分项就停止处理,并返回该分项的值。
         **/
            this.text = ko.observable(params && params.initialText || '');
        },
        template: 'Message: <input data-bind="value: text" /> '+ '(length: <span data-bind="text: text().length"></span>)'
    });

    ko.applyBindings();
</script>
</body>
</html>

效果如下:
ko-component-binding

可以参考官方文档 The “component” binding

component 组合了 view model 和 template ,我们向组件传递参数来设置 view model ,注意,此处 ko.applyBindings() 并没有传递 obj

回到 Magento 2 中,上面讲到 M2 自动调用了 ko.applyBindings() 也没有传递参数,那么 M2 一定是对 ko 做了类似组件的绑定。

magento 2 中使用 ko 绑定

下面我们来做一个例子

File:app\code\ThankIT\HelloWorld\view\frontend\layout\helloworld_knockoutjs_index.xml

<?xml version="1.0" ?>
<page layout="1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="ThankIT\HelloWorld\Block\Knockoutjs\Index" name="knockoutjs.index" template="ThankIT_HelloWorld::knockoutjs/index.phtml">
                <arguments>
                    <argument name="jsLayout" xsi:type="array">
                        <item name="components" xsi:type="array">
                            <item name="customer-list" xsi:type="array">
                                <item name="component" xsi:type="string">ThankIT_HelloWorld/js/view/customer/list</item>
                                <item name="config" xsi:type="array">
                                    <item name="template" xsi:type="string">ThankIT_HelloWorld/customer/list</item>
                                </item>
                            </item>
                        </item>
                    </argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

通过 arguments 传递参数,如果不清楚 layout 中 arguments 的用法,参考 Layout instruction 之 argument 和 action

上面我们这里传递进去一个数组,相当于给 block 对象设置了 $this->jsLayout 的值。

File: app\code\ThankIT\HelloWorld\Block\Knockoutjs\Index.php

<?php
namespace ThankIT\HelloWorld\Block\Knockoutjs;

use Magento\Framework\View\Element\Template;

class Index extends Template
{
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        array $layoutProcessors = [],
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->layoutProcessors = $layoutProcessors;
    }
    public function getJsLayout()
    {
        foreach ($this->layoutProcessors as $processor) {
            $this->jsLayout = $processor->process($this->jsLayout);
        }
        return parent::getJsLayout();
    }
}

File:app\code\ThankIT\HelloWorld\view\frontend\templates\knockoutjs\index.phtml

<div data-bind="template:'ThankIT_HelloWorld/hello'"></div>
<div id="customer-page">
    <div id="block-customer-list" data-bind="scope:'customer-list'" class="block">
    <?php echo $block->getJsLayout();?>
        <!-- ko template: getTemplate() --><!-- /ko -->
        <script type="text/x-magento-init">
            {
                "#block-customer-list": {
                    "Magento_Ui/js/core/app": <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>
                }
            }
      </script>
    </div>
</div>

这里我们调用了 block 中的 getJsLayout() 方法,返回一个 json ,然后我们将它搁在 <script type="text/x-magento-init"> 中,可参考<script type="text/x-magento-init">

它将载入 Magento_Ui/js/core/app 模块,并且将 getJsLayout() 方法输出的 json 对象和作用节点 #block-customer-list 传递给它并执行。

getJsLayout() 输出的东西是这样的:

{"components":{"customer-list":{"component":"ThankIT_HelloWorld\/js\/view\/customer\/list","config":{"template":"ThankIT_HelloWorld\/customer\/list"}}}}

customer-list 正对应着 <div id="block-customer-list" data-bind="scope:'customer-list'" class="block">

data-bind="scope:'customer-list'" 中的 scope 是 magento 2 创建的自定义绑定。

File:app\code\ThankIT\HelloWorld\view\frontend\web\js\view\customer\list.js

define(['jquery', 'ko', 'uiComponent'], function($, ko, Component) {
    "use strict";
    var listCustomers = ko.observableArray([]);
    return Component.extend({
        getListCustomers: function() {
            if (!listCustomers().length) {
                listCustomers([{"entity_id": 1,"email":"123@qq.com","firstname":"wendy","lastname":"wu","created_at":"2017"}]);
            }
            return listCustomers;
        }
    });
});

File: app\code\ThankIT\HelloWorld\view\frontend\web\template\customer\list.html

<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>Email</th>
            <th>Name</th>
            <th>Created At</th>
        </tr>
    </thead>
    <tbody data-bind="foreach: getListCustomers()">
        <tr>
            <td data-bind="text: entity_id"></td>
            <td data-bind="text: email"></td>
            <td data-bind="text: firstname  + ' ' + lastname"></td>
            <td data-bind="text: created_at"></td>
        </tr>
    </tbody>
</table>

下面访问 url http://example.com/helloworld/knockoutjs/index/

结果如下:

magento 2 是如何做到的?

How Does Magento 2 Apply KnockoutJS Bindings

Magento 2: How/Where is the getTemplate Knockout Function Bound?

发表评论

电子邮件地址不会被公开。 必填项已用*标注