从标签时代到富客户端:从 Web 1.0 到 Flex

Part 1: Walking through a transformation

阅读数:1353 2008 年 3 月 6 日

简介

iPhone 的成功明白无误地说明了一个事实:用户希望在他们的软件体验中感受到更多的交互。更多的 交互可以让用户更好地利用程序的特性,提高他们的效率。这就是为什么“交互”不仅仅在个 人信息管理程序中、而且在企业级业务程序中也是非常关键的。良好交互性的最大受益者是各种涉及到数 据可视化的企业应用程序,因为更高的效率直接转化为了更好的决策,也立即会变为商业利润。 Dashboard 是最典型的数据可视化应用程序。最具讽刺意味的是,今天大多数 Dashboard 在创建高效的用户 体验的过程中,缺少交互性。因此我们决定一点点地美化一个典型的 Web 1.0 的 Dashboard,给它增加更多 的交互和丰富的功能。我们不会从头开始创建完整的应用程序,这未免是在重复发明车轮。相反,我们会 重新设计界面,并把它整合到现有的服务器端架构中。通过这次学习,我们会完成一个简单但有意义的转 换。

我们在练习中使用的 Dashboard 是开源的 Pentaho BI 套件的一部分。数据和视图来自于 Pentaho BI 发行 版中的示例应用程序。

尽管我们的示例程序是一个 Dashboard 应用程序,但是其中的概念可以用于任何需要从 Web 1.0 迁移到 RIA 的项目中。我们选用的 RIA 工具集是 Adobe Flex。我们在此讨论的内容,全部是基于 Flex 框架、Flash VM 以及相关支持库的。

如果你想亲自动手完成本文介绍的内容,应该安装下面的软件:

记住首先要启动 Pentaho 服务器、登录到 Dashboard,然后才能运行 Flex 界面。Flex 界面假设你已经登 录并通过验证了。源代码还假设服务器会监听 8080 端口、等待 HTTP 请求。如果你的 Pentaho HTTP 服务器需 要监听其他的端口,请修改源代码。为了方便那些希望跳过安装 Pentaho 服务器这步骤的人,下载的 Pentaho 源代码绑定包中还包含一个伪数据集版本的 Flex 界面。

现在已经万事俱备了,下面可以仔细研究我们的示例程序了。

看看我的 Pentaho Dashboard

首先是免责声明——这篇文章能够仅仅提供了一点浅尝辄止的体验。尽管不是必须的,不 过如果你希望在深入学习本文前先了解一些背景知识的话,可以到learn.adobe.com或者flex.org去看看。这篇文章也没有打算讲述创建可维护的代码构架的最佳 实践,或者用户界面的设计实践。比如,尽管使用Ely Greenfield 的 Chart drill down 组件 [demo]可以改进用户体验,但是它 也会让你付出更多的努力才能设置并运行示例程序,因此我们在示例中并没有选用它。我们希望为那些只 想简单了解一下代码的人,提供一种单纯的复制与粘贴的体验。如果混入了第三方的组件或者一个实实在 在的 MVC 架构,将会令这个体验变得复杂。如果你想进一步深入挖掘这些主题,可以在网上或者Adobe Developer Connection上找到大量的相关文章。

为了让你能够对我们即将创建的东西有一个了解,我们先来看看最终 的效果。

查看 Demo

 

而 Pentaho Dashboard 最初的样子却是这样的:

正如前文提到的,我们使用 Flex 创建新的 Dashboard。这个基于 Flex 的 Dashboard 是用声明式的 MXML 语 言和过程式的 ActionScript 语言编写的,并利用免费的 Flex SDK 编译为 SWF 文件。SWF 文件是 Flash Player VM 上的字节码。你可以在浏览器中运行 SWF 文件,或者通过 Adobe 整合运行时(AIR)将它转化为一个桌面 应用程序。让我们先来看看创建新的 Dashboard 的源代码。

<?xml version="1.0" encoding="utf-8"?>

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="initApp()" layout="horizontal">

<mx:Script>

