内容


使用 Web services 和 Ajax 实现数据自动录入

部署 Web 2.0 技术以节省时间并确保数据的准确性

Comments

编程思想的一些背景知识

美国邮政管理局 (USPS) 提供了多个 Web services (请参阅 USPS Web 工具介绍)。这些 Web services 之一接受邮政区码并返回相应的城市和州的名称。在本文的示例应用程序中,您将使用此 CityStateLookupRequest 来省去用户的一些输入操作。此功能还为您的数据库提供了更好的地址数据,因为它减少了键入错误的机会。

先决条件和假设情况

构思和创建 Ruby on Rails 的 David Heinemeier Hansson 确实是一位智多星!在 RoR 中,他实现了许多好的思想,这些思想使开发 Web 应用程序更加容易,就像我的一个朋友说的那样“它使编程变得更加有趣了!”我认为,其他框架和编程范例将毫无疑问地支持这些思想。不过,本教程并不是介绍如何创建 RoR 应用程序。(有关优秀教程和参考信息的链接,请参阅本文最后的参考资料部分。)

本文的假设情况是您已经创建了一个 RoR 应用程序,该程序有一个地址的 HTML 输入表单(例如 590 Madison Ave, New York, NY 10022)。此 Rails 应用程序还有一个名为 address 的模型和一个相应的数据库表。而且,我们还假设您:

  • 了解 Web 应用程序开发的一些基本设计原则。
  • 已经创建了一个 RoR 应用程序。
  • 了解 RoR 应用程序的以下基本部分:ActiveSupport、ActiveRecord、ActionView、ActionController 和 Migrations 等。
  • 有一个配置用于 RoR 应用程序的数据库(如 IBM® DB2® 或 MySQL)。
  • 通过预见用户的需求与用户联系并知道节省他们时间的重要性。
表 1. 假设您有一个具有这些对象的 RoR 应用程序
Ruby on Rails 文件目录描述
edit.rhtml../app/views/addressadmin编辑地址的视图
_form.rhtml../app/views/addressadminedit.rhtml 使用的片段
addressadmin_controller.rb../app/controllersHTML 输入表单调用的控制器
address.rb../app/modelsActiveRecord 对象
001_create_addresses.rb../db/migrate创建 Addresses 数据库表的脚本

解决方案概述

下表列出了完成此解决方案所涉及的步骤。(不要担心,本文的其余部分将逐一介绍这些步骤。)注意,片段 一词是 Ruby on Rails 的一个术语。它是与 Web 浏览器中显示的内容相关的一个可重用代码段。大多数现代框架都包括某种类型的模板和用于动态组装模板以生成 Web 页的片段功能。片段为应用程序开发人员提供了切实的便利,并极大地帮助他们减轻了开发负担。RoR 命名约定是在片段前面加一个下划线(如 _addressForm.rhtml)。

  1. 修改 _form.rhtml 片段以便在城市和州之前显示邮政区码。
  2. 添加一个片段 (_cityState.rhtml) 以显示城市和州的输入字段。
  3. 修改 _form.rhtml 片段以“侦听”邮政区码字段的更改,并向服务器发出 Ajax 调用。
  4. 修改控制器以验证邮政区码(5 位数字)。如果无效,则向客户端返回空的 Ajax 响应。
  5. 修改控制器以创建有效的 XML 请求并发送到 USPS Web services 。
  6. 修改控制器以便从 USPS Web services 接收和解析 XML 响应。
  7. 修改 Ajax 响应,为 _cityState 片段填充 Web services 的值。
  8. 指出改进解决方案的一些方法,并通过电子邮件向作者发送您的建议。
图 1. 解决方案概述
解决方案概述
解决方案概述

步骤 1:修改视图

此地址表单的可用性可能是个问题。在美国,地址输入字段通常按以下顺序列出:

  1. 街道
  2. 城市
  3. 邮政区码

但此处的表单是按以下顺序列出输入字段的:

  1. 街道
  2. 邮政区码
  3. 城市

我们假设此可用性问题可以通过用户培训、说明文字或常识来解决。可能还有必要将城市和州字段显示为不可输入字段,或者防止全部输入这些字段。请与您的可用性专家共同讨论,以便起草一个可接受的解决方案。

