使用社会网络可以更轻松地获取并聚合数据,从而创建富有革新精神的新 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。
清单 1. 带有 user 列表的 Account 模型
class Account(db.Model): user = db.UserProperty(required=True) subscriptions = db.ListProperty(Account) |
这种方法有一些优点。检索帐户时,可以获得该帐户订阅的所有其他帐户。这是使用诸如 GAE 的 Bigtable 之类的非关系数据库的常见策略:把所有相关数据保存在一起并且无需担心标准化之类的事务。但是,这种方法有一个缺点。如果需要显示特定用户订阅了哪些人的提要该怎么办?这样做的惟一方法是检索所有 Account 模型,查看所有订阅,并查看给定用户是否位于列表中。另外,可以在每个 Account 模型中保存两张列表 — 一张用于 subscriptions,一张用于 subscribers。我们不会采取这种方法,而是使用更传统的多对多模型,如清单 2 所示。
清单 2. Subscribe 模型
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)。
清单 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)。
清单 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。
清单 5. 订阅 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 函数中,如下所示。
清单 6. 帐户模块的 URL 映射
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 中显示了该类。
清单 7. Subscribe 控制器类
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 函数。
清单 8. create_subscription 的 DB 函数
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 控制器进行微小更改,如下所示:
清单 9. 修改后的 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 类,获得聚合服务而不仅仅是用户的服务。该库的代码如下所示:
清单 10. 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 文件并添加一个新部分。
清单 11. 添加到 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 文件中添加一个部分。
清单 12. 添加到 app.yaml 中的内容
- url: /api script: main.py |
注意,仍然把 /api 请求发送给主模块。app.yaml 中为什么需要一个新映射?我们不需要对 aggroGator API 进行验证。这是在 app.yaml 中需要新规则的惟一原因。由于利用主模块,因此需要对它进行修改。
清单 13. 主模块的新规则
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 控制器类中。该类如下所示:
清单 14. 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 以获得聚合条目。该库函数的代码如下所示:
清单 15. 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() 方法。这是一个新方法,如下所示:
清单 16. Entry 类
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)