Magento 2 的 UI Components 介绍(翻译)
Magento 2 的 UI Components 介绍(翻译)
原作发布于 2016年7月10日
UI 组件是 Magento 2 中构建用户界面元素的新方法,管理后台很多地方都是基于它的。
今天我们的教程将带领大家,站在比较高的角度理解 UI 组件的目标是什么,并在尽可能高的层面介绍他的实现细节。最后我们创建一个 网格/列表 UI 组件做总结。
The Positive Spin (积极的一面)
要了解 UI 组件的目标,最简单的方法的是从 Magento 1 的生成后台用户界面的代码说起。下面是 Magento 1 layout update XML 一部分代码,我们以他为例:
<code><!-- #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></code>
这四个 layout 添加了一个产品编辑表, 考虑到 <update handle="editor"/>
部分
<code><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></code>
你会发现给页面添加一个编辑表单是很复杂的事情。
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 配置的。
<code><!-- File: vendor/magento/module-cms/view/adminhtml/layout/cms_block_index.xml --> <!-- ... --> <referenceContainer name="content"> <uiComponent name="cms_block_listing"/> </referenceContainer> <!-- ... --></code>
如果你对 layout xml 完全陌生,上面的代码解释一下就是:
获得名为 content 的 container 的引用,把 cms_block_listing UI 组件加进去。
如果你查看 HTML 源代码,你会发现<uiComponent/>
标签渲染出了如下 HTML 代码:
<code><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></code>
如果你阅读过 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。
<code><div ... data-bind="scope: 'cms_block_listing.cms_block_listing'"> </div></code>
然后渲染 UI 组件通过 KnockoutJS 的 “tag-less” template binding 完成。
<code><!-- ko template: getTemplate() --><!-- /ko --></code>
对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 对象,你会发现他有很多嵌套的子对象。
<code> { "*": { "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": {/*...*/} },</code>
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/>
标签。
<code><!-- #File: vendor/magento/module-cms/view/adminhtml/layout/cms_block_index.xml --> <uiComponent name="cms_block_listing"/></code>
Magento 使用uiComponent
的name
查找名为cms_block_listing.xml
的 XML 文件。
<code>#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></code>
These UI Component XML files are a new domain specific language (DSL)。上面的指令告诉 Magento
- Look up a PHP class name and default arguments for the root level
listing
node - Instantiate that class, using the
argument
node as constructor arguments.
Magento 将会从下面的文件中查找 PHP 类名和默认的参数。
<code>#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="MagentoUiComponentListing"> <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></code>
所以,当 Magento 渲染 时,他就像下面这样开始运行(简化后的样子),
<code>$uiCompOnent= new MagentoUiComponentListing( $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' ] ], ] )</code>
参数的数据来自于<argument/>
节点的合并。每个参数都有不同的作用——不过我们感兴趣的是templates/listing/default
参数。他指定了该组件渲染的 XHTML template。templates/listing/default
字符串对应下面的模板。
<code>#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></code>
这个 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
方法中。
<code>#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>'; }</code>
Magento 将会以 JSON 字符串的方式渲染 UI 组件对象的结构,然后将字符串添加到模版中。
你问 UI 组件的结构是什么? Remember the we’ll get to the rest in a second hand waving we did here?
<code>#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></code>
如果我们看一看上面的节点内容
<code>#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></code>
我们会发现更多配置的 UI 组件。所有名称不是 argument
的 UI 组件的子节点,都是父对象的子节点。Magneto 渲染 listing
对象的时候,它还会在 definitions.xml
中查找 listingToolbar
, columns
等组件的类和参数。
<code>#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="MagentoUiComponentContainer"><!--...--></listingToolbar> <columns class="MagentoUiComponentListingColumns"><!--...--></columns> </components></code>
之前我们用的伪代码实际上更像下面这样:
<code>$uiCompOnent= new MagentoUiComponentListing(...); $listingToolbar = new MagentoUiComponentContainer(...); $columns = new MagentoUiComponentListingColumns(...); $uiComponent->addComponent($listingToolbar); $uiComponent->addComponent($columns);</code>
注意,这些子组件是通过 RequireJS 模块名称来进行配置的。
<code>#File: vendor/magento/module-ui/view/base/ui_component/etc/definition.xml <columns class="MagentoUiComponentListingColumns"> <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></code>
这些就是 Magento 转化为 KnockoutJS view model 的 RequireJS 模块。如果你查看 KnockoutJS view models 的代码,你将发现通常 view model constructor 中配置了其 template 模版。
<code>#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', } //... }); });</code>
Data Source Nodes (数据源节点)
最后,有一个特别的 UI 组件子节点,就是<dataSource/>
<code>#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></code>
名为 dataSource
的节点仍然是 UI 组件,不过他们有“特殊待遇”。当 Magento 从 UI 组件中渲染 JSON 的时候,dataSource 节点被从 children
结构中拉出来,Magento 在主要的顶层组件之后就渲染他们(在组件名后加 _data_source
作为对象的键值)
(译者注,还记得上面的子组件 listing_top
吧,他的层级和 dataSource
节点是一样的,按照道理他应该在 children
下和 listing_top
并列在一个层级,但是现在上升了一个层级,和 cms_block_listing
并列了。)
<code>{ "*": { "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": {/*...*/} },</code>
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.
下面是比较高层面的总结。
- UI 组件通过
x-magento-init
脚本注入全局注册的 KnockoutJS view models (原文:UI Components render an x-magento-init script that populates a global registry of KnockoutJS view models) - UI 组件也加载 HTML 骨架,然后使用 KnockoutJS 和自定义的
scope
绑定来渲染 DOM 节点租成组件。 -
ui_component
XML 文件是一种领域特定语言(domain specific language),用来示例嵌套的层级 UI 组件对象。Magento 会使用它来为x-magento-init
脚本加载 JSON -
ui_component
XML 节点的名称用来查找 php 类进行实例化 - Magento 使用子节点
<argument/>
作为该类的构造参数 - Magento 使用
<dataSource />
中的数据作为 UI 组件的数据源。(例如表格列表中的信息) - 子节点将会作为子组件进行渲染——这些子组件遵循和父组件一样的规则
- 最顶层的 UI 组件配置的 XHTML 模版,Magento 通过 PHP 进行渲染
- 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 创建列表,下次再补上其他的文章。