Magento 2 的 UI Components 介绍(翻译)

原文地址

原作发布于 2016年7月10日

UI 组件是 Magento 2 中构建用户界面元素的新方法,管理后台很多地方都是基于它的。

今天我们的教程将带领大家,站在比较高的角度理解 UI 组件的目标是什么,并在尽可能高的层面介绍他的实现细节。最后我们创建一个 网格/列表 UI 组件做总结。

The Positive Spin (积极的一面)

要了解 UI 组件的目标,最简单的方法的是从 Magento 1 的生成后台用户界面的代码说起。下面是 Magento 1 layout update XML 一部分代码,我们以他为例:

<!-- #File: app/design/adminhtml/default/default/layout/catalog.xml -->
<adminhtml_catalog_product_new>
<update handle="editor"/>
<reference name="content">
<block type="adminhtml/catalog_product_edit" name="product_edit"></block>
</reference>
<reference name="left">
<block type="adminhtml/catalog_product_edit_tabs" name="product_tabs"></block>
</reference>
<reference name="js">
<block type="adminhtml/catalog_product_edit_js" template="catalog/product/js.phtml" name="catalog_product_js"></block>
<block type="core/template" template="catalog/wysiwyg/js.phtml"/>
</reference>
</adminhtml_catalog_product_new>

这四个 layout 添加了一个产品编辑表, 考虑到 <update handle="editor"/> 部分

<!-- #File: app/design/adminhtml/default/default/layout/main.xml -->
<editor>
<reference name="head">
<action method="setCanLoadExtJs"><flag>1</flag></action>
<action method="addJs"><script>mage/adminhtml/variables.js</script></action>
<action method="addJs"><script>mage/adminhtml/wysiwyg/widget.js</script></action>
<action method="addJs"><script>lib/flex.js</script></action>
<action method="addJs"><script>lib/FABridge.js</script></action>
<action method="addJs"><script>mage/adminhtml/flexuploader.js</script></action>
<action method="addJs"><script>mage/adminhtml/browser.js</script></action>
<action method="addJs"><script>prototype/window.js</script></action>
<action method="addItem"><type>js_css</type><name>prototype/windows/themes/default.css</name></action>
<action method="addCss"><name>lib/prototype/windows/themes/magento.css</name></action>
</reference>
</editor>

你会发现给页面添加一个编辑表单是很复杂的事情。

UI 组件的意图是隐藏这种复杂性。Magento 在 layout handle xml 文件中引入了新的<uiComponent/>标签。(Magento 2 handle XML files 类似 Magento 1 的 layout update XML files)在 Magento 2 中,你可以通过下面的代码给页面添加一个产品编辑表单。

<uiComponent name="product_form"/>

通过引入<uiComponent/>,Magento 2 让开发者更容易在不同位置重用不同的组件。While It was possible to drop different Magento 1 UI forms and grids in different areas, you needed to know which blocks and javascript files made up a particular component. Magento 1’s approach made it easy to accidentally setup a grid listing or a form so the component almost worked.

Magento 2 的 UI 组件正是用来解决这一问题的,它也极大地简化了每个人的 layout handle xml 文件。

The Actual Spin (实际情况)

我们刚刚所说的的确是真的,不过 UI 组件系统比美丽的构想更模糊。这是因为 UI 组件还有很多其他目标,这些目标带来了相当的复杂性。

就我所知,UI 组件系统:

  • 简化了 Layout Handle XML 文件
  • 使得后台用户界面元素的构建从 HTML+Javascript 变成了纯 js 的自定义小部件系统。
  • 可以由较小的组件构造出更复杂的 UI 组件。
  • Pre-renders data for UI components as JSON, binding closely to Magento backend data objects
  • 使用 ajax 来更新组件的数据
  • Introduce a new DSL for creating all of the above

UI 组件系统是雄心勃勃的一个系统,和 Magento 2中的许多东西一样,它还没有完全出炉(没有稳定)。一方面你可能想要远离还不太完美的系统,另一方面大多数的网格和表单使用 UI 组件系统来构建界面,还有一些使用传统的 block 渲染加 js 文件。如果你想构建一个全功能的模块,你需要使用 UI 组件系统。

