Pyjamas 简介,第 1 部分: 协同使用 GWT 和 Python 的优势

Pyjamas 是一个很酷的工具(框架),用于用 Python 开发 Asynchronous JavaScript and XML (Ajax) 应用程序。可以使用这个全能的工具编写复杂的应用程序,而不需要编写任何 JavaScript 代码。本系列讨论 Pyjamas 的各个方面,第一篇文章介绍 Pyjamas 的背景知识和基本元素。本文通过示例带领您用 Pyjamas 构建一个示例应用程序。

Rick Hightower, 开发人员, Mammatus Inc.

Rick Hightower 是一位有 20 年软件编写经验的软件开发人员,经常为 developerWorks 撰写文章。他是 Mammatus Inc. 的首席技术官,这是一家位于旧金山湾地区的培训和咨询公司。Rick 是畅销书 Java Tools for Extreme Programming 的合著者,还撰写了 Programming the Java API with Python。他是 Java Developer's Journal 的编委会成员,还经常在 DZone 上发表关于 Java 和 Groovy 主题的文章。



2010 年 8 月 23 日

简介

Google 的 Web Toolkit (GWT) 让我们能够完全用 Java™ 代码开发具有 Ajax 功能的 Rich Internet Application (RIA)。可以使用丰富的 Java 工具集(IDE、重构、代码补全、调试器等等)开发出可以部署在所有主流 Web 浏览器中的应用程序。在 GWT 的帮助下,可以编写出在浏览器中运行但是表现与桌面应用程序相似的应用程序。Pyjamas 是 GWT 的一个变体,是用于用 Python 开发 Ajax 应用程序的工具和框架。

Pyjamas 包含一个单独的 Python 到 JavaScript 编译器以及 Ajax 框架和部件集。可以使用这些组件编写复杂的应用程序,而不需要编写任何 JavaScript 代码。

本文讨论 Pyjamas 的背景知识、基本原理、相关工具和优点,演示如何创建一个示例应用程序,这个程序存储基本的联系信息(姓名、电子邮件地址、电话号码)。还可以 下载 这个示例应用程序的代码。

本系列的第 2 部分将解释如何构建定制的 Pyjamas 组件。


背景知识

Python 是一种流行的编程语言,首先出现在 JVM 上 (Jython),后来移植到了 .Net (IronPython)。Python 语法已经可以生成与 C 程序兼容的机器代码 (Cython)。在 Google 宣布采用 Java 语言之后,Python 成为第一批可以转换为 JavaScript 以跨浏览器的方式运行的语言之一。

强大的 XUL

在 2009 年,Pyjamas-Desktop(现在是 Pyjamas 的组成部分)还转换为使用 XUL。XUL 和 Firefox 的关系或多或少相当于 WebKit 和 Safari 的关系。可以在 XUL 上运行 Pyjamas。据说,在 Hulahop 项目(来自 OLPC Sugar 团队)和 python-xpcom 开发人员的帮助下,把 Pyjamas 移植到 XUL 只花了两天时间。

在不久之前,用 Ajax 编写整个应用程序的希望看起来还很渺茫。但是现在有了 GWT,我们可以完全用 Java 代码开发具有 Ajax 功能的 RIA。GWT 让我们能够编写出在浏览器中运行但是表现与桌面应用程序相似的应用程序。

与之相反,Adobe AIR 和 Silverlight 让 Web 应用程序可以在桌面上运行。Android、Adobe AIR、Google Chrome、Safari 和 iPhone 都使用 WebKit 进行显示。GWT 的问题是,不允许编写作为桌面应用程序运行的应用程序(尽管用于显示的 GWT 开发工具集基于 WebKit)。

Pyjamas 有一个与 GWT 相似的 Python 到 JavaScript 编译器,还包含一套 Ajax 部件,它们的 API 与对应的 GWT 部件相同。(实际上可以参考 GWT 文档开发 Pyjamas 应用程序。)Python 的语法非常简洁、强大;例如 GWT 1.2 有 80,000 行代码,而 Pyjamas 完成相同的任务只用了 8,000 行代码。


Pyjamas 概述

XUL 和 WebKit Python 绑定的问题

MSHTML 似乎是最好的,而且 WebKit 和 XUL 的底层 Python 绑定是变化的。如果 WebKit 团队不将 Python 绑定移植到 WebKit GTK,就会造成数不清的麻烦。

有时候,WebKit 和 xulrunner Python 绑定受到破坏,或至少被忽视。

请记住,Pyjamas-Desktop 并非只与 WebKit 捆绑在一起。Pyjamas 为 Python 开发人员提供 WebKit、XUL 和 MSHTML。因此,Pyjamas-Desktop 可以使用这三种浏览器引擎中的任意一种。由于这个原因,Pyjamas 成了既跨浏览器又跨平台的 GUI 部件集。

