本篇将通过实验来介绍 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.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>
效果: 下面我们来看看 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>
然后我们刷新页面,效果如下: 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 效果如下: 要点是:
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>
效果如下: 可以参考官方文档 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"}}}}
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?