<![CDATA[

// import classes used in ActionScript and not used in the MXML

// ActionScript is code generated from MXML by the free Flex SDK's mxmlc compiler

// The generated ActionScript can be viewed if you pass mxmlc the - compiler.keep-generated-actionscript argument

import mx.charts.HitData;

import mx.collections.ArrayCollection;

import mx.rpc.events.ResultEvent;

// Bindable is a metadata / annotation which generates event code so that the variable



// can be used in an MXML Binding Expression, ie. dataProvider="{territoryRevenue} "

[Bindable] public var territoryRevenue:ArrayCollection;

[Bindable] public var productlineRevenue:Object;

[Bindable] public var topTenCustomersRevenue:Object;

// Variables in ActionScript have namespaces like in Java. You can use the typical public,



// private, and protected namespaces as well as create your own namespaces

// Object types are specified with a colon character and are optional but recommended.

private var _selectedTerritory:String = "*";

private var _selectedProductLine:String = "*";

// the initApp function is called when the Application's creationComplete event is fired (see the mx:Application tag above



private function initApp():void

{

// initializes our data caches

productlineRevenue = new Object();

topTenCustomersRevenue = new Object();

// initiates the request to the server to get the Sales by Territory data



// tSrv is defined in MXML below. It could also have been defined in ActionScript but would have been slightly more verbose tSrv.send();

// Since the Sales by Product Line and Top Ten Customer Sales depend on the selected territory



// we make a call to the functions that will fetch the data based on the selected territory (or pull it from cache)

// in this case the selected territory is "*" or our indicator for all. When the users selects a new territory

// these methods will be called again but the selected territory will be different

updateProductLineRevenue();

updateTopTenCustomersRevenue();

}

// Setter method that causes the data stores for the Sales by Product Line and Top Ten Customer Sales



// to be updated and the charts to be updated accordingly

public function set selectedTerritory (territory:String):void

{

// update the private backing variable

_selectedTerritory = territory;

updateProductLineRevenue();



updateTopTenCustomersRevenue();

}

// Getter method that returns the selected Territory



// This method has the Bindable metadata / annotation on it so that the selectedTerritory property can

// be used in a binding expression

[Bindable] public function get selectedTerritory():String

{

return _selectedTerritory;

}

// Setter method similar to selectedTerritory but for the selected product line



public function set selectedProductLine(productLine:String):void

{

_selectedProductLine = productLine;

updateTopTenCustomersRevenue();



}

[Bindable] public function get selectedProductLine():String



{

return _selectedProductLine;

}

// If the data is in cache then just directly update the chart based on the selected territory.



// If the data is not in cache then assemble the name value pairs that are needed by the

// web service request, then make the request.

private function updateProductLineRevenue():void

{

if (productlineRevenue[_selectedTerritory] == undefined)

{

productlineRevenue [_selectedTerritory] = new ArrayCollection();

var p:Object = new Object();

if (_selectedTerritory != "*")



{

p.territory = _selectedTerritory;

}

plSrv.send(p);



}

else

{

plPie.dataProvider = productlineRevenue[_selectedTerritory];

}

}

// Similar to updateProductLineRevenue except that both the selected territory and



// the selected product line determine the data set.

private function updateTopTenCustomersRevenue():void

{

if (topTenCustomersRevenue [_selectedTerritory + '_' + _selectedProductLine] == undefined)

{

topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine] = new ArrayCollection();

var p:Object = new Object();

if (_selectedTerritory != "*")



{

p.territory = _selectedTerritory;

}

if (_selectedProductLine != "*")



{

p.productline = _selectedProductLine;

}

ttcSrv.send(p);



}

else

{

ttcBar.dataProvider = topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine];

}

}

// This function handles a response from the server to get the Sales by territory. It reorganizes that data



// into a format that the chart wants it in. The tPie chart notices changes to the underlying ArrayCollection

// that happen inside the for each loop. When it sees changes it updates it's view of the data.

private function handleTResult (event:ResultEvent):void