WebKit、XUL 和同类技术把现代技术带入了桌面应用程序。Pyjamas 为 Python 开发人员提供 WebKit。由于可以使用 Webkit,Pyjamas 成了既跨浏览器又跨平台的 GUI 部件集。可以开发出在运行 WebKit 和 XUL 的任何地方运行的部件。在能够运行 GWT 应用程序的任何地方,基于 Pyjamas API 的应用程序都可以运行。另外,Pyjamas 允许编写在 WebKit 和 XUL 上构建的桌面应用程序。这比在 Qt 或 GTK 上构建应用程序更好,因为 WebKit 支持 CSS,可以在许多其他地方可靠地显示(iPhone、Safari、Android 等等)。但是,在 XUL 和 WebKit 的 Python 绑定方面有点儿问题(见边栏)。

与 GWT 一样,Pyjamas 是一个 GUI 组件框架。如果您使用过 Swing 或 GWT,应该觉得熟悉 Pyjamas 开发。与大多数 GUI 框架一样,Pyjamas 是事件驱动的。

用 Pyjamas 创建容器,然后在容器中添加部件。部件可以是标签、文本框、按钮等等。按钮等部件有事件处理器,可以监听来自按钮的单击事件。

用 Pyjamas 进行开发很容易,因为可以使用平时使用的 Python 调试工具。这些工具包括单元测试、打印语句和 Python 调试器(命令行调试器 pdb)。甚至可以使用 Eclipse 的 Python 支持进行调试。请记住,可以编写作为原生 Python 应用程序运行的 Pyjamas 应用程序。不一定要把 Pyjamas 应用程序转换为 JavaScript。可以像使用任何其他 Python GUI 工具集一样使用 Pyjamas。

本文中示例应用程序的 GUI 的第一版是用从命令行运行的 Python 开发的。它最初甚至没有部署到 Web 上,而是作为桌面应用程序运行。这对于开发 RIA 应用程序很有好处,因为能够方便地调试程序。

当准备好把应用程序部署到 Web 上时,需要注意程序包含的库。常常从在浏览器中运行的 Pyjamas 应用程序使用 JavaScript Object Notation (JSON)-RPC 服务。


先决条件

要想构建本文中的示例应用程序,需要下载并安装 Pyjamas。这个任务并不很简单。我曾经尝试在 Ubuntu 上安装 Pyjamas,但是失败了,只好放弃,改为在 Debian 上安装它。(据说 Pyjamas 也可以在 Windows® 上顺利地运行。)在 Debian 上安装的版本没什么问题。安装过程可能会有变动,所以您应该按照 Pyjamas 站点上针对您的环境的最新说明操作(见 参考资料)。

为了构建服务层,使用了 MySQL、Apache、mod_python 和 Python JSON-RPC。


构建示例应用程序

示例联系人管理应用程序存储基本的联系信息,比如姓名、电子邮件地址和电话号码。首先创建一个简单的 Create, Read, Update, and Delete (CRUD) 应用程序,然后添加真正的存储。可以在一个简单的 Python 脚本中实现整个程序,使用内存中的 “数据库”。这个示例使用一个服务层,然后把这个内存中的服务层版本替换为由 JSON 支持的服务层版本,这个版本使用 MySQL 把联系人信息存储在关系数据库中。

分而治之

我喜欢让整个 GUI 与一个模拟层通信,这样可以把 GUI 开发与持久化和业务逻辑层分隔开。按照这种方式,我可以专心开发 GUI 逻辑,而不需要为调试远程 RPC 等问题操心。

要想了解如何编写模拟服务,必须了解运行时应用程序的运行方式。程序异步地调用 JSON 服务。当把 Pyjamas 应用程序编译为 RIA 应用程序(HTML 和 JavaScript 代码)时,Ajax 调用会异步地返回结果。因此,在构建模拟服务时,要模拟 Ajax 库异步地回调 GUI。清单 1 说明 ContactService 通过调用 callback 方法回调 GUI。这模拟以后要添加的 JSON 异步行为。

清单 1. Contact Service
class Contact:
    def __init__(self, name="", email="", phone=""):
        self.name = name
        self.email = email
        self.phone = phone

class ContactService:
    def __init__(self, callback):
        self.callback = callback
        self.contacts = []

    def addContact(self, contact):
        self.contacts.append(contact)
        self.callback.service_eventAddContactSuccessful()
    
    def updateContact(self, contact):
        self.callback.service_eventUpdateContactSuccessful()

    def removeContact(self, contact):
        self.contacts.remove(contact)
        self.callback.service_eventRemoveContactSuccessful()
        
    def listContacts(self):
        self.callback.service_eventListRetrievedFromService(self.contacts)

