Magento 2 - Alan Storm 博客 - 介绍 UI 组件
Magento 2 - Alan Storm 博客 - 介绍 UI 组件
来源: https://alanstorm.com/magento_2_introducing_ui_components/
UI 组件是组建用户界面元素的一个强有力的方法,并且很多新的后台控制台都建立在这个功能之上。今天我们将深入了解 UI 组件系统的目标,包括尽可能地深入了解执行细节,并且最后结束会用 pestle 生成 grid/listing UI 组件。
理解 UI 组件目的最简单的方法是讨论 Magento 1 的后台用户界面形成的代码。这是一个 Magento 1 布局更新 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>
<!-- #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>
你看给一个页面添加产品编辑表单甚至更复杂。
<uiComponent name="product_form"/>
Magento 2 的 UI 组件打算解决这个难题,并且大大地简化了每个人的布局句柄 XML 文件。
- 简化了布局句柄 XML 文件
- 把管理用户界面元素从 HTML+JavaScript 变成了“纯 JavaScript” 自定义组件系统
- 是由更小的组件建造更复杂的 UI 组件的系统
- 作为 JSON 为 UI 组件预渲染数据,跟 Mangento 后台数据对象紧密地绑定在一起
- 使用 AJAX 来更新数据
- 引入一个新的 DSL 来创建以上所有
除非你对复杂的执行细节感兴趣,不然你可能想要直接跳到最后我们用 pestle 创建一个 UI 组件的地方。
<!-- File: vendor/magento/module-cms/view/adminhtml/layout/cms_block_index.xml --> <!-- ... --> <referenceContainer name="content"> <uiComponent name="cms_block_listing"/> </referenceContainer> <!-- ... -->
如果你查看未解析的 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>
<div ... data-bind="scope: 'cms_block_listing.cms_block_listing'"></div>
<!-- ko template: getTemplate() --><!-- /ko -->
总之,Magento 1 用 HTML 中渲染了一个列表,然后用 JavaScript 增强了用户界面的功能。然而 Magento 2 仍然使用一些 HTML 骨架,也把大部分的用户元素的渲染转换成了 RequireJS 模块和 KnockoutJS 模板。
{ "*": { "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": {/*...*/} },
正如我们之前提到的,最初的 getTemplate 的调用最终会渲染很多子组件。第一个 KnockoutJS 模板之所以命名为 collection.html 是因为它是很多不同的 UI 组件的集合。不过今天我们涉及这整个渲染过程。
今天我们涉及的是 PHP 开发人员如何控制在 JavaScript 树中渲染什么。如果我们回到 <uiComponent> 标签。
<!-- #File: vendor/magento/module-cms/view/adminhtml/layout/cms_block_index.xml --> <uiComponent name="cms_block_listing"/>
#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>
- 为根级别的 listing 节点查询 PHP 类名和默认参数
- 用 argument 节点作为构造函数的参数来实例化这个类
#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>
$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' ] ], ] )
以上参数的数据来自于合并在一起的节点。每一个参数有不同影响——但是我们感兴趣的是 templates/listing/default 这个参数。它指的是渲染这个 UI 组件的 XHTML 模板。template/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>
#File: vendor/magento/module-ui/TemplateEngine/Xhtml/Result.phppublic 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>'; }
#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml<?xml version="1.0" encoding="UTF-8"?><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 组件。UI 组件中任何名字不为 argument 的子节点被当做父对象的子节点。比如,当 Magento 渲染 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="MagentoUiComponentContainer"><!--...--></listingToolbar> <columns class="MagentoUiComponentListingColumns"><!--...--></columns> </components>
$uiComponent = new MagentoUiComponentListing(...); $listingToolbar = new MagentoUiComponentContainer(...); $columns = new MagentoUiComponentListingColumns(...); $uiComponent->addComponent($listingToolbar); $uiComponent->addComponent($columns);
#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.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>
#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', } //... }); });
#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>
{ "*": { "Magento_Ui/js/core/app": { "types": {/*...*/}, "components": { "cms_block_listing": { "children": { "cms_block_listing": {/*...*/}, "cms_block_listing_data_source": { "type": "dataSource", "name": "cms_block_listing_data_source", "dataScope": "cms_block_listing", "config": { "data": { "items": [], "totalRecords": 0 }, "component": "Magento_Ui/js/grid/provider", "update_url": "http://magento-2-1-0.dev/admin/mui/index/render/key/e628fdf18db9219474935e85ab3f25b445287503a00a230704b4168c566f8059/", "storageConfig": { "indexField": "block_id" }, "params": { "namespace": "cms_block_listing" } } } } } } } } }
Magento 将在 dataSource 组件中查询组成 UI 组件的实际数据(比如,model 已渲染的数据集合)。
- UI 组件渲染 x-magento-init 脚本,这个脚本填入了 KnockoutJS view models 的全局注册表。
- UI 组件也渲染 HTML 骨架,HTML 使用 KnockoutJS 和自定义 scope 绑定来渲染构成组件的 DOM 节点。
- ui_component XML 文件是一个特定领域的语言来表示 UI 组件对象的嵌套的等级,最终,Magento 将会用它来渲染 x-magento-init 脚本的 JSON 。
- ui_component 的 XML 节点 name 用来查询 PHP 类以示例化。
- Magento 把所以子 <agument /> 节点作为那个类的构造函数的参数。
- Magento 使用任何名为 <dataSource> 的子节点来渲染用在 UI 组件中的实际数据(比如,网格列表信息)。
- 其他任何子节点将会被用在渲染子 UI 组件,这些子 UI 组件将遵循父级的规则。
- 顶层 UI 节点配置一个 XHTML 模板,Magento 通过 PHP 渲染它。
- UI 组件节点配置 RequireJS 模块,Magento 把 RequireJS 模块当做 KnockoutJS view model 构造函数。
就像你看到的,虽然 UIComponent 标签很好地简化了 Magento 2 的布局句柄 XML 文件,他们仍然隐藏了很多更复杂的 UI 渲染系统,包括前端和后台的 Magento 系统代码,并且要求开发人员理解 Magneto 的自定义 RequireJS 和 KnockoutJS 。
$ pestle.phar magento2:generate:ui:grid Which Module? (Pulsestorm_Gridexample)] Pulsestorm_ToDoCrud Create a unique ID for your Listing/Grid! (pulsestorm_gridexample_log)] pulsestorm_todo_listing What Resource Collection Model should your listing use? (MagentoCmsModelResourceModelPageCollection)] PulsestormToDoCrudModelResourceModelTodoItemCollection What's the ID field for you model? (pulsestorm_gridexample_log_id)] pulsestorm_todocrud_todoitem_id
Which Module 参数告诉 pestle 你想要创建你栅格列表的 Magento 模块。一般来说,跟 collection 文件是同一个模块,但是在这个惯例上没有硬性规定。之前的教程里我们已经指定了 Pulsestorm_ToDoCrud 模块。
Create a unique ID for your Listing/Grid! 这个参数是我们想要的 UI 组件 的名称。这将是在 <uiComponent /> 标签中属性 name="" ,也是在磁盘上的 UI 组件 XML 文件的基本文件名。
What Resource Collection Model should your listing use? 这个参数是 model 集合用的类名。我们想要栅格列表展示 Pulsestorm_ToDoCrud model,所以我们使用 PulsestormToDoCrudModelResourceModelTodoItemCollection 这个集合。
What’s the ID field for you model? 这个参数是一个 model 的数据库表的主键数据库列。
<!-- File: app/code/Pulsestorm/ToDoCrud/view/adminhtml/layout/pulsestorm_admin_todocrud_index_index.xml --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="content"> <uiComponent name="pulsestorm_todo_listing"/> </referenceBlock> </body> </page>
<!-- File: app/code/Pulsestorm/ToDoCrud/view/adminhtml/ui_component/pulsestorm_todo_listing.xml --> <!-- ... --> <columns> <!-- ... ---> <column name="title"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">text</item> <item name="label" xsi:type="string" translate="true">Item Title</item> <item name="sortOrder" xsi:type="number">20</item> </item> </argument> </column> <!-- ... ---> </columns> <!-- ... -->
除了生成 UI 组件的 pulsestorm_todo_listing.xml 文件,pestle 也生成了一个 “提供数据” 类和一个“操作页面” 类。
#File: app/code/Pulsestorm/ToDoCrud/Ui/Component/Listing/DataProviders/Pulsestorm/Todo/Listing.php <?php namespace PulsestormToDoCrudUiComponentListingDataProvidersPulsestormTodo; class Listing extends MagentoUiDataProviderAbstractDataProvider{ public function __construct( $name, $primaryFieldName, $requestFieldName, PulsestormToDoCrudModelResourceModelTodoItemCollectionFactory $collectionFactory, array $meta = [], array $data = [] ) { parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); $this->collection = $collectionFactory->create(); } }
#File: app/code/Pulsestorm/ToDoCrud/Ui/Component/Listing/Column/Pulsestormtodolisting/PageActions.php <?php namespace PulsestormToDoCrudUiComponentListingColumnPulsestormtodolisting; class PageActions extends MagentoUiComponentListingColumnsColumn{ public function prepareDataSource(array $dataSource) { if (isset($dataSource["data"]["items"])) { foreach ($dataSource["data"]["items"] as & $item) { $name = $this->getData("name"); $id = "X"; if(isset($item["pulsestorm_todocrud_todoitem_id"])) { $id = $item["pulsestorm_todocrud_todoitem_id"]; } $item[$name]["view"] = [ "href"=>$this->getContext()->getUrl( "adminhtml/pulsestorm_todo_listing/viewlog",["id"=>$id]), "label"=>__("Edit") ]; } } return $dataSource; } }
66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666 谢谢小威