{

territoryRevenue = new ArrayCollection();

tPie.dataProvider = territoryRevenue;

var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['COLUMN-HDR-ROW']['COLUMN-HDR- ITEM'];



for each (var pl:Object in event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'])

{

var spl:Object = new Object();

spl[hdr[0]] = pl['DATA-ITEM'][0];

spl[hdr[1]] = pl ['DATA-ITEM'][1];

territoryRevenue.addItem(spl);

}

}

// Similar to handleTResult except that it handles the data for Sales by Product Line



private function handlePLResult(event:ResultEvent):void

{

var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['COLUMN- HDR-ROW']['COLUMN-HDR-ITEM'];

for each (var pl:Object in event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'])



{

var spl:Object = new Object();

spl[hdr[0]] = pl['DATA-ITEM'][0];

spl[hdr[1]] = pl ['DATA-ITEM'][1];

productlineRevenue[_selectedTerritory].addItem(spl);

}

plPie.dataProvider = productlineRevenue[_selectedTerritory];



}

// Similar to handleTResult except that it handles the data for Top Ten Customer Sales



private function handleTTCResult(event:ResultEvent):void

{

var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['ROW-HDR- ROW'];

var pl:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'];

for (var i:int = 0; i < pl.length; i++)



{

var spl:Object = new Object();

spl.name = hdr[i]['ROW-HDR-ITEM'][0];

spl.sales = pl[i]['DATA-ITEM'];

topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine].addItemAt(spl,0);

}

ttcBar.dataProvider = topTenCustomersRevenue[_selectedTerritory + '_' + _selectedProductLine];



}

// This function is called to format the dataToolTips on the tPie chart.



private function formatTPieDataTip (hitdata:HitData):String

{

return "<b>" + hitdata.item.TERRITORY + "</b><br>" + cf.format(hitdata.item.SOLD_PRICE);

}

]] >

</mx:Script>

<!-- These HTTP Services communicate via HTTP to a server -->



<mx:HTTPService id="tSrv" url="http://localhost:8080/pentaho/ServiceAction?solution=samples&path=steel- wheels/homeDashboard&action=Sales_by_Territory.xaction" result="handleTResult (event)"/>

<mx:HTTPService id="plSrv" url="http://localhost:8080/pentaho/ServiceAction?solution=samples&path=steel- wheels/homeDashboard&action=Sales_by_Productline.xaction" result="handlePLResult(event)"/>

<mx:HTTPService id="ttcSrv" url="http://localhost:8080/pentaho/ServiceAction? solution=samples&path=steel-wheels/homeDashboard&action=topnmdxquery.xaction" result="handleTTCResult(event)"/>

<!-- Non-visual component to format currency's correctly. Used in the formatTPieDataTip function -->

<mx:CurrencyFormatter id="cf" precision="0"/>

<!-- Effects used to make the charts more interactive -->



<mx:SeriesInterpolate id="plEffect"/>

<mx:SeriesSlide id="ttcSlide" direction="right"/>

<!-- Stacked vertical layout container -- >



<mx:VBox height="100%" width="40%">

<!-- Nice box with optional drop shadows, title bars, and control bars -->

<mx:Panel width="100%" height="100%" title="Revenue By Territory">

<!-- Pie Chart -->

<mx:PieChart id="tPie" width="100%" height="100%" showDataTips="true" dataTipFunction="formatTPieDataTip">

<!-- Sets the itemClick property on the PieChart to the embedded ActionScript code. We could have also called a function defined above. -->

<mx:itemClick>

// calls the appropriate setter method

selectedTerritory = event.hitData.item.TERRITORY;

// tells the pie chart to explode the pie wedge the user click on



var explodeData:Array = [];

explodeData[territoryRevenue.getItemIndex(event.hitData.item)] = 0.15;

tPie.series [0].perWedgeExplodeRadius = explodeData;

</mx:itemClick>

<!-- Sets the series property on the Pie Chart. -->

<mx:series>

<!-- The Pie Series defines how the Pie Chart displays it's data. -->

