Magento 2: Simplest XSD Valid UI Component

原文地址

上期的两篇文章中,我们从头创建了一个新的 UI Component 模块。虽然我们成功了,但是是以 <preference> 重写的方式做到的,这种方式禁用了 Magento 2 的 XSD 验证的,作为学习练习还好,但不能用在生产环境中。

使用 <preference> 的方式禁用 XSD 验证是以牺牲系统的稳定性为代价的。这次我们不会再禁用 XSD 的验证,用可以在生产环境中使用的方式来创建 UI Component

前提

如果上次你已经使用了 Pulsestorm_SimpleUiComponent 那么现在请禁用该模块。

$ php bin/magento module:disable Pulsestorm_SimpleUiComponent
The following modules have been disabled:
- Pulsestorm_SimpleUiComponent

Cache cleared successfully.
Generated classes cleared successfully. Please run the 'setup:di:compile' command to generate classes.
Info: Some modules might require static view files to be cleared. To do this, run 'module:disable' with the --clear-static-content option to clear them.

下面我们使用 pestle 创建一个 Admin 模块

php pestle.phar generate_module Pulsestorm SimpleValidUiComponent 0.0.1

php pestle.phar generate_acl Pulsestorm_SimpleValidUiComponent Pulsestorm_SimpleValidUiComponent::top,Pulsestorm_SimpleValidUiComponent::menu_1

php pestle.phar generate_menu Pulsestorm_SimpleValidUiComponent Magento_Backend::system_other_settings Pulsestorm_SimpleValidUiComponent::a_menu_item Pulsestorm_SimpleValidUiComponent::menu_1 "Hello Simple Valid Ui Component" pulsestorm_simplevaliduicomponent/index/index 1

php pestle.phar generate_route Pulsestorm_SimpleValidUiComponent adminhtml pulsestorm_simplevaliduicomponent

php pestle.phar generate_view Pulsestorm_SimpleValidUiComponent adminhtml pulsestorm_simplevaliduicomponent_index_index Main content.phtml 1column

php bin/magento module:enable Pulsestorm_SimpleValidUiComponent

php bin/magento setup:upgrade

这样,我们登陆管理后台,进入 System -> Other Settings -> Hello Simple Valid Ui Component

OK ,我们的模板已经生成了,下面开始进入正题。

A New UI Component

我们在 layout handle XML 文件中加入新的 <uiComponent> 节点。

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/layout/pulsestorm_simplevaliduicomponent_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <referenceBlock name="content">
        <block template="content.phtml" class="Pulsestorm\SimpleValidUiComponent\Block\Adminhtml\Main" name="pulsestorm_simplevaliduicomponent_block_main" />

        <uiComponent name="pulsestorm_simple_valid"/>

    </referenceBlock>
</page>

然后我们创建这个名为 pulsestorm_simple_valid 的 UI Component 的 XML 文件。

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<pulsestorm_simple_valid xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
</pulsestorm_simple_valid>

清空缓存刷新页面,我们会看到如下错误:

Exception #0 (Magento\Framework\Exception\LocalizedException): Element
'pulsestorm_simple_valid': No matching global declaration
available for thevalidation root.

就像我们之前的文章中说的那样,这个错误是因为 vendor/magento/module-ui/view/base/ui_component/etc/definition.xml 中没有定义 pulsestorm_simple_valid ,而且我们没有办法增加这个节点进去,因为 Magento 的 XSD 验证不能通过名为 <pulsestorm_simple_valid/> 的 root 节点。

在 2.1 版本的 Magento 中,我们是没有好办法不动系统方法去做到的增加 root 节点的,但是我们可以变通一下。

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
</container>

<container> 节点是一个有效的 root 节点,清空缓存刷新页面,我们这次会看到如下错误:

Fatal error: Method Magento\Ui\TemplateEngine\Xhtml\Result::__toString()
must not throw an exception, caught Error: Call to a member function
getConfigData() on null in
/path/to/magento/vendor/magento/module-ui/
Component/Wrapper/UiComponent.php on line 0

换句话说,schema validation 错误已经没有了,这次只是因为缺少 dataSource 节点

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <dataSource name="pulsestorm_simple_valid_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <!-- the PHP class that implements a data provider -->
            <argument name="class" xsi:type="string">Pulsestorm\SimpleValidUiComponent\Model\DataProvider</argument>

            <!-- redundant with the `dataSource` name -->
            <argument name="name" xsi:type="string">pulsestorm_simple_valid_data_source</argument>

            <!-- required: means ui components are meant to work with models -->
            <argument name="primaryFieldName" xsi:type="string">entity_id</argument>

            <!-- required: means ui components are meant to work with URL passing -->
            <argument name="requestFieldName" xsi:type="string">id</argument>
        </argument>
    </dataSource>