清单 1 显示了一个名为 _form.rhtml 的输入表单以及输入文本字段。注意,输入文本字段 zip5 已经移动到城市和州的上方。最后一行 debug(params) 是可选的。在开发和测试阶段,我通常在 RoR 视图中包含此调试数据。

清单 1. 输入字段顺序更改后的代码片段 (_form.rhtml)
<%= error_messages_for 'address' %>

<p><label for="address_street">Street</label><br/>
<%= text_field 'address', 'street'  %></p>

<p><label for="address_zip5">Zip5</label><br/>
<%= text_field 'address', 'zip5', :size => "9", :maxlength => "5"  %></p>

<p><label for="address_city">City</label><br/>
<%= text_field 'address', 'city'  %></p>

<p><label for="address_state">State</label><br/>
<%= text_field 'address', 'state'  %></p>

<%= debug(params) %>

步骤 2:添加 Rails 片段

此解决方案中的第二步是通过将城市和州输入字段拆分为新片段来细分 _form.rhtml 输入表单。RoR 命名约定是在片段前加一个下划线。因此,新片段的名称是 _cityState.rhtml。新文件与 _form.rhtml 位于同一目录。清单 2 显示了新文件 _cityState.rhtml 的代码。

清单 2. 新的 RoR 片段 (_cityState.rhtml)
<p><label for="address_city">City</label><br/>
<%= text_field 'address', 'city'  %></p>

<p><label for="address_state">State</label><br/>
<%= text_field 'address', 'state'  %></p>

能够将城市和州字段与其他地址字段放在同一个文件中是比较理想的。我曾尝试这样做了,但只能将其放在一个新文件中。为什么呢?原因是使用来自 Ajax 调用的响应来更新多个表单字段比较困难。可能是我在 RoR 方面的经验不足,也可能是生成的 JavaScript 代码无法处理。经验不足的可能性比较大一些。清单 3 显示了 _form.rhtml 片段在移除城市和州输入字段之后的代码。注意,为新代码给定了 id = "ajaxLookup";这将在下一步中进行解释。

清单 3. 包含新片段 (_form.rhtml)
<%= error_messages_for 'address' %>

<p><label for="address_street">Street</label><br/>
<%= text_field 'address', 'street'  %></p>

<p><label for="address_zip5">Zip5</label><br/>
<%= text_field 'address', 'zip5', :size => "9", :maxlength => "5"  %></p>

<div id = "ajaxLookup">
  <%= render :partial => "cityStateFields" %>
</div>

命名约定是 RoR 中的一项重要思想。RoR 假设片段命名时使用一个前导下划线。并且 RoR 还假设对该片段的引用不包含下划线。因此,在清单 3 中,预期该行中不会包含下划线:<%= render :partial => "cityStateFields" %> 是正确的。RoR 查看此行并在同一目录中查找名为 _cityStateFields.rhtml 的文件。

步骤 3:侦听邮政区码的更改

Rails 提供了对 Ajax 的内置支持。这是 Rails 非常出色的功能之一。如清单 4 所示,只需添加几行代码就可以侦听邮政区码字段的更改。

清单 4. 将 Ajax 侦听器添加到邮政区码 (_form.rhtml)
01 <%= javascript_include_tag :defaults %>
02
03 <p><label for="address_street">Street</label><br/>
04 <%= text_field 'address', 'street'  %></p>
05
06 <p><label for="address_zip5">Zip5</label><br/>
07 <%= text_field 'address', 'zip5', :size => "9", :maxlength => "5"  %></p>
08
09 <div id = "ajaxLookup">
10  <%= render :partial => "cityStateFields" %>
11 </div>
12
13 <%= observe_field :address_zip5,
14              :frequency    => 2.00,
15              :update       => "ajaxLookup",
16              :url          => {:action => :cityStateSearch, :id => @address},
17              :with         => "‘zip5=’ + encodeURIComponent(value)"
18 %>
19
20 <%= debug(params) %>

注意:清单左端的两位数行号仅用于解释目的;它们不出现在代码中。

