上期的两篇文章中,我们从头创建了一个新的 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
节点是因为:
- XSD 允许它作为
<container>
节点的子节点 - 它的基本功能相对简单
- 它是通用的,不太可能是特定功能的片段(比如
<listingToolbar/>
)
这些特点使得它是很适合劫持的。劫持,这里的意思是我们将利用 UI Component 的 xml 合并,使得我们的 htmlContent
blocks 可以做如下事情:
- 使用不同的 component class
- 使用不同的 RequireJS view model constructor factory
- 使得上面的 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 稳定地用在插件中。