<mx:PieSeries nameField="TERRITORY" field="SOLD_PRICE" labelPosition="insideWithCallout" labelField="TERRITORY"/>

</mx:series>

</mx:PieChart>

</mx:Panel>

<!-- A Binding Expression in the title bar of the Panel uses the Bindable getter for the selectedTerritory property -->



<mx:Panel width="100%" height="100%" title="Revenue By Product Line (Territory = {selectedTerritory})">

<mx:PieChart id="plPie" width="100% " height="100%" showDataTips="true">

<mx:itemClick>

selectedProductLine = event.hitData.item.PRODUCTLINE;

var explodeData:Array = [];



explodeData[productlineRevenue [_selectedTerritory].getItemIndex(event.hitData.item)] = 0.15;

plPie.series [0].perWedgeExplodeRadius = explodeData;

</mx:itemClick>

<mx:series>

<!-- The showDataEffect on the Series uses Binding to (re)use an effect defined above -->

<mx:PieSeries nameField="PRODUCTLINE" field="REVENUE" labelPosition="insideWithCallout" showDataEffect=" {plEffect}" labelField="PRODUCTLINE"/>

</mx:series>

</mx:PieChart>

</mx:Panel>

</mx:VBox>

<mx:Panel width="100%" height="100%" title="Top 10 Customers (Territory = {selectedTerritory} | Product Line = {selectedProductLine})">

<mx:BarChart id="ttcBar" width="100%" height="100%" showDataTips="true">

<mx:series>

<mx:BarSeries xField="sales" showDataEffect="{ttcSlide}"/>

</mx:series>

<mx:verticalAxis>

<mx:CategoryAxis categoryField="name"/>

</mx:verticalAxis>

</mx:BarChart>

</mx:Panel>

</mx:Application>

下载并查看源代码。注意应用程序有两个版本。一个是使用伪数据集的 pentaho_dashboard_demo.mxml,另一个是会连接到真实的 Pentaho 服务器以获取数据的 pentaho_dashboard.mxml。

迁移过程

向 RIA 迁移的过程的第一步是设计一个新界面。同时你还需要明确如何向应用程序暴露出数据和服务。 Pentaho 已经为我们暴露出了数据和服务,所以留下来最难的部分是把数据解析为 Flex Charting 组件所期 望的格式。最终你可以将后端的服务与新的界面挂接到一起。让我们来深入每一步的细节。

设计新的接口

创建新 RIA 接口的最佳思考方式是什么?不是考虑如何使现有的接口更丰富,而是要以用户希望如何查 看以及与数据交互为出发点,重新进行思考。在这个过程中,设计师会发挥很大的作用。如果可能,尽可 能地发挥他们的创造性。作为开发者,你可以先创建一个原型。为了提高效率,你需要创建一个模拟数据 集。你可以在 Sales_by_Productline.xaction、Sales_by_Territory.xaction 和 topnmdxquery.xaction 文 件中找到我们使用的数据集。在例子中,我把模拟数据集做成与 Pentaho 暴露出来的 Web Services 很相似 。尽管这不是必须的,但它简化了挂接到真正的服务上所需的工作。 有了模拟数据集,我们可以在设计 上进行更快的迭代了。

我们所做的设计看上去与原来的 Dashboard 几乎一样。如果有一位设计师帮助我们(实际上并没有), 我们可以拿出更有创造性的产品。尽管如此,我们这些开发者还是能够创建出有更多交互的 Dashboard。 我们其实可以设计出更复杂的界面来,但是在这个例子中,还是希望保持代码易于阅读和理解。

创建 UI 的代码相当直接。下面的 MXML 代码就能创建一个 PieChart:

<mx:PieChart width="100%" height="100%">

<mx:series>

<mx:PieSeries nameField="TERRITORY" field="SOLD_PRICE" labelPosition="insideWithCallout" labelField="TERRITORY"/>

</mx:series>

</mx:PieChart>

为了增加交互性,你可以添加一个 itemClick 事件处理器:

<mx:itemClick>

selectedTerritory = event.hitData.item.TERRITORY;

