当前位置: 技术文章>> magento2中整合knockoutjs的原理与使用方法

文章标题:magento2中整合knockoutjs的原理与使用方法
  • 文章分类: Magento
  • 13016 阅读
系统学习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独立。


推荐文章