大功告成!大约通过 10 行 Ruby 代码,您就利用 Ajax 功能构建了此视图。在后台进程中,RoR 和原型库处理所有 JavaScript。现在我们逐一说明这 20 行代码。

第 01 行指示 RoR 包含 Prototype 和 Scriptaculous JavaScript 库。第 03-12 行的作用同上。第 13 行使用 Prototype 库中的 observe_field 方法。observe_fieldPrototypeHelper 类中的一个 Helper 方法。用简明的语言叙述就是,第 13-17 行的作用是每两秒检查一次 zip5 输入字段。如果 zip5 输入字段中有用户输入,就会在当前控制器(即 addressadmin_controller.rb)中调用操作 cityStateSearch,此逻辑由用户浏览器中的 JavaScript 运行。当 zip5 输入字段更改时,将执行从用户的浏览器到服务器的 Ajax 调用。注意第 09 行和第 15 行的相关性:第 15 行标识如何处理来自操作 cityStateSearch 的响应。此操作的响应(如果有)将更新名为 ajaxLookup<div> 标签。第 09 行的 div 标签 ID 等于 ajaxLookup。因此,来自操作 cityStateSearch 的响应将传递到第 10 行。第 17 行说明了向操作发送哪些的名称和值。因此,在本例中,字符串 zip5=90210 被传递到 addressadmin_controller 的名为 cityStateSearch 的操作。

接下来,开始使用控制器功能。最后一步指定,用户浏览器在邮政区码(即 zip5)输入字段中检测到更改时,将异步调用名为 cityStateSearch 的操作。下面是需要在服务器端通过编码实现的主要功能:

  • 验证邮政区码。
  • 构建 XML 以调用 Web services 。
  • 调用 Web services 。
  • 解析来自 Web services 的响应。
  • 将响应发回用户的 Web 浏览器。

步骤 4:验证邮政区码

使用无效的邮政区码调用 USPS Web services 没有任何意义。做一些进一步的限定即可消除大多数无效的邮政区码。在美国,邮政区码是五位数。清单 5 显示的代码可以检查 zip5 参数是否符合五位数的要求。

清单 5. 验证邮政区码 (addressadmin_controller.rb)
01  def cityStateSearch
02
03    if params[:zip5].nil?
04      logger.debug("zip5 is null")
05    elsif !(params[:zip5] =~ /\d{5}/)
06      logger.debug("We have a bad ZIP code -- not 5 digits.")
07      logger.debug("zip5 = #{params[:zip5]}")
08    else
09      logger.debug("We have a good 5-digit ZIP code.")
10      logger.debug("zip5 = #{params[:zip5]}")
11
12      if params[:address].nil?
13        @address = Address.new
14      else
15        @address = Address.find(params[:id])
16        if @address.update_attributes(params[:address])
17          flash[:notice] = 'Address was successfully updated.'
18        end
19      end
20    end
21
22  end   #cityStateSearch

第 01 行开始定义新操作 cityStateSearch。与此控制器中的其他操作不同,cityStateSearch 操作由 JavaScript(客户端 Ajax 代码)异步调用。第 03 行检查参数值是否为 null(在 Ruby 调用时为 nil)。第 05 行是一个正则表达式,用于将参数字符串值与 /\d{5}/ 进行比较,后者是我们都知道和喜欢使用的五位数正则表达式。表达式前面的感叹号用于对 elsif 表达式求反。(对,这是 RoR 的正确语法,就是我们通常使用的 else if。)

第 12-19 行用于创建或更新 @address 对象。第 5-7 行有些小技巧。当逻辑执行到这些行时,它将退出操作并返回到浏览器。最终用户是不知道的。当 zip5 为 nil 或者不是 5 位数时,此操作将跳过其余的逻辑步骤并立即返回。只要用户的指针处于视图中的 zip5 输入文本字段中,就会每两秒重新调用一次此操作。此功能早在 frequency 参数的 observe_field 方法中已经配置。

第 21 行是添加下一部分代码的地方。由于这是开发代码,因此使用了许多调试语句。在多次提炼和优化代码之后,我删除了 logger.debug 语句。而且,我是一个 RoR 新手,使用许多调试语句也是对我编程样式的一种宽慰,这在有些地方被准确地描述为“设法使其符合形式”。