var explodeData:Array = [];



explodeData[territoryRevenue.getItemIndex (event.hitData.item)] = 0.15;

tPie.series[0].perWedgeExplodeRadius = explodeData;

</mx:itemClick>

当我们设置 selectedTerritory 时(就像在 itemClick 事件处理器中所做的),会引起一些其他的动作 。这个例子中,我们希望刷新数据集,这个数据集与相应的饼图绑定到一起了:

public function set selectedTerritory(territory:String):void

{

_selectedTerritory = territory;

updateProductLineRevenue();



updateTopTenCustomersRevenue();

}

我们也可以很简单地为数据集已改变的 Chart 增加一个平滑的转化。第一件要做到事情就是添加一个我 们打算使用的效果的实例

<mx:SeriesInterpolate id="plEffect"/>

我们还需要把 showDataEffect 绑定到效果的实例上,这样,当数据发生变化后,就可以通知饼状图使 用这个效果了

<mx:PieSeries nameField="PRODUCTLINE" field="REVENUE"   labelPosition="insideWithCallout" showDataEffect="{plEffect}"   labelField="PRODUCTLINE"/>

我们也可能想增加一个鼠标悬浮时的提示:

<mx:PieChart id="tPie" width="100%" height="100%"   showDataTips="true" dataTipFunction="formatTPieDataTip">

dataTipFunction 被命名为 formatTPieDataTip,后者返回一个自定义的字符串:

private function formatTPieDataTip(hitdata:HitData):String

{

return "<b>" + hitdata.item.TERRITORY + "<b><br>" + cf.format(hitdata.item.SOLD_PRICE);

}

我们可以指定很多其他的风格来自定义应用程序的感观(look and feel)。开发者可以将风格内联在 属性中,也可以定义在外部的 CSS 文件中。

暴露后端

使用 Pentaho,我们可以很容易地发现 Web Service 并与之交互。Web Service 给我们提供了用于 Dashboard 的数据。可以返回按照区域分布的销售版图的 URL 是(在本地运行时): 这里

有些 URL 会携带参数,来确定返回什么数据集。例如,“十大客户”的 Web Service 可以带 有两个参数,区域和产品类型,它们共同决定了应该返回哪些信息。

将前端挂接到后端

要想把前端挂接到后端,你首先要创建一个 HTTPService(如果使用的是 SOAP,则创建 WebService):

<mx:HTTPService id="tSrv"   url="http://localhost:8080/pentaho/ServiceAction?solution=

samples&path=steel -wheels/homeDashboard&action=Sales_by_Territory.xaction"

result="handleTResult(event)"/>

result 属性指定了响应从服务器返回后要执行的代码。在这个例子中,我们只要调用一个名为 handleTResult 的方法,并传递一个 Event 类型的参数就可以了。这个方法负责把我们接收到的数据转换为 Charts 所需的样式。无论我们在使用 Charts 还是其他诸如 DataGrid 之类的组件,都需要一些简单的处理。 在这个例子中,我们要对数据做一些处理然后再把它交给 Chart。

private function handleTResult(event:ResultEvent):void

{

territoryRevenue = new ArrayCollection();

tPie.dataProvider = territoryRevenue;

var hdr:ArrayCollection = event.result.Envelope.Body.ExecuteActivityResponse.swresult['COLUMN- HDR-ROW']['COLUMN-HDR-ITEM'];

for each (var pl:Object in event.result.Envelope.Body.ExecuteActivityResponse.swresult['DATA-ROW'])



{

var spl:Object = new Object();

spl[hdr[0]] = pl['DATA-ITEM'][0];

spl[hdr[1]] = pl['DATA-ITEM'][1];\\\\\\\\

territoryRevenue.addItem(spl);

}

}

在这个例子里,所有的代码都放到一个文件中了。在实际的项目中,我们会按照模型、视图和控制器 的架构将它拆分到不同的文件中。我们在其他的时间里详细讨论如何使用正确的设计模式来构建大型的应 用程序。至此,我们就完成了这个示范的应用程序。从现在开始到本文结束,我们将通过 Adobe Flex 讨论 RIA 的一些有趣的方面。