下文代表着我(Alan)对当前的(Magento 2.1)UI组件的理解.其中的细节将来很可能会有变化,但是希望核心的概念保持不变。

没有为想要开发后台 UI 界面的开发人员准备的标准实践课程——像往常一样,最好的做法是看看核心团队对他们自己的组件做了什么,模仿他们,并且在 Magento 版本更新的时候,密切注意自己的模块/扩展代码。

如果你对复杂的实现细节没有兴趣,你可以跳到文章末尾,使用 Pestle 创建 UI 组件部分。

Pure Javascript (纯 JS)

如果你后台进入 Content -> Block,你会看到你 Magento 系统中的所有 CMS Blocks 列在一张表格中。如果你对 Blocks 不是很了解,他们是创建可重用的 HTML 代码块的一种方式。Block information is stored in Magento’s backend using CRUD Models

你看到的列表就是一个 UI 组件,是通过以下 layout handle xml 配置的。

<!-- File: vendor/magento/module-cms/view/adminhtml/layout/cms_block_index.xml -->
<!-- ... -->
<referenceContainer name="content">
<uiComponent name="cms_block_listing"/>
</referenceContainer>
<!-- ... -->

如果你对 layout xml 完全陌生,上面的代码解释一下就是:

获得名为 content 的 container 的引用,把 cms_block_listing UI 组件加进去。

如果你查看 HTML 源代码,你会发现<uiComponent/>标签渲染出了如下 HTML 代码:

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'cms_block_listing.cms_block_listing'">
<div data-role="spinner" data-component="cms_block_listing.cms_block_listing.cms_block_columns" class="admin__data-grid-loading-mask">
<div class="spinner">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<!-- ko template: getTemplate() --><!-- /ko -->
<script type="text/x-magento-init">
{"*": {"Magento_Ui/js/core/app": {...very large js object...}}}
</script>
</div>

如果你阅读过 Magento 2 高级 js 系列的文章,尤其是Magento 2 Javascript Init Scripts,你就知道 x-magento-init 标签将会调用 Magento_Ui/js/core/app RequireJS 模块,并将large js object作为参数传递给他。

不涉及更深入的实现细节(实现细节 some of which you can read about in these Stack Exchange answers),this javascript code ends up creating a series of javascript constructor objects that Magento will use as KnockoutJS view models(创建了一个用于 view model 的对象)

浏览器中界面元素的实际呈现由 KnockoutJS 处理。外面的 div 框架使用了Magento’s custom KnockoutJS scope binding,绑定由text/x-magento-init创建的view model。

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

然后渲染 UI 组件通过 KnockoutJS 的 “tag-less” template binding 完成。

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

getTemplate的调用实际上启动了一系列嵌套的模板渲染——从一个名为collection.html的文件开始。你可以通过浏览器的 XHR 调试窗口查找所有.html的模板文件。如果你这里有较多疑惑,你可以参阅Magento 2 KnockoutJS 集成。另外,记住Magento 的核心团队使用了一些自定义的标签和属性来增强KnockoutJS的模板,这可能会带给你一些迷惑。

总的来说,Magento 1 用 HTML 来渲染,用 js 增强用户界面的功能。Magento 2 依然会使用一些 HTML 搭建结构,但是用户界面元素的大部分渲染工作转由 RequireJS 模块和 KnockoutJS 模板来做。

Sub Components (子组件)

如果仔细审视一下x-magento-init的 JSON 对象,你会发现他有很多嵌套的子对象。

