Magento 2 的 UI Components 介绍(翻译)

3.15K 浏览开发笔记

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 使用uiComponentname查找名为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

  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 类名和默认的参数。

<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 templatetemplates/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.

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

  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 创建列表,下次再补上其他的文章。

0