Contact 类代表一个联系人(姓名、电子邮件地址、电话号码)。ContactService 只有内存中的联系人列表(没有存储到磁盘)。这个简单的类让我们可以开发 GUI;在开发显示逻辑之后,只需经过简单的修改,就可以用真正的 JSON 服务测试 GUI。

ContactService 使用名称以 service_eventXXX 开头的方法将服务事件通知给 ContactListGUI(在清单 2 中定义)。

ContactListGUI 相当简单,只有 125 行代码,它管理 9 个 GUI 部件。它还与 ContactService 协作管理 CRUD 列表,见清单 2。

清单 2. ContactListGUI
import pyjd # this get stripped out for JavaScript translation
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Button import Button
from pyjamas.ui.Label import Label
from pyjamas import Window

from  pyjamas.ui.Grid import Grid
from  pyjamas.ui.Hyperlink import Hyperlink
from  pyjamas.ui.TextBox import TextBox

# Constants
CONTACT_LISTING_ROOT_PANEL = "contactListing"
CONTACT_FORM_ROOT_PANEL = "contactForm"
CONTACT_STATUS_ROOT_PANEL = "contactStatus"
CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar"
EDIT_LINK = 3
REMOVE_LINK = 4

#Service code removed

class ContactListGUI:

    def __init__(self):
        self.contactService = ContactService(self)
        self.currentContact = Contact("Rick", "rhightower@gmail.com", "555-555-5555")
        self.addButton = Button("Add contact", self.gui_eventAddButtonClicked)
        self.addNewButton = Button("Add new contact", self.gui_eventAddNewButtonClicked)
        self.updateButton = Button("Update contact", self.gui_eventUpdateButtonClicked)

        self.nameField = TextBox()
        self.emailField = TextBox()
        self.phoneField = TextBox()
        self.status = Label()
        self.contactGrid = Grid(2,5)
        self.contactGrid.addTableListener(self)

        self.buildForm()
        self.placeWidgets()
        self.contactService.listContacts()	

    
    def onCellClicked(self, sender, row, cell):
        print "sender=%s row=%s cell=%s" % (sender, row, cell)
        self.gui_eventContactGridClicked(row, cell)

    def onClick(self, sender):
        if sender == self.addButton:
            self.gui_eventAddButtonClicked()
        elif sender == self.addNewButton:
            self.gui_eventAddNewButtonClicked()
        elif sender == self.updateButton:
            self.gui_eventUpdateButtonClicked()
                
    def buildForm(self):
        formGrid = Grid(4,3)
        formGrid.setVisible(False)
        
        formGrid.setWidget(0, 0, Label("Name"))
        formGrid.setWidget(0, 1, self.nameField);

        formGrid.setWidget(1, 0, Label("email"))
        formGrid.setWidget(1, 1, self.emailField)
        
        formGrid.setWidget(2, 0, Label("phone"))
        formGrid.setWidget(2, 1, self.phoneField)
        
        formGrid.setWidget(3, 0, self.updateButton)
        formGrid.setWidget(3, 1, self.addButton)

        self.formGrid = formGrid
        
    def placeWidgets(self):
        RootPanel(CONTACT_LISTING_ROOT_PANEL).add(self.contactGrid)
        RootPanel(CONTACT_FORM_ROOT_PANEL).add(self.formGrid)
        RootPanel(CONTACT_STATUS_ROOT_PANEL).add(self.status)
        RootPanel(CONTACT_TOOL_BAR_ROOT_PANEL).add(self.addNewButton)

    def loadForm(self, contact):
        self.formGrid.setVisible(True)
        self.currentContact = contact
        self.emailField.setText(contact.email)
        self.phoneField.setText(contact.phone)
        self.nameField.setText(contact.name)
    
    def copyFieldDateToContact(self):
        self.currentContact.email = self.emailField.getText()
        self.currentContact.name = self.nameField.getText()
        self.currentContact.phone = self.phoneField.getText()

ContactListGUI init 方法通过调用 buildForm 方法创建一个新的表单,并在其中添加用于编辑联系人数据的字段。然后,init 方法调用 placeWidgets 方法,这个方法把 contactGridformGridstatusaddNewButton 部件放在驻留这个 GUI 应用程序的 HTML 页面中定义的位置,见清单 3。

图 1 显示联系人管理应用程序中使用的部件的概况。

