系统学习magento二次开发,推荐小册:《Magento中文全栈二次开发 》
本小册面向Magento2以上版本,书代码及示例兼容magento2.0-2.4版本。涵盖了magento前端开发,后端开发,magento2主题,magento2重写,magento2 layout,magento2控制器,magento2 block等相关内容,带领您成为magento开发技术专家。
虽然KnockoutJS将自己标榜为MVVM(模型,视图,视图模型)框架,但PHP开发人员会发现模型部分有点薄。KnockoutJS本身没有数据存储的原生概念,并且像许多现代JavaScript框架一样,它被设计为与仅服务后端配合使用的最佳效果。即 KnockoutJS 的“模型”是其他一些框架,发出 AJAX 请求以填充视图模型值。
KnockoutJS可能会让你措手不及的另一件事是,它不是一个“全栈”的javascript应用程序框架(值得称赞的是,它不会这样标榜自己)。KnockoutJS对你如何把它包含在你的项目中,或者你如何组织你的代码没有意见(尽管文档清楚地表明KnockoutJS团队成员是RequireJS的粉丝)。
这给像Magento这样的服务器端PHP框架带来了一个有趣的挑战。不仅有一定程度的JavaScript脚手架需要围绕KnockoutJS,而且Magento 2不是一个纯服务的框架。虽然Magento 2的新API功能正在朝着这个方向大踏步前进,但Magento 2并不是一个纯服务的框架。即后端框架开发人员还需要构建脚手架以将业务对象数据放入 KnockoutJS。
今天我们将深入探讨Magento 2的KnockoutJS集成。在本教程结束时,您将了解Magento 2如何应用KnockoutJS绑定以及Magento 2如何初始化自己的自定义绑定。您还将了解Magento如何修改一些核心的KnockoutJS行为,为什么他们这样做,以及这些更改为您自己的应用程序和模块打开的其他可能性。
本文是介绍Magento 2中高级JavaScript概念的较长系列的一部分。虽然阅读之前的文章不是100%强制性的,但如果您在下面的概念上苦苦挣扎,您可能需要在下面的评论中指出您的Magento Stack Exchange问题之前查看以前的文章。
### 创建Magento模块
虽然这篇文章是javascript的重,但我们希望我们的示例代码在具有Magento基线HTML的页面上运行。这意味着添加一个新模块。我们将按照本系列第一篇文章中相同的方式执行此操作,并使用 pestle 创建一个具有 URL 端点的模块
$ pestle.phar generate_module Pulsestorm KnockoutTutorial 0.0.1 $ pestle.phar generate_route Pulsestorm_KnockoutTutorial frontend pulsestorm_knockouttutorial $ pestle.phar generate_view Pulsestorm_KnockoutTutorial frontend pulsestorm_knockouttutorial_index_index Main content.phtml 1column $ php bin/magento module:enable Pulsestorm_KnockoutTutorial $ php bin/magento setup:upgrade
任何通过Magento 2 for PHP MVC开发人员系列工作的人都应该熟悉这些命令。运行上述操作后,您应该能够在系统中访问以下URL
http://magento.example.com/pulsestorm_knockouttutorial/
并查看呈现的模板。Pestle在这里不是强制性的 - 如果您有使用Magento页面的首选方式,请随意使用它。app/code/Pulsestorm/KnockoutTutorial/view/frontend/templates/content.phtml
### 需要JS初始化
在我们上一篇文章和官方的 KnockoutJS 教程中,KnockoutJS 初始化是一件简单的事情。
object = SomeViewModelConstructor(); ko.applyBindings(object);
对于教程应用程序,这是有道理的。但是,如果您要将所有视图模型逻辑,自定义绑定,组件等保留在单个代码块中,则KnockoutJS将很快变得难以管理。
相反,Magento的核心团队创建了RequireJS模块,当它被列为依赖项时,将执行任何和所有KnockoutJS初始化。你可以像这样使用这个模块Magento_Ui/js/lib/ko/initialize
requirejs(['Magento_Ui/js/lib/ko/initialize'], function(){ //your program here });
关于这个 RequireJS 模块需要注意的一件有趣的事情是它不返回任何值。相反,将RequireJS模块列为依赖项的唯一目的是启动Magento的KnockoutJS集成。当您在野外看到它时,这可能会让您感到困惑。例如,考虑来自不同Magento RequireJS模块的这段代码。
#File: vendor/magento/module-ui/view/base/web/js/core/app.js define([ './renderer/types', './renderer/layout', 'Magento_Ui/js/lib/ko/initialize' ], function (types, layout) { 'use strict'; return function (data) { types.set(data.types); layout(data.components); }; });
声明了三个 RequireJS 依赖项,
#File: vendor/magento/module-ui/view/base/web/js/core/app.js [ './renderer/types', './renderer/layout', 'Magento_Ui/js/lib/ko/initialize' ]
但在生成的函数中仅使用两个参数
#File: vendor/magento/module-ui/view/base/web/js/core/app.js function (types, layout) { //... }
我不清楚这是否是一种聪明的编程,或者它是否违反了 RequireJS 的精神。也许两者兼而有之。
无论如何,当你第一次在你自己的基于RequireJS的程序中使用这个库时,Magento将初始化KnockoutJS。后续包含实际上不会执行任何操作,因为 RequireJS 会在您第一次加载模块时缓存它们。
### KnockoutJS 初始化
如果我们看一下模块的来源Magento_Ui/js/lib/ko/initialize
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js define([ 'ko', './template/engine', 'knockoutjs/knockout-repeat', 'knockoutjs/knockout-fast-foreach', 'knockoutjs/knockout-es5', './bind/scope', './bind/staticChecked', './bind/datepicker', './bind/outer_click', './bind/keyboard', './bind/optgroup', './bind/fadeVisible', './bind/mage-init', './bind/after-render', './bind/i18n', './bind/collapsible', './bind/autoselect', './extender/observable_array', './extender/bound-nodes' ], function (ko, templateEngine) { 'use strict'; ko.setTemplateEngine(templateEngine); ko.applyBindings(); });
我们看到一个相对简单的程序,但它还包括其他十九个模块。介绍每个模块的功能超出了本文的范围。考虑以下精彩片段。
模块是模块的别名。koknockoutjs/knockout
vendor/magento/module-theme/view/base/requirejs-config.js 11: "ko": "knockoutjs/knockout", 12: "knockout": "knockoutjs/knockout"
该模块是实际的挖空库文件。,和
模块是KnockoutJS社区的额外功能。这些都不是正式的 RequireJS 模块。knockoutjs/knockoutknockoutjs/knockout-repeatknockoutjs/knockout-fast-foreachknockoutjs/knockout-es5
以 开头的模块是 Magento 对 KnockoutJS 的自定义绑定。这些是正式的 RequireJS 模块,但实际上并不返回模块。相反,每个脚本操作全局对象以向 KnockoutJS 添加绑定。我们将在下面讨论绑定,但如果您是好奇的类型,请尝试调查其他绑定的实现细节。这是一个有用的练习。希望Magento尽快获得我们的官方文档。./bind/*koscope
这两个模块是KnockoutJS功能的Magento核心扩展。extender
该模块返回 KnockoutJS 模板引擎的自定义版本,是我们将深入研究的第一个自定义版本。./template/engine
### Magento KnockoutJS 模板
回顾一下,在股票 KnockoutJS 系统中,模板是预先编写的 DOM/KnockoutJS 代码块,您可以通过引用它们的 .这些块通过脚本标记添加到页面的 HTML 中,其类型idtext/html
<script type="text/html" id="my_template"> <h1 data-bind="text:title"></h1> </script>
这是一个强大的功能,但给服务器端框架带来了一个问题——如何在页面上呈现正确的模板?您如何确定模板将在那里而不每次都重新创建它?KnockoutJS的解决方案是将组件绑定与RequireJS等库一起使用,但这意味着您的模板绑定到特定的视图模型对象。
Magento的核心工程师需要一种更好的方法来加载KnockoutJS模板 - 他们通过将本机KnockoutJS模板引擎替换为从RequireJS模块加载的引擎来实现这一点。Magento_Ui/js/lib/ko/template/engine
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js define([ 'ko', './template/engine', //... ], function (ko, templateEngine) { 'use strict'; //... ko.setTemplateEngine(templateEngine); //... });
如果我们看一下 RequireJS 模块Magento_Ui/js/lib/ko/template/engine
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/template/engine.js /** * Copyright © 2016 Magento. All rights reserved. * See COPYING.txt for license details. */ define([ 'ko', './observable_source', './renderer' ], function (ko, Source, Renderer) { 'use strict'; var RemoteTemplateEngine, NativeTemplateEngine = ko.nativeTemplateEngine, sources = {}; //... RemoteTemplateEngine.prototype = new NativeTemplateEngine; //... RemoteTemplateEngine.prototype.makeTemplateSource = function (template) { //... } //... return new RemoteTemplateEngine; });
我们看到Magento制作了一个新对象,该对象原型继承自本机KnockoutJS渲染引擎,然后修改了一些方法来添加自定义行为。如果你不了解你的javascript内部,这意味着Magento复制了股票KnockoutJS模板系统,对其进行了一些更改,然后将其新模板引擎换成库存模板引擎。
这些修改的实现细节超出了本文的范围,但最终结果是一个 KnockoutJS 引擎,它可以通过 Magento 模块的 URL 加载模板。
如果这没有意义,一个例子应该澄清问题。将以下内容添加到我们的文件中。content.phtml
#File: app/code/Pulsestorm/KnockoutTutorial/view/frontend/templates/content.phtml <div data-bind="template:'Pulsestorm_KnockoutTutorial/hello'"></div>
在这里,我们添加了一个 KnockoutJS 绑定并传递给它字符串。如果我们在上述情况下重新加载页面,您将在 javascript 控制台中看到如下错误templatePulsestorm_KnockoutTutorial/hello
> GET http://magento-2-0-4.dev/static/frontend/Magento/luma/en\_US/Pulsestorm\_KnockoutTutorial/template/hello.html 404 (Not Found)
Magento采用了我们的字符串()并使用第一部分()来创建视图资源的基本URL,并使用第二部分()和附加的前缀来完成URL。如果我们向以下文件添加 KnockoutJS 视图Pulsestorm_KnockoutTutorial/helloPulsestorm_KnockoutTutorialhellotemplate.html
#File: app/code/Pulsestorm/KnockoutTutorial/view/frontend/web/template/hello.html <p data-bind="style:{fontSize:'24px'}">Hello World</p>
并重新加载页面,我们将看到Magento从上面的URL加载了我们的模板,并应用了其KnockoutJS绑定。
此功能使我们能够避免在需要新模板时用标签乱扔 HTML 页面,并鼓励在 UI 和 UX 功能之间重用模板。<script type="text/html">
### 无视图模型
回到模块,在Magento设置自定义模板引擎之后,Magento调用KnockoutJS的方法。这将启动将当前 HTML 页面呈现为视图。如果我们看一下该代码,就会立即弹出一些内容。initialize.jsapplyBindings
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js ko.setTemplateEngine(templateEngine); ko.applyBindings();
Magento在没有视图模型的情况下调用。虽然这是一个合法的 KnockoutJS 调用——告诉 KnockoutJS 在没有数据或视图模型逻辑的情况下应用绑定似乎毫无用处。没有数据的视图有什么用?applyBindings
在股票KnockoutJS系统中,这将是毫无用处的。理解Magento在这里做什么的关键是在我们的KnockoutJS初始化中备份
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js define([ //... './bind/scope', //... ],
Magento的KnockoutJS团队创建了一个名为的自定义KnockoutJS绑定。下面是一个使用范围的示例 - 从Magento 2主页提升。scope
<li class="greet welcome" data-bind="scope: 'customer'"> <span data-bind="text: customer().fullname ? $t('Welcome, %1!').replace('%1', customer().fullname) : 'Default welcome msg!'"></span> </li>
当你像这样调用 scope 元素时
data-bind="scope: 'customer'"
Magento将客户视图模型应用于此标签及其后代。
您可能想知道 - 客户视图模型到底是什么?!如果您在主页的源代码中再往下看一点,您应该会看到以下脚本标记
<script type="text/x-magento-init"> { "*": { "Magento_Ui/js/core/app": { "components": { "customer": { "component": "Magento_Customer/js/view/customer" } } } } } </script>
正如我们从本系列的第一篇文章中知道的那样,当Magento遇到带有属性的脚本标签时,它将text/x-magento-init*
初始化指定的 RequireJS 模块 (Magento_Ui/js/core/app)
调用该模块返回的函数,传入数据对象
RequireJS 模块是一个实例化 KnockoutJS 视图模型以与属性一起使用的模块。它的完整实现超出了本文的“范围”,但在高层次上,Magento将为每个配置为的RequireJS模块实例化一个新的javascript对象,并且该新对象成为视图模型。Magento_Ui/js/core/appscopecomponent
如果这没有意义,让我们通过上面的示例。Magento查看密钥,并看到一个密钥/对象对。x-magento-initcomponents
"customer": { "component": "Magento_Customer/js/view/customer" }
因此,对于密钥,Magento将运行等效于以下内容的代码。customer
//gross over simplification var ViewModelConstructor = requirejs('Magento_Customer/js/view/customer'); var viewModel = new ViewModelConstructor; viewModelRegistry.save('customer', viewModel);
如果特定组件对象中有额外的数据
"customer": { "component": "Magento_Customer/js/view/customer", "extra_data":"something" }
Magento也会将该数据添加到视图模型中。
完成上述操作后,视图模型注册表将具有一个名为 的视图模型。这是Magento将应用于绑定的视图模型。customerdata-bind="scope: 'customer'"
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/bind/scope.js define([ 'ko', 'uiRegistry', 'jquery', 'mage/translate' ], function (ko, registry, $) { 'use strict'; //... update: function (el, valueAccessor, allBindings, viewModel, bindingContext) { var component = valueAccessor(), apply = applyComponents.bind(this, el, bindingContext); if (typeof component === 'string') { registry.get(component, apply); } else if (typeof component === 'function') { component(apply); } } //... });
它是从视图模型注册表中获取命名视图模型的行,然后以下代码实际上是在 KnockoutJS 中将对象作为视图模型应用的代码registry.get(component, apply);
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/bind/scope.js //the component variable is our viewModel function applyComponents(el, bindingContext, component) { component = bindingContext.createChildContext(component); ko.utils.extend(component, { $t: i18n }); ko.utils.arrayForEach(el.childNodes, ko.cleanNode); ko.applyBindingsToDescendants(component, el); }
该变量来自模块,该模块是 RequireJS 模块的别名。registryuiRegistryMagento_Ui/js/lib/registry/registry
vendor/magento/module-ui/view/base/requirejs-config.js 17: uiRegistry: 'Magento_Ui/js/lib/registry/registry',
如果很多东西飞过你的头,别担心。如果要查看特定范围的绑定中可用的数据,以下调试代码应指导您。
<li class="greet welcome" data-bind="scope: 'customer'"> <pre data-bind="text: ko.toJSON($data, null, 2)"></pre> <!-- ... --> </li>
如果您是有兴趣深入了解创建视图模型的真实代码(而不是我们上面的简化伪代码)的人之一,则可以从该模块开始。Magento_Ui/js/core/app
#File: vendor/magento/module-ui/view/base/web/js/core/app.js define([ './renderer/types', './renderer/layout', 'Magento_Ui/js/lib/ko/initialize' ], function (types, layout) { 'use strict'; return function (data) { types.set(data.types); layout(data.components); }; });
此模块具有名为 的依赖项。正是在这个依赖模块中,Magento初始化视图模型,并将它们添加到视图模型注册表中。Magento_Ui/js/core/renderer/layout
#File: vendor/magento/module-ui/view/base/web/js/core/renderer/layout.js
代码在那里有点粗糙,但如果你需要知道这些视图模型是如何实例化的,那就是你可以找到它们的地方。
### 具有任何其他名称的组件
所有这一切中的一个粘性检票口是单词组件。这个绑定+系统基本上是对原生KnockoutJS组件系统的不同看法。scopex-magento-init
通过使用与KnockoutJS相同的组件术语,Magento开辟了一个混乱的新世界。甚至官方文档似乎对组件是什么或不是什么有点困惑。这就是一个大型软件团队的生活,左手不知道右手在做什么——身体的其他部分对第三只手从背部长出来感到害怕。
当与同事讨论这些功能或在Magento论坛上提问时,区分KnockoutJS组件和Magento组件非常重要。
### 2.1 候选版本中的更改
为了结束今天的比赛,我们将讨论Magento 2.1候选版本中对上述内容的一些更改。从概念上讲,系统仍然相同,但细节有一些变化。
首先,KnockoutJS的初始化现在发生在RequireJS模块中。Magento_Ui/js/lib/knockout/bootstrap
#File: vendor/magento/module-ui/view/base/web/js/lib/knockout/bootstrap.js 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(); });
请注意,Magento的核心开发人员将所有绑定加载移动到一个单独的模块,定义在Magento_Ui/js/lib/knockout/bindings/bootstrap
#File: vendor/magento/module-ui/view/base/web/js/lib/knockout/bindings/bootstrap.js
最后,返回的“Magento Javascript组件”有一个包含参数的更改方法签名,并且函数的参数清楚地表明它的签名也发生了变化。Magento_Ui/js/core/appmergelayoutlayout
#File: vendor/magento/module-ui/view/base/web/js/core/app.js define([ './renderer/types', './renderer/layout', '../lib/knockout/bootstrap' ], function (types, layout) { 'use strict'; return function (data, merge) { types.set(data.types); layout(data.components, undefined, true, merge); }; });
除了对实现细节感兴趣的人感兴趣之外,这些变化还指出了Magento的javascript模块和框架正在迅速变化的事实,与PHP代码不同,Magento的RequireJS模块没有标记来表示稳定性。@api
除非你绝对需要,否则最好避免动态更改这些核心模块的行为,并尽可能保持你自己的JavaScript独立。