这个示例的精彩之处为何?

这个简单的示例证明了对于传统的 Web 应用程序来说,丰富的、带有更多交互的用户界面是多么重要。 这个程序的美妙之处,是它为现有的程序提供一个很棒的界面,却不需要进行大量的重构。这种便利一部 分要归因于 Pentaho 暴露出了它的服务器端调用,另一部分归因于 Flex 能够平稳地消化掉这些服务的能力 ,这得利于 Flex 的组件和数据绑定模型。

这里示例同样示范了如何谨慎地选择、匹配各种技术。一个优秀的、健壮的 Java 服务器程序可以有效 地被挂接到一个富 Flex 界面,从而创造出令人眩目的程序。这是一个很有前景的方向,这个示例希望能将 这种理念带入现实。

应用程序的另一方面是,我们通过真实的、可运行的代码能够看到,使用 ActionScript 和 MXML 构建 Flex 应用程序是多么容易。对于大多数熟悉 Java 和 XML 语法的人来说,ActionScript 和 MXML 看起来是相当 亲切的。不是吗?

现在还差什么?如何弥补这一缺口?

这个示例并不是产品级的软件。它更关注于一个简单的原型。但是,如果花些时间将 Pentaho Web Service 调用抽象为一个 ActionScript 库,这可能是迈向健壮程序的第一步。有一些开源项目已经开始着 手构建这样的库了。示例程序也在向这个方向靠拢,并且使用了很少的一些面向对象的构造。应用 MVC 架 构模式,面向接口编程,恰当地使用继承和多态,就能创建出可维护的、干净的应用程序。在示例程序中 ,我们只是简单复制了旧程序的外观。在理想的情况下,我们应该根据交互性需求创建出全新的界面,然 后将可视化组件和 Web Service 组件绑定到一起。为了创建出满足生产环境要求的应用程序,我们应该投 入时间和精力。

进一步阅读

这个示例仅仅是富互联网应用程序的冰山一角。 比如我们可做的改进之一就是把图表做成可以逐层深 入(drill down)细节的,并加上更有意义的过渡效果。Ely Greenfield 建了一个很漂亮的演示,他详细 说明了如何用 Flex Charts 实现 drill down 效果,请移步他的博客 。我们使用的是 Flex 2 构建示例程序,不过在 Flex 3 中,Chart 组件还包含了一些新的特性。你可以在这里或者这里读到更多的信息。如果你想学习 Flex 编程,可以查看 flex.orglearn.adobe.com

关于作者

James Ward 是 Adobe 公司的 Flex 技术布道者,也是 Adobe 的 JCP 代表,负责 JSR 286、299 和 301。就像他 最爱的爬山一样,他也热爱编程,认为它提供了无穷尽的新发现。他在爬山的过程中到过很多地方。同样 ,技术也带给他很多冒险经历,其中包括:九十年代早期是 pascal 和汇编;九十年代中期是 Perl、HTML 和 JavaScript;Java 及其大量框架都是始于九十年代后期。现在他的主要工作是利用 Flex 构建好看的前端, 而后端还是基于 Java。在来到 Adobe 之前,他曾经创建过一个服务门户。你可以在他的博客上了解更多信 息:www.jamesward.org

Shashank Tiwari 是Saven Technologies首席技术官。Saven Techologires 位于芝加哥,为银行和金融服务公司提供服务。 Shashank 是一位经验丰富的开发者、作家和演说家,在是 JSR 274, 283, 299, 301 和 312 专家组的成员。 他对大量语言都感兴趣,包括 Java、ActionScript、Ptyhon、Perl、PHP、G++、Groovy、JavaScript、 Ruby 和 Matlab。他也是 OReilly 网络的知名作者。最近他在忙于使用 Flex 和 Java 构建 Web 2.0 应用程序。更 多信息可以访问:www.shanky.org

查看英文原文:From Tags to Riches: Going from Web 1.0 to Flex

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论