图 1. 联系人管理 GUI 中的部件
屏幕显示联系人姓名、电话号码、电子邮件和用于添加或更新联系人的按钮
清单 3. ContactListGUI GUI 事件处理器
<html>
    <head>
      <meta name="pygwt:module" content="Contacts">
      <link rel='stylesheet' href='Contacts.css'>
      <title>Contacts</title>
    </head>
    <body bgcolor="white">

      <script language="javascript" src="bootstrap.js"></script>

      <h1>Contact List Example</h1>

      <table align="center">
      <tr>
        <td id="contactStatus"></td> 
      </tr>
      <tr>
        <td id="contactToolBar"></td>
      </tr>
      <tr>
        <td id="contactForm"></td>
      </tr>
      <tr>
        <td id="contactListing"></td>
      </tr>
      </table>
    </body>
</html>

常量(比如 CONTACT_LISTING_ROOT_PANEL="contactListing")对应于 HTML 页面中定义的元素的 ID(比如 id="contactListing")。这让页面设计者可以控制应用程序部件的布局。

基本的应用程序现在构建好了。下一节讨论几个常见的使用场景。


在装载页面时显示列表

当首次装载示例应用程序的页面时,它调用 ContactListEntryPoint__init__ 方法。__init__ 方法调用 ContactServiceDelegatelistContacts 方法,该方法又异步地调用服务的 listContact 方法。模拟的 ContactServicelistContact 方法调用服务事件处理器方法 service_eventListRetrievedFromService,如清单 4 所示。

清单 4. ContactListGUI:调用 listContact 事件处理器
class ContactListGUI:
    …
    def service_eventListRetrievedFromService(self, results):
        self.status.setText("Retrieved contact list")
        self.contacts = results;
        self.contactGrid.clear();
        self.contactGrid.resizeRows(len(self.contacts))
        row = 0
        
        for contact in results:
            self.contactGrid.setWidget(row, 0, Label(contact.name))
            self.contactGrid.setWidget(row, 1, Label (contact.phone))
            self.contactGrid.setWidget(row, 2, Label (contact.email))
            self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None))
            self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None))
            row += 1

service_eventListRetrievedFromService 事件处理器方法存储服务器发送的联系人列表。然后:

  • 清空显示联系人列表的 contactGrid
  • 调整行的数量,与服务器返回的联系人列表的规模匹配。
  • 循环处理联系人列表,把每个联系人的姓名、电话号码和电子邮件数据放进每行的前三列。
  • 为每个联系人提供 Edit 链接和 Remove 链接,让用户可以轻松地删除和编辑联系人。

编辑现有的联系人

当用户单击联系人列表中的 Edit 链接时,调用 gui_eventContactGridClicked,见清单 5。

清单 5. ContactListGUI 的 gui_eventContactGridClicked 事件处理器方法
class ContactListGUI:

    …
    def gui_eventContactGridClicked(self, row, col):
         contact = self.contacts[row]
         self.status.setText("Name was " + contact.name + " clicked ")
         if col==EDIT_LINK:
             self.addNewButton.setVisible(False)
             self.updateButton.setVisible(True)
             self.addButton.setVisible(False)
             self.loadForm(contact)
         elif (col==REMOVE_LINK):
             self.contactService.removeContact(contact)

    …
    def loadForm(self, contact):
        self.formGrid.setVisible(True)
        self.currentContact = contact
        self.emailField.setText(contact.email)
        self.phoneField.setText(contact.phone)
        self.nameField.setText(contact.name)

gui_eventContactGridClicked 方法检查用户单击的是哪一列,从而判断单击的是 Edit 链接还是 Remove 链接。然后,它隐藏 addNewButtonaddButton,让 updateButton 可见。updateButton 显示在 formGrid 中,让用户能够把更新信息发送回 ContactService。然后,gui_eventContactGridClicked 调用 loadForm(见 清单 5),它:

  • formGrid 设置为可见。
  • 设置正在编辑的联系人。
  • 把联系人属性复制到 emailFieldphoneFieldnameField 部件中。

当用户单击 Update 按钮时,调用 gui_eventUpdateButtonClicked 事件处理器方法,见清单 6。这个方法:

  • addNewButton 可见,让用户可以添加新的联系人。
  • 隐藏 formGrid
  • 调用 copyFieldDateToContact,从而把 emailFieldphoneFieldnameField 部件中的文本复制回 currentContact 的属性。
  • 调用 ContactServiceDelegate 的 updateContact 方法,把更新的联系人信息传递回服务。
清单 6. ContactListGUI 的 gui_eventUpdateButtonClicked 事件处理器方法
class ContactListGUI:

    …

    def gui_eventUpdateButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.updateContact(self.currentContact)

    def copyFieldDateToContact(self):
        self.currentContact.email = self.emailField.getText()
        self.currentContact.name = self.nameField.getText()
        self.currentContact.phone = self.phoneField.getText()

