桌面程序是Adobe Integrated Runtime(AIR)的杀手级应用(killer app)。在与其他工程师谈论Adobe AIR时,他们经常会提出同一个问题:什么是Adobe AIR的杀手级应用?对这个问题考虑的越深入,我就越觉得,除了工程师或架构师以外,其他人只是想使用某些应用程序中的桌面功能。
以对话应用程序为例。当然,可以通过web进行对话。AOL就有基于web的AOL Instant Messenger(AIM),还有其他不计其数的web对话小部件。 但人们仍然喜欢使用AIM、iChat或Trillian客户机,因为他们觉得用着方便。用户无须打开浏览器即可启动对话客户机。这些客户机的特点与桌面应用程序一样:拥有本地菜单;在系统托盘中显示;拥有弹出式本地窗口;可与其他本地应用程序合作;用户双击即可打开等等。
用户能够通过Adobe AIR访问跨平台技术中的所有桌面功能,同时还能使用开发web应用程序所用的那些开发工具。这真是太完美了!
本文将演示如何创建基于Adobe AIR的简单对话应用程序。它将与利用JSON传输数据的Ruby on Rails web应用程序进行通讯。为执行与Ajax有关的任务,它将利用广受欢迎的Prototype.js库与Rails应用程序进行通讯。
Adobe AIR对话应用程序还符合Adobe AIR新安全模型,这可确保从服务器上下载且在客户机上执行的JavaScript代码不会直接访问Adobe AIR应用程序编程接口(API)。通过这种方法,就不会通过强大的Adobe AIR接口下载恶意代码,以至于危害用户的计算机。
从图1可以看到,这个Adobe AIR对话应用程序实际上是由不同的HTML页面构成的:root.htm页面,它可访问Adobe AIR API并可利用这些API维护对话消息的本地数据库缓存;ui.htm页面,它可利用Prototype.js库与Rails服务器进行通讯。
图1. 处理与Ajax有关的任务以及本地数据库缓存的两个AIR页面
在Rails新安全模型中,root.html页面禁止运行解释JSON代码所必需的eval命令。这可防止JSON代码诱骗客户机运行Adobe AIR方法,以至对客户机器造成损害。
ui.html页面可运行eval命令,以解释来自服务器的JSON响应。但它不能直接访问Adobe AIR接口。在root.html和ui.html这两个页面之间有一个特殊“桥梁”,当它下载了新消息时,ui.html页面就能通知root.html页面。这种更小型、更安全的“桥梁”就是使客户机免受恶意代码侵害的机制。
对整个对话系统有了基本理解后,接下来将着手创建Rails服务器后台应用程序。
创建Rails服务器
要创建Rails应用程序,首先选择Macintosh中很好用的Locomotive应用程序。在Windows平台中,可使用Instant Rails。这些套装应用程序都拥有创建Rails应用程序所需的一切功能。它们的界面很简单,如图2所示,其中将显示机器中的所有应用程序,并可创建新应用程序。
图2. Locomotive界面
我要求Locomotive创建名为chat的新应用程序。然后利用chat应用程序中的上下文菜单在主页面中加载浏览器,如图3所示。
图3. Rails加载页面
好了,一切就绪。现在要创建数据库和对话模型了。首先创建三个MySQL数据库:chat_development、chat_test和chat_production。然后利用Rails生成器创建名为messages的新模型。
然后编辑生成的001_create_messages.rb文件,并添加要在表格中显示的数据列,如清单1所示。
清单 1. 001_create_messages.rb
class CreateMessages def self.up create_table :messages do |t| t.column :user, :string t.column :posted, :datetime t.column :message, :string end end def self.down drop_table :messages end end 我添加了三列:用户名字段、消息提交日期和时间字段以及消息正文字段。如果想使用其他对话主题扩展本例,只需在此处添加其他表格并在控制器中增加新方法。 我还对models目录下的message.rb文件进行了必要的修改,如清单2所示。 清单 2. message.rb class Message end 瞧瞧,我没进行任何修改。Rails的功能难道不齐全吗? 接着要创建对话控制器,Adobe AIR对话应用程序将利用该控制器发送新消息并选择新消息。清单3就是该控制器的代码。 清单 3. chat_controller.rb class ChatController scaffold :message def post msg = Message.new msg.user = params[:user] msg.message = params[:message] msg.posted = DateTime.now msg.save render( :text => { :id => msg.id }.to_json ) end def getall render( :text => Message.find(:all).to_json ) end def getsince msgs = Message.find( :all, :conditions => [ "id >?", params[:id ] ] ) render( :text => msgs.to_json ) end end 在代码开始部分,调用了经典的Rails scaffolding方法,以在浏览器中显示这些消息。Adobe AIR应用程序开始运行后,就可删除该方法。我还添加了新的post方法,它使用“user”和“message”两个参数,然后将新消息的提交日期设为当日。 getall方法返回一个JSON数组,数组中包含消息表中的所有数据。getsince方法也返回消息的JSON数组,其中只包含在指定id之后创建的消息。这样可提高Adobe AIR应用程序中的选择效率。 为测试该控制器,我在Rails应用程序中打开对话控制器。结果如图4所示。 图4. scaffolding列表界面 由于此时的数据库中没有任何消息,因此列表为空。单击New message链接添加消息,这将打开数据输入表单,如图5所示。 图5.使用scaffolding创建新消息 在其中输入示例消息并单击Create,这将返回如图6所示的列表。 图5.创建消息后的列表 该列表显示我有了一条新记录。很好,这表示数据库连接运行正常。但Adobe AIR应用程序不会使用该接口,因此要测试JSON接口。首先是getall方法,我在浏览器中修改URL,使其指向getall动作,就是这样!JSON返回了该条记录,如图7所示。 图 1-7.JSON getall动作 效果真不错。在导出JSON数据时,to_json方法的确非常方便。 接下来测试post方法,Adobe AIR应用程序将利用该方法提交对话消息。只要在URL中输入post动作,并指定“user”和“message”作为URL参数,就可测试该方法。 post动作返回以JSON编码的新建记录的id值,如图8所示。 图8. 消息提交动作产生的JSON响应 最后测试getsince动作。它与getall类似,但使用id参数,并且只返回id值大于指定值的消息。这里我将id指定为0,这将返回所有消息。可以看到返回了两条记录,一条是利用scaffolding添加的消息,另一条是刚刚提交的消息。 图9就是getsince动作的结果。 图9. getsince动作的JSON响应 以上就是对话示例的服务器部分。认真地讲,Ruby实现这种功能需要多少行代码?也许15行以上。真疯狂!而Rails绝不会让我失望。 接着创建在用户桌面计算机中运行的AIR对话应用程序。 我设想的对话应用程序其实很简单:在窗口顶端显示对话消息,首先显示用户名,然后显示消息。在窗口底部,我想有个地方可以输入用户名和消息,然后可以单击一个按钮将其添加到对话窗口中。我的要求如图10所示。 图10. AIR对话窗口 我将其设计得很简单,以方便读者可以根据需要对其进行修改,并创建自己的对话应用程序。 代码开始部分是XML文件,它告诉应用程序有关Adobe AIR的信息。清单4就是application.xml文件的代码。 清单 4. application.xml appId="com.example.SimpleChat" version="1.0"> appId="com.example.SimpleChat" version="1.0"> 其中只有两个关键元素:在
var db; var Exposed = {}; Exposed.trace = function(str) { AIR.trace(str); } Exposed.alertNewMessage = function() { if ( AIR.Shell.supportsDockIcon ) { AIR.Shell.shell.icon.bounce(); } } Exposed.addMessage = function( id, user, posted, message ) { var del = new AIR.SQLStatement(); del.text = "DELETE FROM messages WHERE id = :id;"; del.sqlConnection = db; del.parameters[':id'] = id; del.execute( ); var add = new AIR.SQLStatement(); add.text = "INSERT INTO messages (id, user, posted, message) VALUES ( :id, :user, :posted, :message );"; add.sqlConnection = db; add.parameters[':id'] = id; add.parameters[':user'] = user; add.parameters[':posted'] = posted; add.parameters[':message'] = message; add.execute( ); } Exposed.getMessages = function() { var get = new AIR.SQLStatement(); get.text = "SELECT * FROM messages;"; get.sqlConnection = db; get.execute(); var data = get.getResult().data; return ( data == null ) ? [] : data; } function doLoad() { document.getElementById('UI').contentWindow.parentSandboxBridge = Exposed; window.resize = document.getElementById('UI').contentWindow.childSandboxBridge.onResizeEvent; db = new AIR.SQLConnection( true ); db.open( AIR.File.applicationResourceDirectory.resolvePath( "chat.db" ), true ); var cr_exp = new AIR.SQLStatement(); cr_exp.text = "CREATE TABLE IF NOT EXISTS messages ( id, user, posted, message );"; cr_exp.sqlConnection = db; cr_exp.execute(); if ( window.addEventListener != null ) window.addEventListener( 'resize', function() { window.resize( ); }, false ); if ( document.addEventListener != null ) document.addEventListener( 'resize', function() { window.resize( ); }, false ); document.getElementById('UI').contentWindow.childSandboxBridge.startMessages(); }
您也许想知道为何要在对话应用程序中使用本地数据库。道理很简单:为了快速启动。如果旧消息存储在本地,则当每次加载应用程序时就不必回头查找它们。所要做的就是首先显示本地存储的消息,然后要求用户提供排在最后一个id值之后的新消息,这样即可得到所有的新消息。 doLoad()函数创建了该数据库。然后,为提供给ui.html页面的Exposed变量增加两个方法,分别用于添加消息以及获取消息列表。这就是本文开头提到的root.html沙盒和ui.html沙盒之间的“桥梁”。这可确保在ui.html中运行的JSON评估代码不会运行任何Adobe AIR方法。它只可访问消息添加和消息获取方法。 alertNewMessage是root.html的另一个函数,它既有趣又有用。当一条需要用户关注的新消息到达时,alertNewMessage就会使Dock栏中的应用程序图标(适用于Mac OS X机器)上下跳动。这是AIR的一项强大功能。它可以方便地访问诸如此类的操作系统特性,使用户感觉是在使用一种全面集成的应用程序。 root.html的最后一项重要功能是,它可将大小调整消息转发给ui.html,以确保将窗口调整到适当大小。 清单6就是ui.html页面的代码。 清单 6. ui.html
body { font-family: arial, verdana, sans-serif; font-size:small; margin:0px; padding:0px; } #chat td { font-size: small; }
Username:
var lastid = 0; function addmessage() { new Ajax.Request( 'http://192.168.1.192:3005/chat/post', { method: 'post', parameters: $('chatmessage').serialize(), onSuccess: function( transport ) { $('messagetext').value = ''; } } ); } function startMessages() { var msgs = parentSandboxBridge.getMessages(); for( var i = 0; i { var elTR = $('chat').insertRow( -1 ); var elTD1 = elTR.insertCell( -1 ); elTD1.appendChild( document.createTextNode( msgs[i].user ) ); var elTD2 = elTR.insertCell( -1 ); elTD2.appendChild( document.createTextNode( msgs[i].message ) ); lastid = parseInt( msgs[i].id ); } window.setTimeout( getMessages, 1000 ); } function getMessages() { new Ajax.Request( 'http://192.168.1.192:3005/chat/getsince?id='+lastid, { onSuccess: function( transport ) { var msgs = eval( transport.responseText ); for( var i = 0; i { var message = msgs[i].attributes.message; var user = msgs[i].attributes.user; var posted = msgs[i].attributes.posted; var id = parseInt( msgs[i].attributes.id ); parentSandboxBridge.addMessage( id, user, posted, message ); if ( id >lastid ) { var elTR = $('chat').insertRow( -1 ); var elTD1 = elTR.insertCell( -1 ); elTD1.appendChild( document.createTextNode( user ) ); var elTD2 = elTR.insertCell( -1 ); elTD2.appendChild( document.createTextNode( message ) ); lastid = id; parentSandboxBridge.alertNewMessage(); } } window.setTimeout( getMessages, 1000 ); } } ); } function onResizeEvent() { $('chatcontainer').style.width = (document.width-5)+'px'; $('chatcontainer').style.height = (document.height-100)+'px'; } childSandboxBridge = { onResizeEvent: onResizeEvent, startMessages: startMessages }; function doLoad() { onResizeEvent(); }
该页面首先设置onResizeEvent 回调函数。然后利用startMessages()函数从数据库中获取缓存消息的当前列表,并预加载消息表格。startMessages()函数启动一个计时器,它将调用getMessages()函数以从服务器中获取所有新消息。 getMessages()函数使用了Ajax.Request类,这是Prototype.js库中一个很好用的类,以向服务器请求执行getsince动作。当JSON返回时,代码将利用eval函数从JSON数组中获取数据,然后向表中添加新消息。 当用户单击Add Message按钮时将调用addmessage()函数,它利用Ajax.Request向对话控制器中的post动作发送用户名和消息。然后,当getMessages()函数下一次执行选择时,它就会挑选这条新消息,而该函数每秒运行一次。 最后一段代码是onResizeEvent函数,它按照窗口的新宽度和高度调整消息表。 我利用Adobe AIR SDK中的adl函数加载对话应用程序。接着输入“Cool!”并单击Add Message按钮,最后出现如图11所示的窗口。 图11.添加消息之后 要测试大小调整代码,可选中窗口右下角的控件,然后调整形状,使其更宽及更短。 图12就是窗口的最终效果。 图12.调整窗口大小之后 大小调整代码修改了表的大小,以大体适应窗口的目前面积。 后续工作 显然,这个对话应用程序远不如iChat或Trillian那样完善。但它是一个不错的应用程序基础,读者可在开发自己的应用程序时将其作为模板。就是说,通过Adobe AIR特性集,读者可以创建适用于任何协议的、功能完善的对话客户机。Adobe AIR可启动本地窗口、使任务栏图标上下跳动、播放声音、管理本地菜单、读取并写入本地文件系统、处理拖放操作等等。它拥有创建高端对话客户机所需的一切功能,且所有功能均可以JavaScript或Flex/Flash的形式提供,而这取决于您的需要。