目前为止不管后台写了多少逻辑(已经登录了google,取了相册数据),我们的gadget都还是那个看上去白白的gadget。而要想让它看上去有所不同,就要在main.xml这个文件中,制定我们想要的“长相”(就跟征婚启事里写的一样,身高1米6至1米7,体重不超过55公斤,相貌端正,贤良淑惠)。
如果你已经下载了我提供的源码,就可以打开看看,对照实际效果来看代码,应该很好理解。我们总计在界面上放了几样东西:
其中值得注意的事情有这么几件:
一是背景图片绝非可有可无,按google的说法,像label这种东西,如果没有放在一张背景图片之上的话,是显示不出来的。
二是gadget中界面的内容,样式和布局都在这一个文件中指定。
三是gadget的界面没有html那种流动布局的效果,就是说,所有要显示的元素,必须明明白白的指出它的位置,也就是每个元素的x和y属性,是从该元素的父元素左上角开始计算的坐标。如果你先写了一个label(拿label举个例子,实际上用什么效果都是一样的),再挨着它写了一个label,两个label你都没有指定x和y的值,那么这个两个label会重叠着显示在一起。不信你可以试一试。
四是图片的源文件位置,从代码中可以看到指定本机上的相对目录是可以的,那么指定一个网络上的url可以么?例如http://www.sina.com.cn/images/logo.gif?如果你头脑中还存在着html的印象,可能想当然的以为可以这么做,而事实上不行,gadget与web没有天然的联系(没记错的话,我已经说过四次了)。后面处理相册缩略图的时候,我们会看到怎么把网络上的图片显示出来。
写过图形用户界面程序的人一定想问,如何让界面上的元素与代码产生联系呢?例如我们的列表,我想在代码中对它作些修改的时候,如何取得它的引用呢?在gadget中这一点还比较方便,主要有两个途径:一是只要你给元素赋了name属性,例如我就给列表项起了一个名字叫做contentlistbox,在main.xml中的这一行:
之后就可以直接在代码中用contentlistbox这个值来访问这个列表项了(而且任你在代码中怎么找,也找不到声明或者初始化这个变量的地方)——当然前提是起的名字必须是唯一的。有意思吧?
第二种方式比较传统也比较少用,可以通过dom对象访问每个元素。
废话不多说,来看看在代码中给列表插入列表项怎么做。
列表项对应着gadget api提供的一个名为item的对象实例,但我们要用new item()这样的语法来得到一个新的列表项并逐一设置它的属性么?不不,有更简便也更好玩的方法,我们只要新建一个字符串:
然后调用列表contentlistbox的方法来添加就可以,像这样:
方便么?这种用法使得开发人员不需要为一个图形界面的组件掌握两套语法(xml的和javascript的),非常贴心。
好,现在来说另一个问题,既然不能为一个img对象的src属性指定一个网络地址,那到底如何显示网络上的图片呢?答案很长,如果你有了图片的url(就是 http://开头的那种啦),首先要通过xmlhttprequest把图片的数据取回来,然后把这部分数据赋给src属性。
具体点,记得一个请求最重要的四部分数据么?url:就是该图片的url;请求类型:因为是要求数据,自然是“get”;请求头:对本请求来说没有;消息体:同样没有。
所以发请求的部分并不困难,待请求的状态变为4,也就是说明回传数据已到达的时候,就可以从请求的responsestream这个属性得到图片的二进制数据。假设在代码中我们要显示的图片是,记得么,使用名字可以直接访问这个图片,再假设我们的请求对象取名为xhrequest,像下面这样:
如此就可以了!哈哈,简单吧(我当初倒是找了半天,读过了youtube gadget的代码才参透呢,愚笨愚笨)。
在我们剩下的唯一一个重要函数main的fetchalbumthumbnail()中,就是使用这种方法来取得相册缩略图的图片并显示在gadget的界面中的。
这个函数我就不逐一分解了,相信你一定看得懂。
大的方向上说,从picasa服务器上取数据,有两种方式,一种是使用google已经开放的各种语言的api,可以在页面http://code.google.com/apis/picasaweb/developers_guide_protocol.html找到很多相关的信息。另一种方式便是使用最朴素的网络请求方式来自己构造请求并解析回传的数据。
由于picasa只提供了java,.net,python和php的接口,而gadget目前只能使用javascript,因此我们只能使用朴素方式。
继续第三节的路子,仍然使用xmlhttprequest向picasa服务发起请求,也要处理好四部分信息。
请求发向哪个url:为了获取picasa的相册信息,要向http://picasaweb.google.com/data/feed/api/user/default发请求,这个url其实可以有很多变化的地方。例如user/default这个地方是请求所附token所属的用户相册信息,这里当然可以明确的指定用户名。”api”可以换成”base”,这个将影响回传数据的格式,但goolge推荐使用api而不是base。
请求的类型:我们是要索取数据,因此这是一个查询的动作,应该使用get。
请求头:只需要把token放进去就好。这样来放:
消息体:对于我们查询相册的请求,不需要任何的消息体。
具体的代码都在main.prototype.fetchalbumsinfo()函数中,就像这样:
最后两个函数是下一步要做的工作:解析回传的相册数据,并下载每个相册的缩略图。
要想解析回传数据,首先得知道回传的数据是什么。你可以把这些数据打印出来看看,应该是类似下面的样子:
怎么,看着有点眼熟?没错,这个回传数据所使用的格式正是标准的atom feed(更多的描述可以参考w3c的标准和下面的链接:http://code.google.com/intl/zh-cn/apis/picasaweb/developers_guide_protocol.html)。
可以根据atom feed的格式来编写我们解析回传数据的函数parsealbumfeed(),这个函数的作用是从回传的xml数据中找出我们关心的几样东西:该用户目前拥有的所有的相册信息,包括每个相册的标题,描述,访问权限以及缩略图的地址。找出这些信息以后,将会拼成一个包含相册(album)的数组作为函数返回值。
具体代码如下:
这个函数中用到了一些我们还没有新建的类,相册(album)以及缩略图(thumbnail)。这些类的声明可以放在一个新的名为album.js的文件中,并在我们整个gadget的main.xml文件中指名要导入它。因此main.xml的最后几行应该看上去是这个样子:
而album.js的内容大体如下:
最后还要在main.js里面添加一个函数createdomdocument(),用来提供一个dom对象供我们解析xml用。代码如下:
下一节来说说怎么取得相册的缩略图并显示在gadget的界面中。
向google的服务器发起登录请求之后,得到了免死金牌token,以后就可以拿着这个token去犯罪,不是,去google的其它服务取数据,但是在此之前应该第一,从响应的消息中把token找出来;第二,这个token应该想办法保存起来,以备以后使用。
上一节已经把响应的内容打印了出来,它的格式也很简单,因此用下面的代码很容易就可以把响应的内容转成方便我们使用的形式,即一个map的形式,通过键值对来存储:
在我们的相应回调函数里,就可以调用这个函数处理一下响应,从结果中取键为”auth”这一项的值,并保存在gadget host为我们准备好的一个负责持久化的对象options中。找到上一节main.prototype.login的代码,把响应的回调函数改成下面的样子:
最后加的一行main.onloginsuccess()就是我们下一步动作的起点,在这里应该开始去取用户mymail2009.test@gmail.com所拥有的相册信息了,我们先声明一个空函数放在那里。
继续之前多扯两句options这个对象,这是gadget host提供的持久化对象,你可以从代码中看到它还有对存储的内容进行加密的功能,google的文档中提到这个options对象在后台实际上是把内容保存在一个xml文件中,当然该文件的位置是不会告诉你的啦,哈哈。
下一节将向picasa服务发起请求!
google帐户最早用来申请巨大的gmail邮箱(如今看来,一般个大吧),随着后来的blogger,picasa,docs等各种服务上线,也就顺路继承了过来。现在使用一个google帐户,就可以同时使用这些服务。
既然我们打算写一个从picasa取相册数据的gadget,就免不了要先了解一些和goolge帐户有关的知识。因为picasa的数据也是受保护的,并非谁要看都可以(公开的相册除外哦,那都是炫耀册,巴不得全天下人都看见呢),我们的程序也不例外,要想取到相册的数据,程序必须向google的服务器证明自己得到了相应用户的授权。
一个人类用户当然可以这样做:打开picasa的凯发k8网页登录首页,发现要求登录,于是输入自己的用户名密码,成功后就查看自己的相册。我们的程序可干不了,它不会打开浏览器,好吧,这个它会,但打开以后它找不着用户名的输入框在哪,即便找到了,也不知该往里面填什么,即便填对了,也不知要看什么,即便看到了,也看不懂,即便看懂了也学不会……(读者:你贫不贫?)
所以一切的一切都还要咱们自己来写,当然少不了google的帮忙。
为了方便应用程序的登录,google在自己的服务器上开放了被称之为“google account authentication”的服务,我们只用到其中一种方式:clientlogin。使用这种方式访问google的服务大致是下面的流程:
很容易看出来,这基本上是一个两步骤的工作:首先使用一个google帐户访问google account authentication 服务,并得到一个可以合法访问服务数据的token(google把它叫做得到一个“授权”,不过习惯上还是叫token吧,就是令牌,拿了以后皇帝不能砍你头的那种,此过程也叫做申请一个token);使用上一步得到的token去访问具体的服务并取得数据(我们的例子中就是访问picasa服务)。
有一些东西从图上看不出,我来说一说。一是程序访问gmail的时候使用的不是这种方式(毕竟gmail太早啦,那时连google自己都没有考虑清楚吧),但其他大部分goolge服务,包括calendar,docs,picasa,blogger,contacts,google apps等等,都是上面这个流程。二是并非申请了一个token以后,就可以访问google所有的服务,实际上需要为每个服务申请不同的token。
具体到代码中,我们使用xmlhttprequest对象来发送请求并且接受回传的数据。
xmlhttprequest是gadget host提供的一个类型(注意我没有说对象,因此要用的时候你还得自己初始化,也就是new一下,哈哈),其行为与w3c所指定的标准xmlhttprequest相同。再一次的,不要联想到浏览器,你不能假设这个xmlhttprequest与ie或者firefox提供的xmlhttprequest有任何联系,更不能依赖这样的假设来编写程序。
好,废话少说,还用上一节新建的“白gadget“(笑),在main.js文件里添加这样一个函数:
调用这个函数就可以得到一个xmlhttprequest的对象啦。
然后为我们的gadget添加一个主类,并把需要的对象引用也声明好,这些都写在main.js文件中,像这样:
我们就要在main.login()函数中写我们取token的逻辑。
详细说说申请token的过程。请求是通过xmlhttprequest对象发起的,而对一个请求来说,最重要的信息有四个:请求的url,请求的类型,请求头和消息体。
url是说你的请求要发往哪里,既然我们要使用google的服务,那当然要往google那里发了,具体应该为:
https://www.google.com/accounts/clientlogin
如果你没有看出这是一个安全的https请求,那我提醒一下(如果你看出来了,我就不提醒了,笑)。
请求的类型是指你要google的服务器替你做什么事情,是返回你要查询的数据?还是为你更新已有的数据,抑或仅仅是提交一些数据,还是要服务器帮你删除一些数据?
google的服务器通过你提交请求的类型来做相应的操作,每一种操作的类型对应如下:
看着眼熟么?没错,正是轻量级的web service接口rest!
我们做登录显然是一个提交的动作, 要把我们的用户名和密码告诉google,因此我们的请求类型是post。
对登录来说,请求头没有特殊要求,只需要请求头content-type
,
其值为application/x-www-form-urlencoded
所需的用户名,密码等信息被统一称为“属性”,属性的值将放在消息体中发送。因此你的消息体看起来是下面这个样子的一个字符串:
email=mymail2009.test@gmail.com&passwd=mymail2009&service=lh2&source=gd-picasa-gadget-1.0.0.0&accounttype=hosted_or_google
注意其中红色的部分,用户名和密码的位置你当然很容易找到,”service=lh2”这一项就指明了你要为访问什么服务申请token,lh2是指picasa,如果访问google docs则要填writely,详细的列表可以看这一节最后的附录。
好,把登录的代码整个贴出来,你应该很容易找到以上四部分对应的地方。
在请求的回调函数中,目前只是先简单的打印了相应的文本内容,实际上应该在这里做更多的事,详情咱们下节再聊。如果你看到类似下面这样的输出内容,说明登录的请求成功了。如果没有成功,很可能是因为我已经换掉了用户名和密码,用你自己的google帐户试试看。
应该看到的内容:
sid=
dqaaahyaaadyq4htotaeyru0uexp9yxz1uc_w3-kbtzfpug78xqdgiykob-sv2qdxtduol-
nprjm9ssq-aevsbodrcuy3uwgfm8sx_z6fxzpgajzhzqx5ytzr0ajhcekfh
4yoobfs0ice2li0lwqs6_2bfyiullmwra8m3vfuvzne3chjruhza
lsid=
dqaaahgaaaclsimwrfkaonw8ziytz7neizjnmqzojinqsdxm3elei36mv
7gzm72bmiqdqawt8fd1dpp68p5bs1xyoxupmdunuszm1bzsaixbieouajz1xjlysuqg-0p9969zycvum2tqwka1bfvu2uqvjmaabsgj10vkzzvcabzb8nqf_mwryg
auth=
dqaaahcaaaclsimwrfkaonw8ziytz7neizjnmqzojinqsdxm3elei36
mv7gzm72bmiqdqawt8fcmxysit75kflxcis5bznncsyvucwkm-dtnzctoutm9iwojyvnbud9utfyzpdbu1oyxsfy_qjhzfzdat2qc
cexsiykmvlfhhit9rpz4gk2xlq/n
auth那一项后面的值就是token啦,可以不被砍头了。
附录:已知的google服务及服务名
calendar data api |
cl |
google base data api |
gbase |
blogger data api |
blogger |
contacts data api |
cp |
documents list data api |
writely |
picasa web albums data api |
lh2 |
google apps provisioning api |
apps |
spreadsheets data api |
wise |
youtube data api |
youtube |
用到的工具有两个,一个是随google desktop sdk附带的gadget designer,用来编写并有限的预览界面,还可以调试javascript(这个就更有限了);一个是google desktop,用来测试写好的gadget。下面要写的例子是我在为某研究院某个项目策划阶段作poc时所写的一个小例子,可以显示一个google用户的picasa相册中的album名称和缩略图。虽然很小,但包含了google账户的自动登录,显示网络图片,xmlhttprequest的使用等很多实用技巧。整个完成之后是这个样子:
请跟我一起来。现在打开gadget designer,选择file->new gadget,输入了名称“picasa”之后,就可以看到一个完整gadget的雏形了。你可以找到这个项目所在的文件夹,双击其中的gadget.gmanifest,此时如果你已经安装了google desktop,就可以看到desktop自动启动,并把这个很“白”的gadget(别笑,除了一张白色背景图片以外,确实什么也没有)显示在sidebar中。如图:
到项目文件夹里可以看到一个main.xml文件和一个main.js文件。我们的界面就是在main.xml文件里指定的,打开它,可以看见它指定了一张gadgetdesigner帮我们生成的白色png图片作背景,还指定了我们要导入哪些个.js文件。我们来小改两个地方:
一是把view的height改成250,二是给img元素添加一个属性name并给一个值,就像这样:
然后双击gadget.gmanifest,看看更改效果:
乍一看貌似没什么改变,但是注意看我用黑色线圈出来的那一条横杠,那是我们的gadget的下边沿,说明它的高度还是变化了,但是白色的背景没有变,因为我们没有改变背景图片的大小。现在通过.js文件中代码的方式来改变背景图片的高度,可以看出些有意思的东西。
打开main.js文件,你应该会看到一个view_onopen()函数,这就是gadget启动时会自动调用的第一个函数(好吧,并不严格,但是在调用的顺序上,它的确是相当靠前的),我们就在这个函数内部添加下面这一句:
再双击gadget.gmanifest运行看看,白色背景也变高了吧。
我知道你一定会问,代码里的bgimage是什么东西?怎么没见在任何地方声明这个变量,也没见任何地方作初始化呢?回想我们刚才在main.xml文件里做了什么?我们给背景图片取了一个名字,叫bgimage,而且别怀疑,你在代码里访问的这个bgimage,正是那张图片!背后的工作就是gadget host通过javascript引擎为我们做的,凡是在.xml文件里放置的东西(无论什么,图片也好,按钮也好,一个抽象的div也好),只要你给了一个name属性,在javascript代码中就可以直接使用这个名字来访问该对象(前提是你给的名字得是独一无二的),这与浏览器中随时可以访问document对象而不用做任何声明一样,那是浏览器这个运行环境提供的对象,随时可用。
另一个值得注意的地方是在.xml文件里,属性的值都必须加上引号,像height=”250”(因为那里使用的是标准的xml语法),而在javascript代码中,就要根据属性具体的类型来决定,像高度这种整数型的值,就不用加。
你可能还会问,那么bgimage这个对象,是什么类型的,它有些什么属性和方法可供我使用呢?它是一个img类型的对象,参考这个链接,这也是google desktop gadget的api参考页面,列出了gadget host提供的各种对象属性和方法的说明(虽然事实验证,google自己列的这些都不全面,后话)。
最后叮嘱一句:尽管main.xml文件里的东西(什么img啊,以后还会加进div啊,checkbox之类的东西)看起来多么的像html,gadget都和web没有天然的联系。google自己发布了一些gadget,例如gmail和google docs,外观与这两个服务的网页非常像,再加上gadget也主要使用javascript开发(也少不了universal gadget跟着掺合),间接导致了总有人把gadget显示的地方考虑成一个小的浏览器窗口,而想把web的一些东西简单的放在这里,到底行不行呢?李宁说:一切皆有可能。阿迪说:没有不可能。匹克说:我能,无限可能。我要说:可能,但很难(笑)。
所以在编写gadget的时候,最好的方法是把它当成纯粹的桌面程序,忘掉web的那一套。
这一节给大家入个门,下一节开始说说在gadget中怎么做google帐户的登录,还会很罗嗦的,请见谅(笑)。
下面是一个gadget项目在google desktop disigner里面的结构截图。
资源这东西好理解,无非是程序要用到的各种图片啦,字符串啦等等。读者:字符串?什么意思?答:把程序会用到的一系列字符串统一存放,想引用的时候使用一个常量名字就可以,而不必在需要这些字符串的地方每次都重写一遍,和java中的property文件作用类似。
其余的两部分会分节来详细讲解。
当然说只有三部分,是指我们大多只关心这么多,实际上还有第四部分,一个gadget settings文件,其中大多是关于这个gadget的元信息,什么作者啊,创建日期啊,uuid啊,户口所在地啊,最高学历啊,婚姻状况啊,哦,我给说成简历了(笑)。
前面也说到过,一个gadget其实就是一个桌面应用程序(再一次的,不管写起来某些语法多么得像html,gadget与web都没有天然的联系),只不过这个程序在gadget host的管理之下,行话叫“托管”。windows下没有单独的gadget host,它被合并在google desktop里面(算是另一种捆绑吧)。而linux下的确有干干净净的gadget host,且有源码下载,我们所有对gadget的理解也都源于这个版本和相关的文档。
那么在gadget host看来,一个gadget是什么东西呢?
以我写的一个小picasa gadget为例,在picasa gadget初次加载之前,它是一个.gg的压缩包(其实就是一个标准的zip包,被改了后缀名而已),gadget host会从中读取需要的文件,然后做相应的解释。
gadget host可以看成只有两部分组成:一个ui的渲染器和一个javascript引擎。
说ui渲染器之前就不得不回头重提刚才说到的一个gadget包括了一系列.xml文件这件事。实际上这些.xml文件就是用来指定你想写的gadget的界面的,就是说,你的gadget跑起来以后长成什么样子,是由这些个.xml文件来决定的(当然,严格说来可以使用javascript在运行时改变一些内容,但请不要抬杠,笑)。
这些.xml文件中最主要的是main.xml这个文件,你的gadget窗口有多大,在什么位置有几个按钮,列表有没有滚动条,背景是什么颜色等等,都在这里指定。还包括这些东西上的事件监听函数也一并在这里声明(不知为何,让我莫名的想起微软的mfc,当然,严格说来可以使用javascript在运行时动态改变这些内容,但请不要再次抬杠,笑)。
ui渲染器干什么呢?就是来把这个.xml所要求的界面转换成具体的系统调用,让操作系统来完成绘图(好吧好吧,你喜欢严格,那我告诉你,linux版本下首先被转换为qt的c 类,由qt来发起对系统绘图的调用)。
既然gadget的程序逻辑都使用javascript来编写,理所应当的,gadget host必然要包含一个javascript解释器来解释这些代码,这个解释器也被叫做javascript引擎。gadget host里确实有这么个东西,叫做spider monkey,它恰好也是firefox所使用的javascript引擎。广义上说,一个引擎的作用主要是解释它遇到的一切javascript代码,如果代码使用到核心javascript的功能和对象,它便直接提供;如果代码使用到了一些依赖于底层的对象(例如gadget host就提供了很多专有的javascript对象和方法供使用,这些都是核心javascript之外的东西),则引擎还要负责转发这样的请求(你可以说,这实际上是适配器做的事,我这样简化有助于理解,请不要一再抬杠,笑)。
也可以这样从逻辑上看gadget的组成:即一个gadget就是一组图形界面,加这些界面上每个控件(按钮啊,列表啊,输入框等等)的事件监听函数,这种界面描述与事件逻辑分离的程序模型,和微软的xaml c#简直如出一辙。因此一个gadget的开发实际上也就可以分为这两大步骤:先写界面的xml文件,再写逻辑部分的javascript。下面一节就用一个小例子来看看具体如何做。别嫌我说得太详细哦。