{
"*": {
"Magento_Ui/js/core/app": {
"types": /*...*/
"components": {
"cms_block_listing": {
"children": {
"cms_block_listing": {
/*...*/
"children": {
"listing_top": {
"type": "container",
"name": "listing_top",
"children": {
"bookmarks": {/*...*/},
"columns_controls": {/*...*/},
"fulltext": {/*...*/},
"listing_filters": {/*...*/},
"listing_massaction": {/*...*/},
"listing_paging": {/*...*/}
},

Older developers will be bemused to note the return of nodes named children — a practice we thought was left behind in Magento 1. These child element are each, themselves, fully featured UI Components. cms_block_listing 是由 listing_top, bookmarks, 等组件构成的。

前文我们提到,getTemplate的调用以渲染许多子组件告终。collection.html作为第一个 KnockoutJS 模板,他的命名也体现了这是由许多 UI 组件构成的collection。很遗憾,今天的教程没有时间完整梳理这个渲染流程。

今天我们要讲的是 PHP 开发者如何控制渲染的 js tree。我们回到<uiComponent/>标签。

<!-- #File: vendor/magento/module-cms/view/adminhtml/layout/cms_block_index.xml -->
<uiComponent name="cms_block_listing"/>

Magento 使用uiComponentname查找名为cms_block_listing.xml的 XML 文件。

#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<listing 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="js_config" xsi:type="array">
<item name="provider" xsi:type="string">cms_block_listing.cms_block_listing_data_source</item>
<item name="deps" xsi:type="string">cms_block_listing.cms_block_listing_data_source</item>
</item>
<item name="spinner" xsi:type="string">cms_block_columns</item>
<item name="buttons" xsi:type="array">
<item name="add" xsi:type="array">
<item name="name" xsi:type="string">add</item>
<item name="label" xsi:type="string" translate="true">Add New Block</item>
<item name="class" xsi:type="string">primary</item>
<item name="url" xsi:type="string">*/*/new</item>
</item>
</item>
</argument>
<!-- ... we'll get to this in a second ... -->
</listing>

These UI Component XML files are a new domain specific language (DSL)。上面的指令告诉 Magento

  1. Look up a PHP class name and default arguments for the root level listing node
  2. Instantiate that class, using the argument node as constructor arguments.

Magento 将会从下面的文件中查找 PHP 类名和默认的参数。

#File: vendor/magento/module-ui/view/base/ui_component/etc/definition.xml
<components xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_definition.xsd">
<!-- ... -->
<listing sorting="true" class="Magento\Ui\Component\Listing">
<argument name="data" xsi:type="array">
<item name="template" xsi:type="string">templates/listing/default</item>
<item name="save_parameters_in_session" xsi:type="string">1</item>
<item name="client_root" xsi:type="string">mui/index/render</item>
<item name="config" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
</item>
</argument>
</listing>
<!-- ... -->
</components>

所以,当 Magento 渲染 时,他就像下面这样开始运行(简化后的样子),

$uiComponent = new Magento\Ui\Component\Listing(
$context, $components, [
'template'=>'templates/listing/default',
'save_parameters_in_session'=>'1',
'client_root'=>'mui/index/render',
'config'=>[
'component'=>'uiComponent'
],
'js_config'=>[
'provider'=>'',
'deps'=>''
],
'spinner'=>'cms_block_columns',
'buttons'=>[
'add'=>[
'name'=>'add',
'label'=>'Add New Block',
'class'=>'primary',
'url'=>'*/*/new'
]
],
]
)

参数的数据来自于<argument/>节点的合并。每个参数都有不同的作用——不过我们感兴趣的是templates/listing/default参数。他指定了该组件渲染的 XHTML templatetemplates/listing/default字符串对应下面的模板。

#File: vendor/magento//module-ui/view/base/ui_component/templates/listing/default.xhtml
<div
class="admin__data-grid-outer-wrap"
data-bind="scope: '{{getName()}}.{{getName()}}'"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd">
<div data-role="spinner" data-component="{{getName()}}.{{getName()}}.{{spinner}}" class="admin__data-grid-loading-mask">
<div class="spinner">
<span/><span/><span/><span/><span/><span/><span/><span/>
</div>
</div>
<!-- ko template: getTemplate() --><!-- /ko -->
</div>

这个 XHTML 模版是由完全不同于 Magento 中标准的 phtml 模版渲染引擎所渲染。

Magento 通过调用 UI 组件对象中方法(getName())替换{{...}}文本,或者 directly accessing a data property of the same object ({{spinner}}).

可能有人已经注意到模板中没有x-magento-init。加入x-magento-init部分也是由 XHTML rendering engine 完成的——更确切的说,在appendLayoutConfiguration方法中。

#File: vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php
public function __toString()
{
try {
//...
$this->appendLayoutConfiguration();
$result = $this->compiler->postprocessing($this->template->__toString());
} catch (\Exception $e) {
$this->logger->critical($e->getMessage());
$result = $e->getMessage();
}
return $result;
}
//...
public function appendLayoutConfiguration()
{
$layoutConfiguration = $this->wrapContent(
json_encode(
$this->structure->generate($this->component)
)
);
$this->template->append($layoutConfiguration);
}
//...
protected function wrapContent($content)
{
return '<script type="text/x-magento-init"><![CDATA['
. '{"*": {"Magento_Ui/js/core/app": ' . str_replace(['<![CDATA[', ']]>'], '', $content) . '}}'
. ']]></script>';
}

Magento 将会以 JSON 字符串的方式渲染 UI 组件对象的结构,然后将字符串添加到模版中。

你问 UI 组件的结构是什么? Remember the we’ll get to the rest in a second hand waving we did here?

#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<listing 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="js_config" xsi:type="array">
<item name="provider" xsi:type="string">cms_block_listing.cms_block_listing_data_source</item>
<item name="deps" xsi:type="string">cms_block_listing.cms_block_listing_data_source</item>
</item>
<item name="spinner" xsi:type="string">cms_block_columns</item>
<item name="buttons" xsi:type="array">
<item name="add" xsi:type="array">
<item name="name" xsi:type="string">add</item>
<item name="label" xsi:type="string" translate="true">Add New Block</item>
<item name="class" xsi:type="string">primary</item>
<item name="url" xsi:type="string">*/*/new</item>
</item>
</item>
</argument>
<!-- ... we'll get to this in a second ... -->
</listing>

如果我们看一看上面的节点内容

#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml

<listingToolbar name="listing_top">
<argument name="data" xsi:type="array">
<!-- ... -->
</argument>
</listingToolbar>
<columns name="cms_block_columns">
<argument name="data" xsi:type="array">
<!-- ... -->
</argument>
</columns>

我们会发现更多配置的 UI 组件。所有名称不是 argument 的 UI 组件的子节点,都是父对象的子节点。Magneto 渲染 listing 对象的时候,它还会在 definitions.xml 中查找 listingToolbar, columns 等组件的类和参数。

#File: vendor/magento/module-ui/view/base/ui_component/etc/definition.xml
<components xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_definition.xsd">
<listingToolbar class="Magento\Ui\Component\Container"><!--...--></listingToolbar>
<columns class="Magento\Ui\Component\Listing\Columns"><!--...--></columns>
</components>

之前我们用的伪代码实际上更像下面这样:

$uiComponent = new Magento\Ui\Component\Listing(...);

$listingToolbar = new Magento\Ui\Component\Container(...);
$columns = new Magento\Ui\Component\Listing\Columns(...);

$uiComponent->addComponent($listingToolbar);
$uiComponent->addComponent($columns);

注意,这些子组件是通过 RequireJS 模块名称来进行配置的。

#File: vendor/magento/module-ui/view/base/ui_component/etc/definition.xml
<columns class="Magento\Ui\Component\Listing\Columns">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/grid/listing</item>
<!-- ... -->
</item>
</argument>
</columns>

这些就是 Magento 转化为 KnockoutJS view model 的 RequireJS 模块。如果你查看 KnockoutJS view models 的代码,你将发现通常 view model constructor 中配置了其 template 模版。

#File: vendor/magento//module-ui/view/base/web/js/grid/listing.js
define([
'ko',
'underscore',
'Magento_Ui/js/lib/spinner',
'uiLayout',
'uiCollection'
], function (ko, _, loader, layout, Collection) {
'use strict';

return Collection.extend({
defaults: {
template: 'ui/grid/listing',
}
//...
});
});

Data Source Nodes (数据源节点)

最后,有一个特别的 UI 组件子节点,就是<dataSource/>

#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml
<listing 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="js_config" xsi:type="array">
<item name="provider" xsi:type="string">cms_block_listing.cms_block_listing_data_source</item>
<item name="deps" xsi:type="string">cms_block_listing.cms_block_listing_data_source</item>
</item>
<item name="spinner" xsi:type="string">cms_block_columns</item>
<item name="buttons" xsi:type="array">
<item name="add" xsi:type="array">
<item name="name" xsi:type="string">add</item>
<item name="label" xsi:type="string" translate="true">Add New Block</item>
<item name="class" xsi:type="string">primary</item>
<item name="url" xsi:type="string">*/*/new</item>
</item>
</item>
</argument>
<!-- ... -->
<dataSource name="cms_block_listing_data_source">
<!-- ... -->
</dataSource>
</listing>

名为 dataSource 的节点仍然是 UI 组件,不过他们有“特殊待遇”。当 Magento 从 UI 组件中渲染 JSON 的时候,dataSource 节点被从 children 结构中拉出来,Magento 在主要的顶层组件之后就渲染他们(在组件名后加 _data_source 作为对象的键值)

(译者注,还记得上面的子组件 listing_top 吧,他的层级和 dataSource 节点是一样的,按照道理他应该在 children 下和 listing_top 并列在一个层级,但是现在上升了一个层级,和 cms_block_listing 并列了。)

{
"*": {
"Magento_Ui/js/core/app": {
"types": /*...*/
"components": {
"cms_block_listing": {
"children": {
"cms_block_listing": {
/*...*/
"children": {
"listing_top": {
"type": "container",
"name": "listing_top",
"children": {
"bookmarks": {/*...*/},
"columns_controls": {/*...*/},
"fulltext": {/*...*/},
"listing_filters": {/*...*/},
"listing_massaction": {/*...*/},
"listing_paging": {/*...*/}
},

dataSource 组件就是 Magento 寻找 UI 组件真实数据的地方。UI 组件中的数据由 dataSource 组件提供。

Summary of the UI Component Rendering DSL (总结)

OK — that was a bananas-pants amount of information. I just finished writing it and I’m not sure even I followed all of it, so don’t worry if your head is spinning.

下面是比较高层面的总结。

  1. UI 组件通过 x-magento-init 脚本注入全局注册的 KnockoutJS view models (原文:UI Components render an x-magento-init script that populates a global registry of KnockoutJS view models)
  2. UI 组件也加载 HTML 骨架,然后使用 KnockoutJS 和自定义的 scope 绑定来渲染 DOM 节点租成组件。
  3. ui_component XML 文件是一种领域特定语言(domain specific language),用来示例嵌套的层级 UI 组件对象。Magento 会使用它来为 x-magento-init 脚本加载 JSON
  4. ui_component XML 节点的名称用来查找 php 类进行实例化
  5. Magento 使用子节点 <argument/> 作为该类的构造参数
  6. Magento 使用 <dataSource /> 中的数据作为 UI 组件的数据源。(例如表格列表中的信息)
  7. 子节点将会作为子组件进行渲染——这些子组件遵循和父组件一样的规则
  8. 最顶层的 UI 组件配置的 XHTML 模版,Magento 通过 PHP 进行渲染
  9. UI 组件节点配置 RequireJS 模块,而 Magento 使用他们作为 KnockoutJS view model constructors

正如你所看到的,一方面 uiComponent 极大地简化了 Magento 2 中 layout handle XML 文件,隐藏了包括前后台在内的更为复杂的 UI 渲染系统,另一方面这对开发者来说,也要求你理解 Magento 对 RequireJS 和 KnockoutJS 做出的自定义。

Creating a Grid Listing with Pestle (创建一个列表)

从上文可以了解到,UI 组件系统,为了降低 Magento 1 的 layout update XML 系统的复杂度,提供更明确的使用指导。换句话说,对 Magento 2 开发者来说,这正是代码生成工具(例如 pestle ,这是 Alan Storm 的一个项目)的用武之地。

译者注:由于这一段操作有较多的前提条件,而且使用代码自动生成工具,对我们理解如何创建一个列表的帮助可能不太大,所以此处不再继续翻译,有兴趣的请阅读原文。而关于如何用 uiComponent 创建列表,下次再补上其他的文章。

参考 How to Create Admin Grid in Magento 2

发表评论

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