Magento 2 的 UI Components 介绍(翻译)

79 浏览开发笔记

Magento 2 的 UI Components 介绍(翻译)

原文地址

原作发布于 2016年7月10日

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

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

The Positive Spin (积极的一面)

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

             

这四个 layout 添加了一个产品编辑表, 考虑到 部分

  1        js_cssprototype/windows/themes/default.css lib/prototype/windows/themes/magento.css  

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

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

通过引入,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 配置的。

     

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

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

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

如果你阅读过 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。

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

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。我们回到标签。

 

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

#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml      cms_block_listing.cms_block_listing_data_source cms_block_listing.cms_block_listing_data_source  cms_block_columns   add Add New Block primary */*/new     

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     templates/listing/default 1 mui/index/render  uiComponent     

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

$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参数。他指定了该组件渲染的 XHTML templatetemplates/listing/default字符串对应下面的模板。

#File: vendor/magento//module-ui/view/base/ui_component/templates/listing/default.xhtml 

这个 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 ''; }

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      cms_block_listing.cms_block_listing_data_source cms_block_listing.cms_block_listing_data_source  cms_block_columns   add Add New Block primary */*/new     

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

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

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

#File: vendor/magento/module-ui/view/base/ui_component/etc/definition.xml    

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

$uiCompOnent= new MagentoUiComponentListing(...); $listingToolbar = new MagentoUiComponentContainer(...); $columns = new MagentoUiComponentListingColumns(...); $uiComponent->addComponent($listingToolbar); $uiComponent->addComponent($columns);

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

#File: vendor/magento/module-ui/view/base/ui_component/etc/definition.xml    Magento_Ui/js/grid/listing    

这些就是 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 组件子节点,就是

#File: vendor/magento//module-cms/view/adminhtml/ui_component/cms_block_listing.xml    cms_block_listing.cms_block_listing_data_source cms_block_listing.cms_block_listing_data_source  cms_block_columns   add Add New Block primary */*/new        

名为 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 使用子节点 作为该类的构造参数
  6. Magento 使用 中的数据作为 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