上面两个场景说明了这个应用程序的工作方式,以及它如何利用 App Engine for Java 提供的基础设施。ContactListGUI 的其他 GUI 事件处理器见清单 7,其他服务回调处理器见 清单 8

清单 7. ContactListGUI 的 gui_eventUpdateButtonClicked 事件处理器方法
class ContactListGUI:

    …
    def gui_eventContactGridClicked(self, row, col):
         contact = self.contacts[row]
         self.status.setText("Name was " + contact.name + " clicked ")
         if col==EDIT_LINK:
             self.addNewButton.setVisible(False)
             self.updateButton.setVisible(True)
             self.addButton.setVisible(False)
             self.loadForm(contact)
         elif (col==REMOVE_LINK):
             self.contactService.removeContact(contact)


    def gui_eventAddButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.addContact(self.currentContact)

    def gui_eventUpdateButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.updateContact(self.currentContact)


    def gui_eventAddNewButtonClicked(self, sender):
        self.addNewButton.setVisible(False)
        self.updateButton.setVisible(False)
        self.addButton.setVisible(True)
        self.loadForm(Contact())
清单 8. ContactListGUI 服务回调方法
class ContactListGUI:
    …
    def service_eventListRetrievedFromService(self, results):
        self.status.setText("Retrieved contact list")
        self.contacts = results;
        self.contactGrid.clear();
        self.contactGrid.resizeRows(len(self.contacts))
        row = 0
        
        for contact in results:
            self.contactGrid.setWidget(row, 0, Label(contact.name))
            self.contactGrid.setWidget(row, 1, Label (contact.phone))
            self.contactGrid.setWidget(row, 2, Label (contact.email))
            self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None))
            self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None))
            row += 1

    def service_eventAddContactSuccessful(self):
        self.status.setText("Contact was successfully added")
        self.contactService.listContacts()

    def service_eventUpdateContactSuccessful(self):
        self.status.setText("Contact was successfully updated")
        self.contactService.listContacts()

    def service_eventRemoveContactSuccessful(self):
        self.status.setText("Contact was removed")
        self.contactService.listContacts()

编译示例

可以编译这个示例应用程序并在任何现代浏览器中运行它。但是,想调试在浏览器中运行的 RIA 应用程序并不容易。幸运的是,通过使用 Pyjamas-Desktop,可以让整个应用程序作为原生的 Python 应用程序运行,见清单 9。

清单 9. 运行 Pyjamas-Desktop
import pyjd # this get stripped out for JavaScript translation
...
if __name__ == '__main__':
    pyjd.setup("public/Contacts.html")
    contacts = ContactListGUI()
pyjd.run()

清单 9 中的代码实例化一个 Python 桌面应用程序,然后通过调用 run 方法启动桌面。在将这个应用程序作为桌面应用程序运行时,可以使用 pdb 或支持可视化调试的 Python IDE 调试它。

我把 Pyjamas 安装在主目录下的 tools 目录中。在使用 Python 调试器时,一定要把 Pyjamas 和 Pyjamas-Desktop 库添加到路径中,见清单 10。

清单 10. 把 Pyjamas 添加到 PYTHONPATH 中
export PYTHONPATH=/home/rick/tools/pyjamas:/home/rick/tools/pyjamas/library

编写完应用程序之后,可以通过运行 pyjsbuild 把应用程序编译为 HTML、JavaScript 和 JSON-RPC。清单 11 给出一个运行 pyjsbuild 的示例脚本。

清单 11. build.sh
#!/bin/sh

options="$*"
#if [ -z $options ] ; then options="-O";fi
~/tools/pyjamas/bin/pyjsbuild --print-statements $options Contacts.py

编译应用程序之后,只需用一个 Web 服务器驻留 /output 文件夹。这个示例使用新安装的 Debian,所以要用 apt-get 安装 apache2 和 mod_python,见清单 12。

清单 12. 安装 apache2 和 mod_python
$sudo apt-get install apache2 libapache2-mod-python

联系人列表的下一个版本将使用 mod_python。在 /home/rick/tools/pyjamas/examples/contact1 下创建示例应用程序。为了让 Apache 驻留它,在 Apache httpd.conf 文件中添加以下代码(在 Debian 上,这个文件在 /etc/apache2 下面)。

清单 13. /etc/apache2/httpd.conf
Alias /pj "/home/rick/tools/pyjamas" 
<Directory "/home/rick/tools/pyjamas">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride None
    Order deny,allow
    allow from all
</Directory>

添加 JSON-RPC 支持