</container>

data provider class

#File: app/code/Pulsestorm/SimpleValidUiComponent/Model/DataProvider.php
<?php
namespace Pulsestorm\SimpleValidUiComponent\Model;
class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider
{
}

刷新页面,这次我们应该没有错误,加载了页面了。

What Did We Render?

如果我们查看源代码的话,不是通过 DOM Inspector ,我们会看到下面的加载内容:

<div>
    <div data-bind="scope: 'pulsestorm_simple_valid.areas'" class="entry-edit form-inline">
        <!-- ko template: getTemplate() --><!-- /ko -->
    </div>
    <script type="text/x-magento-init">
    {
        "*": {
            "Magento_Ui/js/core/app": {
                "types": {
                    "dataSource": [],
                    "container": {
                        "extends": "pulsestorm_simple_valid"
                    },
                    "html_content": {
                        "component": "Magento_Ui\/js\/form\/components\/html",
                        "extends": "pulsestorm_simple_valid"
                    }
                },
                "components": {
                    "pulsestorm_simple_valid": {
                        "children": {
                            "pulsestorm_simple_valid": {
                                "type": "pulsestorm_simple_valid",
                                "name": "pulsestorm_simple_valid",
                                "config": {
                                    "component": "uiComponent"
                                }
                            },
                            "pulsestorm_simple_valid_data_source": {
                                "type": "dataSource",
                                "name": "pulsestorm_simple_valid_data_source",
                                "dataScope": "pulsestorm_simple_valid",
                                "config": {
                                    "params": {
                                        "namespace": "pulsestorm_simple_valid"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    </script>
</div>

我们看下 <container>definition.xml 配置中的定义:

<!-- File: vendor/magento//module-ui/view/base/ui_component/etc/definition.xml -->

<!-- ... -->
<container class="Magento\Ui\Component\Container">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">uiComponent</item>
        </item>
        <item name="template" xsi:type="string">templates/container/default</item>
    </argument>
</container>
<!-- ... -->

他的 XHTML template 是 templates/container/default ,他的 component 是 uiComponent (Magento_Ui/js/lib/core/collection)

我们选择用 container component 的原因有两点:第一,他是少数几个能用的不会有 XSD 验证错误的 root 节点。第二,uiComponent 正是我们需要的组件。上次(Magento 2: Simplest UI Knockout Component)我们讲到 ui/collection 模块会遍历子元素,并加载子元素的模板,类似于 layout update XML 的 <container> 节点,或者 Magento 1 中的 core/text_list block

但是 templates/container/default XHTML 模板并不是我们需要的:

#File: vendor/magento/module-ui/view/base/ui_component/templates/container/default.xhtml
<div xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd">
    <div data-bind="scope: '{{getName()}}.areas'" class="entry-edit form-inline">
        <!-- ko template: getTemplate() --><!-- /ko -->
    </div>
</div>

这个模板是干吗用的目前还不是很清楚。Magento 的 ui_component files 只用 <container> 作为子节点,就是说它的 XHTML template 从来没有被加载过。很有可能是早期 Magento 使用 <container> 作为 root 根节点的遗留产物,也可能是某种为将来做的准备。管他原因是什么,这就是为什么我们可以用 <container> 作为 root 节点。很难说这个“功能”以后还会不会存在,但目前这是最好的方式了。

Changing the Template

XHTML 模板不是我们想要的,那么我们是不是就卡住了呢?当然不是了,我们可以在 ulsestorm_simple_valid.xml 文件中配置一个新的模板。

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="template" xsi:type="string">templates/pulsestorm_simple_valid/default</item>
    </argument>
    <!-- ... -->

</container>

记住,definition.xml 文件中设置的是默认值,ui_component 文件夹中的文件可以覆盖默认值。

然后我们还要创建 xhtml template

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/templates/pulsestorm_simple_valid/default.xhtml
<div data-bind="scope: '{{getName()}}.{{getName()}}'"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd">

    <!-- ko template: getTemplate() --><!-- /ko -->
</div>

上面的代码基本上参照的是 listing grid (vendor/magento/module-ui/view/base/ui_component/templates/listing/default.xhtml) 的默认 XHTML 。它和我们上篇中用的模板一样,通过 scope 绑定将整个系统运行起来。

这里有几点值得注意。

首先,我们看到 Knockout tag-less 模板绑定:

<!-- ko template: getTemplate() --><!-- /ko -->

我们已经知道了 Knockout 的 view model 是通过我们的 scope 绑定来实现的:

<div data-bind="scope: '{{getName()}}.{{getName()}}'" ...>

这里我们看到了一个不熟悉的地方。我么用了 {{getName()}}.{{getName()}} 来替代了硬编码的 scope 绑定。{{...}} 花括号中的内容是 XHTML template 语言的一部分,他将调用该 UI Component 对象的 getName() 方法。这里的 name 是我们用在 layout handle XML 文件中的 <uiComponent name="pulsestorm_simple_valid"/> 。就是说他等同于:

<div data-bind="scope: 'pulsestorm_simple_valid.pulsestorm_simple_valid'" ...>

下一处让人困惑的地方是:

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd"

We have an XML namespace declaration in the root level

, as well as a schema validation file (xsi:noNamespaceSchemaLocation).记住,这是 XHTML 不是 HTML 模板。他们像 XML 文件那样,这意味着 XHTML 模板中只能由一个 top level node .

虽然这些属性(namespace 等)不是严格要求的,但是 Magento 核心文件中都是有的,所以为了保持一致,我们最好也加上。

如果你很好奇,为什么这些属性最终没有进入到 HTML 中去的话,这是因为在加载前 Magento 移除了他们:

#File: vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php
    public function __toString()
    {
        //...
        foreach ($templateRootElement->attributes as $name => $attribute) {
            if ('noNamespaceSchemaLocation' === $name) {
                $this->getDocumentElement()->removeAttributeNode($attribute);
                break;
            }
        }
        $templateRootElement->removeAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi');
        //...
    }

最后,ui_template.xsd 值得一看

#File: vendor/magento/module-ui/etc/ui_template.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
    <xs:element name="form">
        <xs:complexType >
            <xs:sequence>
                <xs:any minOccurs="0" maxOccurs="unbounded" processContents="lax" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="div" >
        <xs:complexType >
            <xs:sequence>
                <xs:any minOccurs="0" maxOccurs="unbounded" processContents="lax" />
            </xs:sequence>
            <xs:anyAttribute processContents="lax" />
        </xs:complexType>
    </xs:element>
</xs:schema>

完整地讲 xs:schema 语言超出了本篇的范围,但是上面地代码表示 xhtml 文件必须有一个 root 节点,div 或是 form 。另外,我们地文件中不写 si:noNamespaceSchemaLocation 并不能跳过验证,因为这些 .xhtml 文件最终会被合并成 XML 树,带 schema 验证的。

Adding to the Collection

下面我们清空缓存刷新页面,我们什么变化也没有看到。但是,如果我们用 Chrome 的 debugger 工具查看 Sources tab ,我们会发现 Magento 已经加载了 collection.js (Magento_Ui/js/lib/core/collectio)

通过 XHR debugging 我们还会发现 magento 已经加载了 collection.html Knockout.js template

我们切换到 console 中,我们会看到名为 pulsestorm_simple_valid.pulsestorm_simple_valid 的 view model constructor

> reg = requirejs('uiRegistry');
> reg.get(function(item){
    console.log(item.name);
    console.log(item);
});

pulsestorm_simple_valid.pulsestorm_simple_valid
UiClass {_super: undefined, ignoreTmpls: Object, _requesetd: Object, containers: Array[0], exports: Object…}

如果你不确定这里在做什么,请参考 Magento 2: Simplest UI Component

我们下一步要做的是加一些子组件。

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container>
    <!-- ... -->
    <htmlContent name="our_first_content">
        <argument name="block" xsi:type="object">Pulsestorm\SimpleValidUiComponent\Block\Example</argument>
    </htmlContent>
    <!-- ... -->
</container>

The htmlContent node allows you to render the contents of a Magento block object into our x-magento-init script, and then have those contents rendered onto the page via Knockout.js.上面的例子我们将 render 名为 Pulsestorm\SimpleValidUiComponent\Block\Example 的 block 。我们来看 definition.xml 中是怎么定义 <htmlContent/> 的。

<htmlContent class="Magento\Ui\Component\HtmlContent">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">Magento_Ui/js/form/components/html</item>
        </item>
    </argument>
</htmlContent>

我们发现 rendering 是通过 Magento_Ui/js/form/components/html 模块进行的。完整地说,Magento_Ui/js/form/components/html 模块返回一个 Knockout.js view model constructor ,它有一个 Knockout.js “Magento remote” 模板 ui/content/content

//File: vendor/magento/module-ui/view/base/web/js/form/components/html.js
//...
return Component.extend({
    defaults: {
        content:        '',
        showSpinner:    false,
        loading:        false,
        visible:        true,
        template:       'ui/content/content',
        additionalClasses: {}
    },
//...

render 的实现细节作为高级练习六个读者。

下面我们清空缓存刷新页面,然后我们就会看到下面的错误:

Exception #0 (ReflectionException): Class Pulsestorm\SimpleValidUiComponent\Block\Example does not exist

哈,我们忘记创建这个对象了 Pulsestorm\SimpleValidUiComponent\Block\Example

#File: app/code/Pulsestorm/SimpleValidUiComponent/Block/Example.php
<?php
namespace Pulsestorm\SimpleValidUiComponent\Block;

use Magento\Framework\View\Element\BlockInterface;

class Example extends \Magento\Framework\View\Element\AbstractBlock
{
    public function toHtml()
    {
        return '<h1>Hello PHP Block Rendered in JS</h1>';
    }
}

它就是 Magento 的标准 block 类,是可以通过 layout 配置的,它必须继承 Magento\Framework\View\Element\AbstractBlock 类。通常这些是 phtml template block ,但是这里为了简化我们用了 toHtml 方法直接返回了。

刷新页面,我们应该看到下面的样子:

Hijacking htmlContent(劫持 htmlContent)

htmlContent 节点很有意思,if only for their amusing “render some server side code that renders some front-end code that renders some more server side code” pattern, we’re not interested in them today for their core functionality.我们使用 htmlContent 节点是因为:

  1. XSD 允许它作为 <container> 节点的子节点
  2. 它的基本功能相对简单
  3. 它是通用的,不太可能是特定功能的片段(比如 <listingToolbar/>

这些特点使得它是很适合劫持的。劫持,这里的意思是我们将利用 UI Component 的 xml 合并,使得我们的 htmlContent blocks 可以做如下事情:

  1. 使用不同的 component class
  2. 使用不同的 RequireJS view model constructor factory
  3. 使得上面的 RequireJS view model constructor factory 指向一个新的 Knockout.js template

对于第一点,我们要做的只是在 htmlContent XML 节点上加上新的 class 属性:

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<htmlContent class="Pulsestorm\SimpleValidUiComponent\Component\Simple" name="our_first_content">
</htmlContent>

我们也移除了 block argument 。因为 Magento\Ui\Component\HtmlContent 必须要 block argument ,但是我们的 Pulsestorm\SimpleValidUiComponent\Component\Simple 代替了它。

下面创建 Pulsestorm\SimpleValidUiComponent\Component\Simple

#File: app/code/Pulsestorm/SimpleValidUiComponent/Component/Simple.php
<?php
namespace Pulsestorm\SimpleValidUiComponent\Component;
class Simple extends \Magento\Ui\Component\AbstractComponent
{
    const NAME = 'html_content_pulsestorm_simple';
    public function getComponentName()
    {
        return self::getName();
    }
}

对于第二点使用不同的 RequireJS view model constructor factory 我们要这样做:

<htmlContent class="Pulsestorm\SimpleValidUiComponent\Component\Simple"  name="our_first_content">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">Pulsestorm_SimpleValidUiComponent/js/pulsestorm_simple_component</item>
        </item>
    </argument>
</htmlContent>

对于第三点使得 RequireJS view model constructor factory 指向一个新的 Knockout.js template

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/web/js/pulsestorm_simple_component.js
define(['uiElement','ko'], function(Element, ko){
    viewModelConstructor = Element.extend({
        defaults: {
            template: 'Pulsestorm_SimpleValidUiComponent/pulsestorm_simple_template'
        }
    });

    return viewModelConstructor;
});

下面创建 Pulsestorm_SimpleValidUiComponent/pulsestorm_simple_template 模板

<!-- File: app/code/Pulsestorm/SimpleValidUiComponent//view/adminhtml/web/template/pulsestorm_simple_template.html -->
<h1>Our Remote Knockout Template!</h1>

下面清空缓存,刷新页面

恭喜你,我们没有违反 Magento 的 XSD schema 验证,成功创建了一个 UI Component

Wrap Up

这是不是个好主意,有待后续验证。反正这次每个步骤都在我们的掌控中(自定义的 PHP Component ,自定义的 ReuqireJS Component,自定义的 Knockout.js 模板)理论上来说,将来 Magento 核心团队可能会做些修改,到时候我们今天的方法就没有用了。现在,UI Component 的基本问题是,很多证据都表明 UI Component 是 Magento 核心团队专用的,只有时间能告诉我们,第三方开发者是否能够将 UI Component 稳定地用在插件中。

发表评论

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