步骤 5:创建一个有效的 XML 请求以发送到 USPS Web services

在流程的这一阶段,您处于 RoR 应用程序的服务器端,并可以看到一个五位数的邮政区码。由于您可以合理预期邮政区码是合法的,因此现在值得调用 USPS Web services 。要执行此操作,您需要创建一个有效的请求。我们跳过一些步骤,清单 6 显示了一个有效的 XML 请求的示例。

清单 6. 有效的 XML 请求
http://testing.shippingapis.com/ShippingAPITest.dll?API=CityStateLookup
&XML=<CityStateLookupRequest%20USERID="XXXXXXXXXXXX"><ZipCode ID=
"0"><Zip5>90210</Zip5></ZipCode></CityStateLookupRequest>

要使用 USPS Web 工具,您必须注册。注册非常容易而且是免费的(请参阅参考资料部分以了解详细信息)。在注册 USPS Web 工具之后,它们向我发送了测试服务器的名称和一个用户 ID。(在清单 6 中,通过使用 XXXXXXXXXXXX 来代替我的用户 ID)现在我们进一步分析此请求。Web services 端点由 API=CityStateLookup 指定。在 HTML 表单中,您现在可以看出我为什么调用了输入字段 zip5。这是 USPS 请求期望的名称。此 CityStateLookup Web services 在一次请求中最多可以接受五个邮政区码值。为简单起见,此代码仅传递了一个邮政区码,它有 XML 标签 <ZipCode ID= "0">。这样该功能就变得非常显而易见了:您需要获取用户输入的五位数的值,并将其放入名为 <Zip5> 的 XML 标签中。

那么这是一个什么类型的 Web services 呢?

这看起来好像是一个简单的问题,但回答起来并不总是那么容易。Web services 就像 Baskin Robbin 的 31 味冰淇淋或 Starbucks 咖啡菜单。您要一杯咖啡看起来应该非常容易,但如果想要加酱油的 Grande Chai Latte 就不那么容易了。首先我们限定它不是什么:它不是一个 XML-RPC 样式的 Web services 、文件样式的 Web services 、SOAP Web services 或代表性状态传输(Representational State Transfer,REST)(基于名词)请求 Web services 。USPS 的设计者决定将其实现为一个普通的 XML Web services 。USPS Web services 接受 GET 或 POST HTTP 请求。该请求是无状态的,无 cookie 或 URL 重写。请求和响应是区分大小写的。同样,向 USPS Web 工具注册也很容易,并且有大量的文档。(说明一下,我并没有加入 USPS。)

要创建 XML,您需要使用 RoR 中包含的 Builder::XmlMarkup 库。在控制器类文件 addressadmin_controller.rb 的开头,需要添加如清单 7 所示的代码。

清单 7. 要添加到 addressadmin_controller.rb 的代码
require 'open-uri'
require 'uri'
require 'rubygems'
require_gem 'builder'
require "rexml/document"
清单 8. 创建请求的 XML 部分
01  def cityStateSearch
02
03    if params[:zip5].nil?
04      logger.debug("zip5 is null")
05    elsif !(params[:zip5] =~ /\d{5}/)
06      logger.debug("We have a bad ZIP code -- not 5 digits.")
07      logger.debug("zip5 = #{params[:zip5]}")
08    else
09      logger.debug("We have a good 5-digit ZIP code.")
10      logger.debug("zip5 = #{params[:zip5]}")
11      #  Build the XML to call the web service
12      xm = Builder::XmlMarkup.new
13      xmlstuff = xm.CityStateLookupRequest("USERID"=>"XXXXXXXXXXXX") {
14        xm.ZipCode("ID"=>"0") {
15          xm.Zip5(params[:zip5]) }}
16
17    end
18  end   #cityStateSearch

只需要四行(从第 12 到第 15 行)就为请求创建了一个格式正确的 XML 文件。字符串变量 xmlstuff 包含以下 XML:

