{ Hello Magento 2 }

解决 Magento 2 应用问题,更注重深度挖掘。(ง •̀_•́)ง

0%

Magento 2 and konockoutjs

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

准备

ThankIT_HelloWorld 模块的基础上,我们来创建一个页面,用来做实验。 File: app\code\ThankIT\HelloWorld\Controller\Knockoutjs\Index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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

1
2
3
4
5
6
7
8
<?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

1
2
3
4
5
6
7
8
9
<?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

1
2
<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,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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 原来的用法是怎样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!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

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

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

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

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

ko 自定义绑定

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!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 自定义绑定 要点是:

1
2
3
4
5
6
7
8
9
10
11
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.
}
};

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

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

ko component 绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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() 输出的东西是这样的:

1
{"components":{"customer-list":{"component":"ThankIT_HelloWorld\/js\/view\/customer\/list","config":{"template":"ThankIT_HelloWorld\/customer\/list"&#125;&#125;&#125;&#125;

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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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?