关于 mod_python 和 sqllite3 的提示

初看上去,它只是本文中的小示例使用的一个小服务,不需要单元测试或日志记录。大错特错!我最初尝试使用 sqllite3(因为它是 Python 附带的),但是遇到了一些锁问题,这促使我改用 MySQL。关于 mod_python、JSON-RPC 和 sqllite3 的一些教训如下:

  • 当用单元测试在本地运行、以及后来在 apache 下作为 su 运行时,sqlite3 以奇怪的方式锁住文件。
  • 在 mod_python 中很难进行调试,因为无法得到错误消息。日志记录和单元测试是必需的。

我改用 MySQL,使用日志记录和健壮的异常处理,还编写了单元测试。如果没有单元测试和日志记录,这个示例很可能无法完成。如果打算一直用 sudo 运行单元测试,可能可以使用 sqlite3。或者,您能够找到避免锁问题的其他方法。

实现 GUI 逻辑之后,就该开始编写 JSON-RPC 服务了(用 Python 实现)。JSON-RPC 是一个标准;可以用任何编程语言实现服务器端。按照这种方式,Pyjamas 前端应用程序可以与具有 JSON-RPC 后端 Web 服务的现有项目协作。JSON 是一种数据交换格式。它使用两个结构:

  • 名/值对的集合(Python 中的词典、Java 代码中的散列表或 Perl 的关联数组)
  • 数组

JSON-RPC 是一种远程过程调用协议,它使用 JSON 对参数和返回值进行编码和编组。JSON-RPC 项目为 Python 提供了绑定。Twisted、Django 和其他许多 Python 框架也支持 JSON-RPC。安装 JSON-RPC 的一种简便方法见清单 14。

清单 14. 安装 JSON-RPC
$ svn checkout 
   http://svn.json-rpc.org/trunk/python-jsonrpc

$ cd python-jsonrpc
$ python setup.py install

为了编写 JSON-RPC 服务,需要给方法调用加上 @ServiceMethod 注解,然后公开一个名为 service 的模块变量,它指向要用 JSON-RPC 公开的实例。清单 15 给出一个示例。

清单 15. ContactService:联系人列表的 JSON-RPC 服务
import logging

logging.basicConfig(filename="/tmp/contactjson.log",
                    level=logging.DEBUG)


logging.debug("Loading contact service")

from jsonrpc import ServiceMethod

use_mysql=True

if use_mysql:
    import MySQLdb as db_api
    logging.debug("Using mysql")
else:
    import sqlite3 as db_api
    logging.debug("Using sqllite3")


db_url = "/tmp/contacts"


class ContactService:

    @ServiceMethod
    def test(self):
        logging.info("Test called")
        return "test"

    def connection(self):
        if use_mysql:
            connection =  db_api.connect(passwd="mypassword", db="contactdb", user="root")
        else:
            connection =  db_api.connect(db_url)
        return connection

    def run_update(self, func):
        
        connection = self.connection()
        cursor = connection.cursor()
        try:
            func(cursor)
            cursor.close()
            connection.commit()
        except Exception, e:
            connection.rollback()
            logging.exception("problem handling update")
            raise e
        finally:
            connection.close()

    def run_query(self, func):
        connection = self.connection()
        cursor = connection.cursor()
        lst = None
        try:
            func(cursor)
            lst = cursor.fetchall()
            cursor.close()
        except Exception, e:
            logging.exception("problem handling query")
            raise e
        finally:
            connection.close()
        return lst

    @ServiceMethod
    def addContact(self, contact):
        logging.debug("Add contact called %s", `contact`)
        def ac(cursor):
            if use_mysql:
                cursor.execute(""" 
                  insert into contact 
                          (name, phone, email) 
                  values (%(name)s, %(phone)s, %(email)s) 
                  """, contact)
            else:
                cursor.execute(""" 
                  insert into contact 
                          (id, name, phone, email) 
                  values (NULL, :name, :phone, :email) 
                  """, contact)
        self.run_update(ac)


    @ServiceMethod
    def updateContact(self, contact):
       logging.debug("Update contact called %s", `contact`)
       def uc(cursor):
           if use_mysql:
               cursor.execute(""" 
                  update contact 
                          set name = %(name)s, email = %(email)s, phone = %(phone)s
                  where id=%(id)s;
                  """, contact)
           else:
               cursor.execute(""" 
                  update contact 
                          set name = :name, email = :email, phone = :phone
                  where id=:id;
                  """, contact)

       self.run_update(uc)


    @ServiceMethod
    def removeContact(self, contact):
       logging.debug("Remove contact called %s", `contact`)
       def uc(cursor):
           if use_mysql:
               cursor.execute("delete from contact where id=%(id)s;", contact)
           else:
               cursor.execute("delete from contact where id=:id;", contact)
       self.run_update(uc)

        
    @ServiceMethod
    def listContacts(self):
        logging.debug("list contact called")
        def lc(cursor):
            cursor.execute("select name, phone, email, id from contact")
        lst = self.run_query(lc)
        def toMap(x):
            return {"name":x[0],"phone": x[1], "email":x[2], "id":x[3]}
        return map(toMap, lst)