<CityStateLookupRequest%20USERID="XXXXXXXXXXXX"><ZipCode ID= "0"><Zip5>90210</Zip5></ZipCode></CityStateLookupRequest>

还有几个更重要的正确设置此请求格式的步骤。您需要使用清单 9 中的两行代码转义请求中的特殊字符。

清单 9. 转义请求中的特殊字符
uri_enc = URI.escape('http://testing.shippingapis.com/ShippingAPITest.dll
    ?API=CityStateLookup&XML=' + xmlstuff)
uri = URI.parse(uri_enc)

这几行代码会将所有特殊字符转换为正确编码的 HTTP 请求内容。通过为服务器名称、API 名称等设置变量或属性文件可以改进此代码,我将把这项任务留给您去做。您的目标只是能够正确执行对 Web services 的调用即可,可以在以后提炼和优化此代码。现在您已经有一个格式正确的 HTTP/XML 请求并可以准备调用 USPS Web services 了。

步骤 6:调用 Web services 并接收响应

在此步骤中,您将调用 USPS Web services 并接收一个响应。使用的代码必须能够解析 XML 响应。清单 10 显示了 USPS CityStateLookup 响应的一个示例。

清单 10. USPS CityStateLookup 响应
<?xml version="1.0"?>
<CityStateLookupResponse><ZipCode ID="0"><Zip5>90210</Zip5>
<City>BEVERLY HILLS</City><State>CA</State></ZipCode>
</CityStateLookupResponse>

这次我还直接使用 USPS Web 工具文档中的此示例。在此步骤中,您的目的是解析城市和州信息并将其放入 @address 对象的变量中。最后,这些变量作为 Ajax 响应的一部分返回。

请记住以下要点:Builder 库允许创建 XML,同时模块 REXML 支持解析 XML 数据。

清单 11. 调用 Web services 和解析响应 (addressadmin_controller.rb)
# The call to the Web service -- response is in var 'doc'
doc = REXML::Document.new open(uri)
logger.debug("doc = " + doc.to_s)
doc.elements.each("CityStateLookupResponse/ZipCode") { |element| 
logger.debug(element)
logger.debug("element[0] = " + element[0].to_s)
logger.debug("element[0].text = " + element[0].text)
logger.debug("element[1] = " + element[1].to_s)
logger.debug("element[1].text = " + element[1].text)
logger.debug("element[2] = " + element[2].to_s)
logger.debug("element[2].text = " + element[2].text)
# Set the model field values to the response from the Web service
@address.city = element[1].text
@address.state = element[2].text
      }

清单 11 中的代码遍历 XML 响应来查找城市 (element[1]) 和州 (element[2])。element[0] 是 zip5 的值。前面已经提到过,Web services 在一次请求中最多可以查找五个邮政区码。此代码的未来改进中应考虑遍历这些值。但是,在这个简单的示例中,每个请求始终只能传入一个邮政区码值。此代码在处理无城市/州响应值时还可以做得更好。这里只使用了最少的功能,因此您可以根据特定项目的约束和要求来改进此代码。如果在使用 Builder 库或 REXML 模块时存在问题,请参阅 RoR API 文档,其中提供了大量的示例。

如果您熟悉使用其他语言创建或解析 XML,就会立即发现在 RoR 中也是如此容易。RoR 为您带来的便利是轻松应对各种问题!XML 创建——不费吹灰之力。Ajax——助您一臂之力。XML 解析——小菜一碟。RoR 经常提醒我,目标是为最终用户快速完成实际工作。这确实是一个不错的感觉。

现在简要重述一下刚才的操作,您验证了邮政区码,创建了调用 Web services 的 XML 请求,解析了 XML 响应并将城市和州字段放入了 @address 对象。从用户的角度看,此过程的所有步骤都是异步完成的并且是在大约一秒钟内完成的。用户的指针仍在 Web 浏览器的 Html 表单的邮政区码字段中。不等他们感觉到操作过程,此操作就返回了带有正确城市和州值的 @address 对象。将信息发送到 partial _cityStateFields.rhtml 并填充到用户的浏览器。希望这能给用户带来惊喜——您给用户的感觉是,您的程序是为用户着想,在这里为他们提供帮助。您预知了他们的需求,并尽最大努力让他们的工作更轻松。

清单 12 显示了文件 addressadmin_controller.rb 中 cityStateLookup 操作的完整代码。

清单 12. cityStateLookup 操作的完整代码 (addressadmin_controller.rb)
require 'open-uri'
require 'uri'
require 'rubygems'
require_gem 'builder'
require "rexml/document"

class AddressadminController < ApplicationController
  
  <!-- other methods/actions -->

  def cityStateSearch

    if params[:zip5].nil?
      logger.debug("zip5 is null")
    elsif !(params[:zip5] =~ /\d{5}/)
      logger.debug("We have a bad ZIP code -- not 5 digits.")
      logger.debug("zip5 = #{params[:zip5]}")
    else
      logger.debug("We have a good 5-digit ZIP code.")
      logger.debug("zip5 = #{params[:zip5]}")

      if params[:address].nil?
        @address = Address.new
      else
        @address = Address.find(params[:id])
        if @address.update_attributes(params[:address])
          flash[:notice] = 'Address was successfully updated.'
        end
      end

      #  Build the XML to call the Web service
      xm = Builder::XmlMarkup.new
      xmlstuff = xm.CityStateLookupRequest("USERID"=>"XXXXXXXXXXXX") {
      xm.ZipCode("ID"=>"0") {
      xm.Zip5(params[:zip5]) }}

      webservice = 'http://testing.shippingapis.com/ShippingAPITest.dll?'
      uri_enc = URI.escape(webservice + 'API=CityStateLookup&XML=' + xmlstuff)
      uri = URI.parse(uri_enc)

      # The call to the Web service -- response is in var 'doc'
      doc = REXML::Document.new open(uri)
      logger.debug("doc = " + doc.to_s)
      doc.elements.each("CityStateLookupResponse/ZipCode") { |element| 
        #logger.debug(element.attributes["name"])
        logger.debug(element)
        logger.debug("element[0] = " + element[0].to_s)
        logger.debug("element[0].text = " + element[0].text)
        logger.debug("element[1] = " + element[1].to_s)
        logger.debug("element[1].text = " + element[1].text)
        logger.debug("element[2] = " + element[2].to_s)
        logger.debug("element[2].text = " + element[2].text)
        # Set the model field values to the response from the web service
        @address.city = element[1].text
        @address.state = element[2].text
      }
    end  # valid ZIP code if-statement-checkers
   render :partial => "cityStateFields"
  end

其他思考

下面是此解决方案和 Web services 的一些其他评论:

  • Web services 的运行独立于编程语言。此示例使用了 Ruby on Rails,但还可以使用其他语言和框架。
  • 此解决方案没有计划在 USPS Web services 不可用时该如何操作。本地缓存也许能够最大限度地减少停机的影响。
  • 从 Web services 解析 XML 响应时编写的完美代码也许是最少的。编写模板可能对解决方案的这一问题有所帮助。
  • 如果只输入邮政区码就可以查找城市和州,为什么还劳驾用户输入城市和州?
  • 将 Web services 标准引入市场的这些工作组 的效率如何?
  • 是否有人想过要实现统一描述发现集成(Universal Description Discovery Integration,UDDI)标准?
  • Web services 标准太复杂吗?开发人员是否可以完成所有首字母缩写词?
  • 市场几乎不会指定只有一个获胜者。许多竞争对手在下一轮竞争中可能抢得先机。但市场淘汰失败者的效率好像特别高。
  • 持反对意见的人对 RoR 的性能、安全性和是否值得推向生产的质疑会有多久?

许多左右市场的大型公司在评说 Web services 的不足,本文通过一个简单易懂的 Web services 说明了一个常见问题。美国邮政管理局的人员实现了一个可靠并且非常有用的 Web services 。Jakarta Struts 是解决以前 Web 应用程序框架问题(即解决整个模型视图控制器 (MVC) 堆栈问题)的一大改进。Ruby on Rails 至少是在 Struts 基础上进行了显著改进。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=SOA and web services, Open source
ArticleID=302094
ArticleTitle=使用 Web services 和 Ajax 实现数据自动录入
publish-date=04212008