目前為止不管后臺寫了多少邏輯(已經登錄了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這個屬性得到圖片的二進制數據。假設在代碼中我們要顯示的圖片是<img name=”myImg”/>,記得么,使用名字可以直接訪問這個圖片,再假設我們的請求對象取名為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的首頁,發現要求登錄,于是輸入自己的用戶名密碼,成功后就查看自己的相冊。我們的程序可干不了,它不會打開瀏覽器,好吧,這個它會,但打開以后它找不著用戶名的輸入框在哪,即便找到了,也不知該往里面填什么,即便填對了,也不知要看什么,即便看到了,也看不懂,即便看懂了也學不會……(讀者:你貧不貧?)
所以一切的一切都還要咱們自己來寫,當然少不了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%40gmail.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類型的對象,參考http://code.google.com/intl/zh-CN/apis/desktop/docs/gadget_apiref.html這個鏈接,這也是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。下面一節就用一個小例子來看看具體如何做。別嫌我說得太詳細哦。