service = ContactService()

#If you can't get mod_python working 
# you can use CGI with the following line.
#handleCGI(service)
# You have to import handleCGI from jsonrpc

清单 15 可以使用 MySQL(它很容易安装)或 sqlite3(它是 Python 附带的)。要想使用 sqlite3,应该把 use_mysql 设置为 False。

清单 16 给出用于测试这个服务的单元测试,这对于开发示例应用程序是必需的。这个清单给出单元测试使用的实用程序类。

清单 16. TestContactService
import unittest
from contacts import ContactService
from dbscript import *

class TestContactService(unittest.TestCase):

    def setUp(self):
        self.cs = ContactService()
        try:
            drop_table()
        except:
            print "unable to drop contact table"
        try:
            create_table()        
        except:
            print "unable to create contact table"

    def testAdd(self):
        clear_table()
        cs = self.cs
        cs.addContact({"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com"
                      })
        list = cs.listContacts()
        print list
        found = False
        for cdict in list:
            if cdict["name"]=="Richard": found = True
        self.assertTrue(found)

    def testUpdate(self):
        cs = self.cs
        insert_test_data()
        cs.updateContact(
            {"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com",
                       "id":1})

        list = cs.listContacts()
        print list
        found = 0
        for cdict in list:
            if cdict["name"]=="Richard": found +=1
        self.assertTrue(found==1)


    def testRemove(self):
        cs = self.cs
        insert_test_data()
        cs.removeContact(
            {"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com",
                       "id":1})

        list = cs.listContacts()
        print list
        found = 0
        for cdict in list:
            if cdict["name"]=="Richard": found +=1
        self.assertTrue(found==0)


        
        
if __name__ == '__main__':
    unittest.main()

清单 17 中的 dbscript.py 可以构建 MySQLdb 联系人表或 sqlite3 联系人表。

清单 17. 用于创建、删除和填充联系人表的 dbscript
use_mysql = True

if use_mysql:
    import MySQLdb as db_api
else:
    import sqlite3 as db_api

db_url = "/tmp/contacts"


create_table_sql = """ 
create table contact (
     id INTEGER %s PRIMARY KEY, 
     name VARCHAR(50), 
     phone VARCHAR(50), 
     email VARCHAR(50));
"""

if use_mysql:
    create_table_sql = create_table_sql % ("AUTO_INCREMENT",)
else:
    create_table_sql = create_table_sql % ("",)


def run_script(func):
    if use_mysql:
        connection =  db_api.connect(passwd="mypassword", db="contactdb", user="root")
    else:
        connection =  db_api.connect(db_url)

    cursor = connection.cursor()
    try:
        func(cursor)
        connection.commit()
        cursor.close()
    finally:
        connection.close()
    
def create_table():
    def ct(cursor):
        cursor.execute(create_table_sql)

    run_script(ct)

def drop_table():
    def dt(cursor):
        cursor.execute("drop table contact;")
    run_script(dt)

def clear_table():
    def dt(cursor):
        cursor.execute("delete from contact;")
    run_script(dt)

def insert_test_data():
    def itd(cursor):
        if use_mysql:
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
               'Bob', '5', 'b@b.com');") 
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
                'Rick', '5', 'b@b.com');")
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
                'Sam', '5', 'b@b.com');")
        else:
            cursor.executescript(""" 
    insert into contact (id, name, phone, email) values (NULL, "Bob", "5", "b@b.com"); 
    insert into contact (id, name, phone, email) values (NULL, "Rick", "5", "b@b.com"); 
    insert into contact (id, name, phone, email) values (NULL, "Sam", "5", "b@b.com"); 
           """)

    run_script(itd)

dbscript 创建和删除联系人表,在其中填充单元测试使用的测试数据。完成 JSON-RPC 服务之后,可以在 httpd.conf 文件中添加清单 18 所示的代码,让 Apache HTTPD 提供它以供使用。

清单 18. /etc/apache2/httpd.conf
Alias /services "/home/rick/services" 

<Location /services/>
    AddHandler mod_python .py
    PythonHandler jsonrpc
</Location>

请记住,在修改服务之后,需要重新启动它,见清单 19。

