在学习新框架时,我们经常会看到一些描述框架基本功能的简单演示,例如著名的TodoMVC Application 。 太好了–我的意思是谁不喜欢Todo应用,对不对? 今天好了,我们将采取略有不同的方针。 我们将避免通用,而是将重点放在Aurelia框架的独特核心功能之一:视觉合成上。
上一篇文章已经介绍了新手 Aurelia,它具有扩展HTML的功能。 到本文结束时,我们应该更好地了解合成如何帮助将小的可恢复组件组装成复杂的屏幕。 为此,我们将创建一个报告生成器应用程序。 您可以在此处找到该应用程序的演示,并在此处找到完整的源代码 。
什么是视觉组成?
在计算机科学中,组合的基本思想是在对象组合的情况下,采用小的实体,简单的对象/数据类型,并将它们组合成更大,更复杂的对象/数据类型。 同样的情况适用于函数组合,其中一个函数的结果作为属性传递给下一个,依此类推。 视觉合成通过允许一个人将多个不同的子视图聚合为一个更复杂的视图来共享这一基本概念。
讨论视觉组成时要考虑的重要事项是异构子项目和同类子项目之间的区别。 为了理解这一点,让我们看下图。
视觉构图类型的比较
在左侧,我们看到了均匀组成的示例。 顾名思义,这都是关于渲染具有相同类型和仅变化内容的项目。 创建重复列表时,大多数框架都使用这种类型的组合。 如示例所示,想象一个简单的项目列表,这些项目被一个接一个地依次渲染。 在右侧,我们可以看到一个异构组成的示例。 主要区别在于具有不同类型和视图的项目的组装。 该示例演示了一个页面,该页面由具有不同内容和目的的几个构建块组成。
许多框架通过路由器视图提供此功能,其中将特定的视图区域放置在屏幕上,并加载不同的路由端点。 这种方法的明显缺点是应用程序需要路由器。 除此之外,创建复杂的视图合成仍然是一项繁琐的任务,特别是如果您考虑了嵌套的合成。
另一方面,除了路由器视图之外,Aurelia还提供了另一种方法,即通过自定义元素将视觉合成作为一流的功能公开。 这样,即使在视觉上,它也可以实现关注点的分离,从而引导开发人员创建小型且可重复使用的组件。 结果是增加了模块化,并有机会从现有视图中创建新视图。
使用Aurelia的撰写元素
为了利用Aurelia中的视觉组成,我们可以利用预定义的compose自定义元素 。 它基于Aurelia的关键约定之一,即视图和视图模型(VM)对(本文也将其称为页面)进行操作。 简而言之, compose
允许我们在另一个视图内的任何特定位置包含页面。
以下代码段演示了如何使用它。 在我们希望包括Hello World
页面的位置,我们只需定义自定义元素,并将其view-model
属性的值设置为包含VM定义的文件的名称。
<template>
<h1>Hello World</h1>
<compose view-model="hello-world"
model.bind="{ demo: 'test' }"></compose>
</template>
如果需要将一些其他数据传递给引用的模块,则可以使用model
属性并将值绑定到该属性。 在这种情况下,我们传递一个简单的对象,但也可以从调用VM中引用属性。
现在, HelloWorld
VM可以定义一个Activate方法,该方法将获取绑定模型数据作为参数传递。 例如,此方法甚至可能返回Promise以便从后端获取数据,这将使合成过程等待其解决。
export class HelloWorld {
constructor() { }
activate(modelData) {
console.log(modelData); // --> { demo: 'test' }
}
}
除了加载虚拟机,还将加载相应的HelloWorld
视图,并将其内容放入compose元素中。
但是,让我们说我们不想遵循VM和视图对的默认约定。 在这种情况下,我们可以使用其他属性view
并将其指向我们要用作视图的HTML文件。
<compose view-model="hello-world"
model.bind="{ demo: 'test' }"
view="alternative-hello-world.html"></compose>
在这种情况下,仍将加载VM,但是合成引擎将不加载hello-world.html
,而是将alternative-hello-world.html
的内容插入到compose元素中。 现在,如果我们需要动态决定应该使用哪个视图,该怎么办? 我们可以做到这一点的一种方法是将view
属性绑定到调用VM的属性,该VM的值将由某些逻辑确定。
// calling VM
export class App {
pathToHelloWorld = "alternative-hello-world.html";
}
// calling view
<compose view-model="hello-world"
model.bind="{ demo: 'test' }"
view.bind="pathToHelloWorld"></compose>
很好,但可能不适合每个用例。 如果HelloWorld VM需要自己决定要显示哪个视图怎么办? 在那种情况下,我们只需让它实现一个名为getViewStrategy
的函数,该函数必须以字符串形式返回视图文件的名称。 需要注意的重要一点是,将在activate
函数之后调用该函数,该函数允许我们使用传递的模型数据来确定应显示哪个视图。
export class HelloWorld {
constructor() { }
activate(modelData) {
this.model = modelData;
}
getViewStrategy() {
if( this.model.demo === 'test' )
return 'alternative-hello-world.html';
else
return 'hello-world.html';
}
}
准备项目设置
现在,我们已经了解了compose元素如何发挥其魔力,让我们看一下报表构建器应用程序。 为了开始开发,我们在Skeleton Navigation App上进行了构建。 由于该应用程序仅使用由其他子视图组成的单个复杂视图,因此某些部分(例如路由器)已被剥离。 首先,请访问我们的GitHub存储库 ,下载master分支并将其解压缩到文件夹中,或者通过打开终端并执行以下命令在本地克隆它:
git clone https://github.com/sitepoint-editors/aurelia-reporter.git
要完成安装,请按照项目自述文件中“运行应用程序”下列出的步骤进行操作。
创建报告视图
我们应用程序的入口点是页面app.html
(位于src
文件夹中)。 VM( app.js
)只是一个空类,预加载了Twitter Bootstrap。 如下面的代码片段所示,该视图充当主应用程序的容器。 您会注意到,它是由两个单独的页面组成的屏幕,分别称为toolbox
和report
。 第一个充当我们各种可拖动工具的容器,第二个充当放置这些小部件的工作表。
<template>
<div class="page-host">
<h1 class="non-printable">Report Builder</h1>
<div class="row">
<compose class="col-md-2 non-printable" view-model="toolbox"></compose>
<compose class="col-md-10 printable" view-model="report"></compose>
</div>
</div>
</template>
查看toolbox.html
我们看到该视图正在输出可用小部件的列表以及按钮以打印或清除报告。
<template>
<h3>Toolbox</h3>
<ul class="list-unstyled toolbox au-stagger" ref="toolboxList">
<li repeat.for="widget of widgets"
class="au-animate"
title="${widget.type}">
<i class="fa ${widget.icon}"/> ${widget.name}
</li>
</ul>
<button click.delegate="printReport()"
type="button"
class="btn btn-primary fa fa-print"> Print</button>
<button click.delegate="clearReport()"
type="button"
class="btn btn-warning fa fa-remove"> Clear Report</button>
</template>
toolbox
VM通过声明一个同名的属性并将其实例化在其构造函数中来公开这些小部件。 这是通过从各自位置导入小部件并将其实例(由Aurelia的依赖项注入创建的)传递到widgets
数组来完成的。 另外, EventAggregator
被声明并分配给属性。 我们稍后再讨论。
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {Textblock} from './widgets/textblock';
import {Header} from './widgets/header';
import {Articles} from './widgets/articles';
import {Logo} from './widgets/logo';
@inject(EventAggregator, Textblock, Header, Articles, Logo);
export class Toolbox {
widgets;
constructor(evtAgg, textBlock, header, articles, logo) {
this.widgets = [
textBlock,
header,
articles,
logo
];
this.ea = evtAgg;
}
...
}
那么这些小部件包含什么? 查看项目结构,我们可以在子文件夹src/widgets
找到所有它们。 让我们从一个简单的开始:徽标小部件。 该小部件仅在其视图内显示图像。 VM通过实现属性type
, name
和icon
遵循默认模式。 我们已经看到了在工具箱转发器块中使用的那些。
// logo.html
<template>
<img src="images/main-logo.png" />
</template>
// logo.js
export class Logo {
type = 'logo';
name = 'Logo';
icon = 'fa-building-o';
}
查看textblock
小部件,我们看到了另外一种激活方法,它接受来自合成引擎的初始模型数据
// textblock.js
export class Textblock {
type = 'textblock';
name = 'Textblock';
icon = 'fa-font';
text = 'Lorem ipsum';
activate(model) {
this.text = model;
}
}
为了查看该模型如何可用于视图,让我们看一下report
页面。 我们认为它是均质和异质成分的混合体。 该报告实际上是一个无序列表,将输出添加到其中的所有小部件-这是同类部分。 现在,每个窗口小部件本身都有不同的显示和行为,这构成了异构部分。 compose标记将传递初始模型以及子视图的view-model
。 此外,绘制了一个删除图标,该图标可用于从报表中删除窗口小部件。
<template>
<ul class="list-unstyled report" ref="reportSheet">
<li repeat.for="widget of widgets" class="au-animate">
<compose
model.bind="widget.model"
view-model="widgets/${widget.type}" class="col-md-11"></compose>
<i class="remove-widget fa fa-trash-o col-md-1 non-printable"
click.trigger="$parent.removeWidget(widget)"></i>
</li>
</ul>
</template>
通过查找相应窗口小部件的id
并将其从report.widget
数组中进行拼接来执行report.widget
。 Aurelia的中继器将负责更新视图,以实际删除DOM元素。
removeWidget(widget) {
let idx = this.widgets.map( (obj, index) => {
if( obj.id === widget.id )
return index;
}).reduce( (prev, current) => {
return current || prev;
});
this.widgets.splice(idx, 1);
}
通过事件进行组件间通信
我们已经提到工具箱具有“清除报告”按钮,但是这如何触发清除添加到report
页面的所有小部件? 一种可能性是在工具箱中包含对report
VM的引用,并调用此方法提供的方法。 但是,这种机制会在这两个元素之间引入紧密的联系,因为如果没有报表页面,该工具箱将无法使用。 随着系统的发展,越来越多的零件相互依赖,最终将导致过于复杂的情况。
一种替代方法是使用应用程序范围的事件。 如下图所示,工具箱的按钮将触发一个自定义事件,报表将订阅该事件。 收到此事件后,它将执行清空窗口小部件列表的内部任务。 使用这种方法,由于事件可能由另一个实现甚至另一个组件触发,因此这两个部分变得松散耦合。
用于创建清除所有功能的事件
为了实现这一点,我们可以使用Aurelia的EventAggregator 。 如果查看上面的toolbox.js
代码片段,则可以看到EventAggregator
已被注入到toolbox
VM中。 我们可以在clearReport
方法中看到它的作用,该方法只是发布一个名为clearReport
的新事件。
clearReport() {
this.ea.publish('clearReport');
}
请注意,我们还可以将额外的有效载荷与数据一起传递,以及通过自定义类型而不是字符串来标识事件。
然后, report
VM会在其构造函数中订阅此事件,并根据请求清除小部件数组。
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import sortable from 'sortable';
@inject(EventAggregator)
export class Report {
constructor(evtAgg) {
this.ea = evtAgg;
this.ea.subscribe('clearReport', () => {
this.widgets = [];
});
}
...
通过插件使用外部代码
到目前为止,我们还没有研究实际的拖放功能,我们将使用该功能将小部件从工具箱拖到报表上。 当然,可以通过本机HTML5拖放来创建功能,但是为什么要在已有许多不错的库(例如Sortable)为我们完成工作的情况下重新发明轮子呢?
因此,在开发应用程序时,常见的模式是依靠提供开箱即用功能的外部代码库。 但是不仅第三方代码可能以这种方式共享。 通过利用Aurelia的插件系统,我们可以对自己的可重用功能进行相同的处理。 想法是一样的。 我们创建了一个自定义的Aurelia插件,而不是为每个应用程序重写代码,而是托管所需的功能并使用简单的帮助程序将其导出。 这不仅限于纯UI组件,还可以用于共享的业务逻辑或复杂的功能,如身份验证/授权方案。
利用微妙的动画
因此,让我们看一下Aurelia Animator CSS ,这是Aurelia的简单动画库。
Aurelia的动画库围绕一个简单的界面构建,该界面是模板存储库的一部分。 它充当实际实现的一种通用接口。 在某些内置功能与DOM元素一起使用的情况下,Aurelia会在内部调用此接口。 例如, repeater
使用它来触发列表中新插入/删除的元素上的动画。
按照选择加入的方法,为了利用动画,有必要安装一个具体的实现(例如CSS-Animator),该实现通过在样式表中声明CSS3动画来发挥其魔力。 为了安装它,我们可以使用以下命令:
jspm install aurelia-animator-css
之后,最后一步是向应用程序注册插件,这是在手动引导阶段的报告构建器示例的main.js
文件中完成的。
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-animator-css'); // <-- REGISTER THE PLUGIN
aurelia.start().then(a => a.setRoot());
}
注意:插件本身只是一个Aurelia项目,它遵循一个index.js
文件暴露configure
函数的约定,该函数接收Aurelia实例作为参数。 configure
方法完成插件的初始化工作。 例如,它可能会注册诸如自定义元素,属性或值转换器之类的组件,以便可以直接使用它们(与compose
自定义元素一样)。 一些插件接受回调作为第二个参数,该参数可在初始化后用于配置插件。 i18n插件就是一个例子。
报表构建器在撰写阶段使用了微妙的动画,并指示从报表中删除了窗口小部件。 前者在toolbox
视图中完成。 我们将au-stagger
类添加到无序列表,以指示每个项目应按顺序进行动画处理。 现在,每个列表项都需要au-animate
类,该类告诉Animator我们希望对此DOM元素进行动画处理。
<ul class="list-unstyled toolbox au-stagger" ref="toolboxList">
<li repeat.for="widget of widgets"
class="au-animate"
title="${widget.type}">
<i class="fa ${widget.icon}"/> ${widget.name}
</li>
</ul>
我们对reports
视图小部件重复器执行相同的操作:
<li repeat.for="widget of widgets" class="au-animate">
如前所述,CSS-Animator将在动画阶段向元素添加特定的类。 我们需要做的就是在样式表中声明它们。
添加拖放
至于包括第三方库,我们可以利用Aurelia的默认软件包管理器JSPM。 要安装前面提到的库Sortable.js,我们需要执行以下命令,它将以sortable
的名称安装软件包。
jspm install sortable=github:rubaxa/sortable@1.2.0
安装后,JSPM将自动更新文件config.js
并添加其包映射:
System.config({
"map": {
...
"sortable": "github:rubaxa/sortable@1.2.0",
...
}
});
现在已经安装了该软件包,我们可以通过首先导入它然后在attached
挂钩中为我们的小部件列表注册拖放功能,来在toolbox
VM中使用它。 这时务必要这样做,因为这是视图完全生成并附加到DOM的时间。
import sortable from 'sortable';
...
export class Toolbox {
...
attached() {
new sortable(this.toolboxList, {
sort: false,
group: {
name: "report",
pull: 'clone',
put: false
}
});
}
}
您可能想知道
this.toolboxList
的来源。 在上面的动画部分中,查看toolbox
视图的ref
属性。 这只是为视图和VM之间的元素创建映射。
最后一部分是接受report
VM中已删除的元素。 为此,我们可以利用Sortable.js的onAdd
处理程序。 由于拖动的列表元素本身不会放置在报表中,而是放置在视图组成的引用小部件中,因此我们首先必须将其删除。 此后,我们检查小部件的类型,如果是文本块,我们将初始化一个文本提示,该提示将用作小部件的模型数据。 最后,我们创建一个包装对象,其中包括小部件的id
, type
和model
, report
视图将使用该包装对象来组成小部件。
attached() {
new sortable(this.reportSheet, {
group: 'report',
onAdd: (evt) => {
let type = evt.item.title,
model = Math.random(),
newPos = evt.newIndex;
evt.item.parentElement.removeChild(evt.item);
if(type === 'textblock') {
model = prompt('Enter textblock content');
if(model === undefined || model === null)
return;
}
this.widgets.splice(newPos, 0, {
id: Math.random(),
type: type,
model: model
});
}
});
}
结论
就是这样。 我们已经看到了Aurelia的compose元素如何帮助我们创建复杂的视觉合成并将所有组件很好地分离成可重复使用的小部件。 最重要的是,我已经演示了Aurelia插件的概念,可以在多个项目之间共享代码以及如何使用第三方库。 我们Aurelia小组希望您喜欢阅读本文,并很乐意在评论中或Gitter频道上回答任何问题。
From: https://www.sitepoint.com/composition-aurelia-report-builder/