使用社會網路可以更輕鬆地獲取並聚合數據,從而創建富有革新精神的新 Web 應用程序。但是,仍然必須處理創建可伸縮 Web 應用程序的所有常見問題。現在,使用 Google App Engine (GAE) 也可以簡化工作。使用 GAE,可以不必考慮管理應用伺服器池的所有事務,而是集中精力創建優秀的 mashup。本文是共分三部分的系列文章 “使用 Eclipse 在 Google App Engine 上創建 mashup” 的最後一部分,在本文中,將利用並進一步增強在前兩部分中構建的應用程序。我們將添加查看應用程序的其他用戶及訂閱其聚合提要的功能,然後通過將應用程序公開為可由其他 mashup 使用的 Web 服務完成 mashup 構建。
關於本系列
在本系列中,將了解如何開始使用 Google App Engine (GAE)。在 第 1 部分 中,了解了如何設置開發環境,以便可以開始創建運行在 GAE 上的應用程序。還了解了如何使用 Eclipse 簡化應用程序的開發和調試。在 第 2 部分 中,通過添加一些 Ajax 特性增強了該應用程序。還了解了在部署到 GAE 后如何監視該應用程序。本文是第 3 部分,將通過為應用程序創建 RESTful Web 服務返回到生態系統,這樣其他人就可以使用它創建自己的 mashup。
GAE 是創建 Web 應用程序的平台。使用它的最重要的先決條件是具備 Python 知識,因為要在 GAE 中使用 Python 作為編程語言(目前為 Python V2.5.2)。對於本系列,具備一些典型的 Web 開發技能將會有幫助(例如,HTML、JavaScript 和 CSS 知識)。要針對 App Engine 進行開發,需要下載 App Engine SDK(請參閱 參考資料)。在本系列中,還使用 Eclipse V3.3.2 以輔助 GAE 開發(請參閱 參考資料)。並且需要 PyDev 插件以將 Eclipse 轉換為 Python IDE。
訂閱
到目前為止,我們的應用程序 aggroGator 允許用戶聚合(mash up)多項常見 Web 服務並創建這些服務的聚合提要。現在,為了讓事情變得有趣一些,我們希望允許用戶訂閱其他用戶的提要(其中每個用戶的提要可能是提要本身的聚合)。例如,假定需要設置一個帳戶以在 Twitter、last.fm 和 del.icio.us 中訂閱自己的提要,這樣其他朋友隨後可以訂閱 aggroGator 提要以查看這些服務中的所有活動。要處理這種情況,需要再一次重新審視數據模型。
建模
要啟用訂閱,需要允許一個用戶(帳戶)訂閱其他帳戶列表。我們可以採取的一種方法是向每個帳戶中添加用戶列表。每個用戶將添加一個訂閱。此操作的代碼將類似清單 1。
class Account(db.Model): user = db.UserProperty(required=True) subscriptions = db.ListProperty(Account) |
這種方法有一些優點。檢索帳戶時,可以獲得該帳戶訂閱的所有其他帳戶。這是使用諸如 GAE 的 Bigtable 之類的非關係資料庫的常見策略:把所有相關數據保存在一起並且無需擔心標準化之類的事務。但是,這種方法有一個缺點。如果需要顯示特定用戶訂閱了哪些人的提要該怎麼辦?這樣做的惟一方法是檢索所有 Account 模型,查看所有訂閱,並查看給定用戶是否位於列表中。另外,可以在每個 Account 模型中保存兩張列表 — 一張用於 subscriptions,一張用於 subscribers。我們不會採取這種方法,而是使用更傳統的多對多模型,如清單 2 所示。
class Subscribe(db.Model): subscriber = db.ReferenceProperty(Account, required=True, collection_name='subscriptions') subscribee = db.ReferenceProperty(Account, required=True, collection_name='subscribers') |
正如您所見,這個模型類似於關係資料庫中的連接表(join table)。只是因為 GAE 使用非關係資料庫(Bigtable)並不意味著您不能利用與關係資料庫結合使用的技術。現在數據模型已經就緒,讓我們詳細查看如何從最終用戶的角度創建這些多對多關係。
訂閱管理
我們的應用程序可以存儲訂閱,因此只需要一種方法能夠讓用戶創建訂閱。為此,需要為用戶創建一個用於添加訂閱的頁面(參見清單 3)。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/ xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <title>Aggrogator Accounts</title> <link rel="stylesheet" href="/css/aggrogator.css" type="text/css" /> <script type="text/javascript" src="/js/prototype.js"></script> <script type="text/javascript" src="/js/builder.js"></script> <script type="text/javascript" src="/js/effects.js"></script> <script type="text/javascript" src="/js/aggrogator.js"></script> </head> <body> <img id="spinner" alt="spinner" src="/gfx/spinner.gif" style="display: none; position: fixed;" /> <div id="logout"> {{ account.user.nickname }} <a href="{{ logout_url }}">Logout</a> </div> <div class="clearboth"></div> <ol> {% for acc in all_accounts %} <li> <a href="" onclick="subscribe('{{ acc.user.email }}'); return false;"> {{ acc.user.email }}</a> </li> {% endfor %} </ol> </body> </html> |
正如您所見,此頁面只顯示系統中的所有帳戶。然後用戶通過單擊選擇要訂閱的帳戶。您可以設想更複雜的界面。例如,如果存在大量用戶,這樣的界面將變得笨拙,因此基於搜索的系統將更好。或者使用一個系統,允許用戶導入地址本或使用 OpenSocial 等應用程序中的 API 查找已經包含在應用程序中的現有朋友。以上模板需要一張用戶列表,因此讓我們快速查看為頁面創建模型的控制器(參見清單 4)。
#Accounts Module class MainPage(webapp.RequestHandler): def get(self): # get the current user user = users.get_current_user() # is user an admin? admin = users.is_current_user_admin(); # create user account if haven't already account = aggrogator.DB.getAccount(user) if account is None: account = aggrogator.Account(user=user) account.put() # create logout url logout_url = users.create_logout_url(self.request.uri) all_accounts = aggrogator.Account.all() template_values = { 'account': account, 'admin': admin, 'logout_url': logout_url, 'all_accounts': all_accounts, } path = os.path.join(os.path.dirname(__file__), 'accounts.html') self.response.out.write(template.render(path, template_values)) |
該控制器將獲取準備顯示在帳戶頁面中的所有數據。返回到清單 3 中的帳戶頁面,我們將看到在單擊帳戶時調用了 JavaScript。
function subscribe(email) { new Ajax.Request("/accounts/subscribe", { method: "post", parameters: {'email': email}, onSuccess: alert('subscribed to ' + email) }); } |
此 JavaScript 將再次使用 Prototype 庫向伺服器發出 Ajax 請求。調用 URL /accounts/subscribe。要把該 URL 映射到哪裡?創建映射的代碼位於新帳戶模塊的 main 函數中,如下所示。
def main(): app = webapp.WSGIApplication([ ('/accounts/', MainPage), ('/accounts/subscribe', Subscribe), ], debug=True) util.run_wsgi_app(app) if __name__ == '__main__': main() |
正如您在 main 函數中看到的那樣,對 /accounts/subscribe 的調用是由 Subscribe 控制器類處理的。清單 7 中顯示了該類。
class Subscribe(webapp.RequestHandler): def post(self): # get the current user user = users.get_current_user() email = self.request.get('email') aggrogator.DB.create_subscription(user, email) |
該控制器十分簡單。它將獲得當前用戶(訂閱者)和正在添加的訂閱的電子郵件地址。然後將在以前使用的 DB 實用程序類中調用新方法。該類將處理所有與 Bigtable 相關的調用。下面顯示了新 create_subscription 函數。
class DB: @staticmethod def create_subscription(user, email): subscriber = DB.getAccount(user) subscribee = DB.getAccountForEmail(email) subscription = Subscribe.gql("WHERE subscriber = :1 AND subscribee = :2", subscriber, subscribee).get() if subscription is None: Subscribe(subscriber=subscriber, subscribee=subscribee).put() @classmethod def getAccountForEmail(cls, email): user = users.User(email) return cls.getAccount(user) |
該函數首先查找 Account 模型以查找用戶和訂閱電子郵件。對於後者,它將使用新 getAccountForEmail 函數。這將使用 GAE 的用戶的 API 根據電子郵件查找 User 對象,然後查詢 Bigtable 查找帳戶。找到兩個帳戶后,將查看訂閱是否已經存在。如果不存在,則創建一個新訂閱。
當然,有了訂閱之後,需要在主應用程序中使用這些訂閱。我們並不是僅顯示當前用戶的服務,而是需要顯示聚合提要(還有用戶的服務及來自其訂閱的服務)。為此,需要對前幾篇文章中開發的主模塊的 GetUserServices 控制器進行微小更改,如下所示:
class GetUserServices(webapp.RequestHandler): def get(self): user = users.get_current_user() # get the user's services from the cache #userServices = aggrogator.Cache.getUserServices(user) userServices = aggrogator.Aggrogator.get_services(user.email()) stats = memcache.get_stats() self.response.headers['content-type'] = 'application/json' self.response.out.write(simplejson.dumps({'stats': stats, 'userServices': userServices})) |
在這段代碼中,我們只調用了一個新庫類:相應命名的 aggrogator 類,獲得聚合服務而不僅僅是用戶的服務。該庫的代碼如下所示:
class aggrogator: @staticmethod def get_services(username): accounts = [] primary = DB.getAccountForEmail(username) accounts.append(primary) for subscription in primary.subscriptions: accounts.append(subscription.subscribee) services = [] for account in accounts: services.extend(Cache.getUserServices(account.user)) return services |
在這裡,可以再次看到新 Subscribe 模型如何工作。在代碼中,獲得用戶名的帳戶(通過使用以前看到的 getAccountForEmail 函數),然後調用其訂閱屬性。在本例中,只使用此函數從緩存中獲得所有服務。稍後,我們將看到這些用於創建聚合提要的服務。
現在幾乎已經準備好測試新帳戶頁面。必須作出最後一項更改:需要將應用程序配置為向新帳戶模塊發送某些 URL 請求。為此,需要編輯 app.yaml 文件並添加一個新部分。
- url: /accounts/.* script: accounts.py login: required |
這只是文件的新部分。它將把帶有 /accounts/ 的所有請求映射到帳戶模塊中。這部分代碼應當會顯示在先前使用的 catch-all 處理程序(url: /.*)之前,以便獲得優先權。現在可以像以前一樣使用 Eclipse 和 PyDev 並轉到 http://localhost:8080/acounts/ 測試該應用程序。一定要創建多個帳戶,這樣測試會十分有趣。
aggroGator Web 服務
使用社會 Web 服務可以非常輕鬆地創建諸如 aggroGator 之類的有趣應用程序。GAE 允許我們創建此類同樣具有很強可伸縮性的 mashup。因此,圍繞 mashup 創建 API/Web 服務,以便其他人可以使用它創建自己的有趣 mashup 將十分有意義。這實現起來也非常簡單。
對於我們的 Web 服務,首先把它製作成只讀服務。該服務只為用戶提供聚合提要(即在 aggroGator UI 中看到的相同內容)。使用簡單的 REST 樣式的 URL,例如 /api?username=my@email.address。這一次,將由下至上開始。要處理這樣的 URL,需要再次向 app.yaml 文件中添加一個部分。
- url: /api script: main.py |
注意,仍然把 /api 請求發送給主模塊。app.yaml 中為什麼需要一個新映射?我們不需要對 aggroGator API 進行驗證。這是在 app.yaml 中需要新規則的惟一原因。由於利用主模塊,因此需要對它進行修改。
def main(): app = webapp.WSGIApplication([ ('/', MainPage), ('/addService', AddService), ('/getEntries', GetEntries), ('/api', AggroWebService), ('/getUserServices', GetUserServices), ], debug=True) util.run_wsgi_app(app) if __name__ == '__main__': main() |
對該函數所做的全部操作是向映射列表中添加一個條目。將把 /api 映射到 AggroWebService 控制器類中。該類如下所示:
class AggroWebService(webapp.RequestHandler): def get(self): self.response.headers['content-type'] = 'text/xml' username = self.request.get('username') entries = aggrogator.Aggrogator.get_feed(username) str = u"""<?xml version="1.0" encoding="utf-8"?><entries>""" for entry in entries: str += entry.to_xml() str += "</entries>" self.response.out.write(str) |
該服務首先檢索 username 請求參數,然後使用以前看過的 aggroGator 庫,但是使用不同的方法 get_feed 以獲得聚合條目。該庫函數的代碼如下所示:
class Aggrogator: @staticmethod def get_feed(username): services = Aggrogator.get_services(username) entries = [] for svc_tuple in set((svc['service'], svc['username']) for svc in services): entries.extend(Cache.getEntries(*svc_tuple)) entries.sort(key=operator.attrgetter('timestamp'), reverse=True) return entries |
該 library 函數將使用在清單 10 中看到的 get_services 函數檢索聚合服務,然後遍歷服務。代碼將使用一個集合以確保服務是惟一的(即如果用戶已經訂閱了都使用同一項服務的另外兩個用戶)。由於使用了集合,因此必須使用元組(tuple),因為只能使用不可修改的對象。最後,按時間戳降序排列所有條目(首先列出最新的條目)。
返回到清單 14,在擁有條目列表之後,使用某個簡單的字元串連接來創建 XML 文檔。對每個 Entry 實例使用 to_xml() 方法。這是一個新方法,如下所示:
class Entry: def __init__(self, service=None, username=None, title=None, link=None, content=None, timestamp=None): self.service = service self.username = username self.title = title self.link = link self.content = content self.timestamp = timestamp def to_dict(self): return self.__dict__ def to_xml(self): str = """<entry> <service>%s</service> <username>%s</username> <title>%s</title> <link>%s</link> <content><![CDATA[%s]]></content> <timestamp>%s</timestamp> </entry>""" return str % (self.service, self.username, self.title, self.link, self.content, self.timestamp) |
正如您所見,to_xml() 方法僅使用字元串模板和字元串替換來創建 XML 節點。返回到清單 14,在將 XML 文檔創建為字元串后,設置內容類型的響應頭部並將 XML 字元串發送回給請求者。這是我們需要做的全部操作,我們創建了一個可以供其他 mashup 使用的 Web 服務。
結束語
關於 Google App Engine 的 “使用 Eclipse 在 Google App Engine 上創建 mashup” 系列的第 3 部分到此結束。在本文中,添加了訂閱和用於創建訂閱的 UI。修改了使用訂閱的現有應用程序,並且創建了 REST 樣式的 Web 服務以允許其他 mashup 從 aggroGator 進行構建。在此之後我們還可以完成更多事務。可以把註釋添加到 Entry 類中,添加允許用戶向條目添加註釋的 UI。可以提供訂閱視圖和個人視圖。可以擴展 Web 服務,以便允許用戶直接添加到提要中。由於使用了 Google App Engine,並且結合使用了 Eclipse 和 PyDev 等工具,完成所有這些工作變得更加容易。(責任編輯:A6)
[火星人 ] 使用 Eclipse 在 Google App Engine 上創建 mashup,第 3 部分: 使用 RESTful Web 服務已經有604次圍觀