清单 19. 重新启动 Apache2 对 mod_python 应用修改
$sudo /etc/init.d/apache2 restart

在 Pyjamas 中运行 JSON-RPC 代理时,会遇到讨厌的回归错误。为了帮助调试错误,我使用单独的 JSON-RPC 客户机库,见清单 20。

清单 20. Python JSON-RPC 客户机
from jsonrpc import ServiceProxy, JSONRPCException
 
cs = ServiceProxy("http://localhost/services/contacts.py")
 
if cs.test()=="test":
    print "connected"
 
try:
    cs.addContact(
        {"name":"Larry Wall", 
         "phone":"5551212", 
         "email":"rick@rick.com"})
 
except Exception, e:
    print e.error
    print `e.error`

这个步骤对于测试和调试很重要。Pyjamas 开发还不太成熟,所以最好以其他方法测试来自其他来源的 JSON-RPC。

这个示例把 ContactService 改为使用 JSONProxy。JSONProxy 是 Pyjamas 的 JSON-RPC 客户机支持。可以为刚编写的服务创建一个代理对象,见清单 21 中的 ContactsJSONProxy。对象从 JSON 服务异步返回。因此,在调用 JSON 代理时,传递一个 ContactService 实例,它通过实现 onRemoteResponse 从服务异步地接收响应。

清单 21. 使用 JSON-RPC 服务的联系人列表
from pyjamas.JSONService import JSONProxy
...
class Contact:
    def __init__(self, name="", email="", phone="", id=None):
        self.name = name
        self.email = email
        self.phone = phone
        self.id = id
    def to_dict(self):
        return {"name":self.name, "email":self.email, 
                "phone":self.phone, "id":self.id}


class ContactsJSONProxy(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "/services/contacts.py", 
                           ["addContact", "removeContact", 
                            "updateContact", "listContacts","test"])

    

class ContactService:
    def __init__(self, callback):
        self.callback = callback
        self.proxy = ContactsJSONProxy()

    def test(self):
        self.proxy.test(self)

    def addContact(self, contact):
        self.callback.showStatus("Add contact called")
        self.proxy.addContact(contact.to_dict(), self)

    def updateContact(self, contact):
        self.callback.showStatus("Update contact was called")
        self.proxy.updateContact(contact.to_dict(), self)


    def removeContact(self, contact):
        self.callback.showStatus("Remove contact was called")
        self.proxy.removeContact(contact.to_dict(), self)

        
    def listContacts(self):
        self.proxy.listContacts(self)


    def onRemoteResponse(self, response, request_info):        
        if request_info.method == "addContact":
            self.callback.service_eventAddContactSuccessful()
        elif request_info.method == "updateContact":
            self.callback.service_eventUpdateContactSuccessful()
        elif request_info.method == "listContacts":
            def toContact(x):
                return Contact(x["name"], x["email"], x["phone"], x["id"])  
            contacts = map(toContact, response)
            self.callback.service_eventListRetrievedFromService(contacts)
        elif request_info.method == "removeContact":
            self.callback.service_eventRemoveContactSuccessful()
        else:
            self.callback.showStatus(""" REQ METHOD = %s RESP %s """ %
                 (request_info.method,response)) 

    def onRemoteError(self, code, errobj, request_info):
        message = errobj['message']
        if code != 0:
            self.callback.showStatus("HTTP error %d: %s" % (code, message))
        else:
            json_code = errobj['code']
            self.callback.showStatus("JSONRPC Error %s: %s" % (json_code, message))

客户机代码的其余部分与以前很相似,只有一些修饰性的修改。令人吃惊的是,使用真正的远程 RPC 服务的客户机与使用单独的服务版本的客户机差异并不大。这让我们可以快速地开发 GUI,然后插入单独开发和调试的 JSON-RPC 服务。


结束语

在 “Pyjamas 简介” 系列的第 1 部分中,您了解了 Pyjamas 的历史和前景。还学习了如何使用 Pyjamas、mod_python 和 Python JSON-RPC 创建基于 Pyjamas 的应用程序。请关注本系列的第 2 部分,第 2 部分将解释如何构建定制的 Pyjamas 组件。


致谢

特别感谢 Luke Kenneth Casson Leighton 审阅本文并提供宝贵的意见。他还帮助运行示例,对如何调试应用程序提出了建议。


下载

描述名字大小
联系人管理应用程序的示例代码pyjamas.zip2425KB

参考资料

学习

获得产品和技术

  • 下载 IBM 产品评估版 或者在 IBM SOA Sandbox 中在线试用这些来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=512763
ArticleTitle=Pyjamas 简介,第 1 部分: 协同使用 GWT 和 Python 的优势
publish-date=08232010