UML软件工程组织

 

 

像专业人员一样开发 Ajax 应用程序,第 2 部分: 使用 Scriptaculous JavaScript 库和 script.aculo.us
 
2008-08-14 作者:Michael Galpin 出处:IBM
 
本文内容包括:
您正在构建 Web 应用程序吗?这个应用程序是像 cragislist,还是更像 flickr?如果答案是前者,那么您可以跳过本文了。如果是后者,本文正适合您。在这个包含三部分的系列文章的第 2 部分中,学习如何用 Scriptaculous JavaScript 库增强 Web 应用程序。

本文是包含三部分的系列文章的第 2 部分,这个系列讨论可以用来创建 Ajax 应用程序的流行的 JavaScript 库。在 第 1 部分 中,学习了如何用 Prototype 库创建一个用来管理歌曲的 Web 应用程序。在本文中,将使用 Scriptaculous 库构建一个用来管理照片的 Web 应用程序。

本文使用的是 Scriptaculous 的最新版本 1.8.1(参见 参考资料 中的链接)。Scriptaculous 使用 Prototype 1.6 库。您必须熟悉 JavaScript、HTML 和 CSS。本文演示 Scriptaculous 在 Ajax 方面的应用。在后端,结合使用 Ruby on Rails 2.0 和 MySQL 5.0.4(参见 参考资料)。只需稍微调整,就可以改用其他后端技术。

Scriptaculous 简介

Scriptaculous JavaScript 库是目前最流行的库之一。它用来在基于 HTML 的 Web 站点中添加丰富的交互功能。它提供了许多视觉效果和行为,帮助开发人员在 Web 应用程序中添加交互功能。Scriptaculous 是在 Prototype 库的基础上构建的。

图 1. Scriptaculous 和 Prototype 的关系
Scriptaculous 和 Prototype 的关系

如果您阅读了第 1 部分,应该已经见过 Prototype 提供的 Ajax 抽象示例。Scriptaculous 并不自行创建相似的功能,而是使用 Prototype 并在其上添加效果和行为。Scriptaculous 提供拖放元素等大量控件。还提供可以与控件结合使用的非常出色的视觉效果。

拖放控件

最有用、视觉上最吸引人的 Scriptaculous 特性之一是拖放。拖放特性在桌面应用程序中非常常见,但是在 Web 应用程序不常见。在 Web 应用程序中添加这种特性可以提供丰富的用户体验。这个任务看起来很困难,但是 Scriptaculous 大大简化了它。为了演示这个特性,我们将构建一个示例应用程序,通过分析它了解使用 Scriptaculous 的好处。

示例应用程序:照片管理器

我们将创建一个用来管理照片的应用程序作为示例应用程序。我们的应用程序允许通过拖放操作在照片集中添加或删除照片。在后端,我们将使用 Ruby on Rails 和 MySQL。Ruby on Rails 实际上默认包含 Scriptaculous 并提供 API,可以在应用程序中自动使用 Scriptaculous。我们并不从 Rails 使用这些特性;我们只使用 Rails 提供数据服务后端。

照片管理器后端

我们不打算花费太多时间讨论后端,对后端的认识只需足以理解前端操作的意义就够了。本文的 下载 部分提供所有代码。首先,我们使用 rails pix 创建一个 Rails 应用程序。接下来,需要按照 Rails 命名约定创建一个称为 pix_development 的数据库。需要修改 config/database.yml 文件,根据这个数据库修改配置参数(主机、名称、密码)。

下一步是最重要的步骤:为应用程序创建 scaffold。在一般情况下,创建 scaffold 仅仅是开发的基础;但是,在这个示例应用程序中,我们将用它创建一个 REST 式的接口,可以使用 Ajax 调用这个接口。命令如下:ruby script/generate scaffold photo thumb :string caption :string inSet :boolean。这会创建一个模型,其中包含一个 thumb 字段(用于缩略图)、一个 caption 和一个 Boolean(表示照片是否在照片集中)。注意,命令 rake db:migrate 将在数据库中创建我们需要的表。scaffold 生成命令还为应用程序创建一个控制器。我们需要修改这个控制器,为 Rails 自动创建的 REST 式服务提供 JSON 版本。修改后的控制器见清单 1:

清单 1. 照片 Web 控制器
 
                
class PhotosController < ApplicationController
  # GET /photos
  # GET /photos.xml
  protect_from_forgery :only => [:create]
  def index
    @photos = Photo.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @photos }
      format.json { render :json => @photos }
    end
  end

  # GET /photos/1
  # GET /photos/1.xml
  def show
    @photo = Photo.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @photo }
      format.json { render :json => @photo }
    end
  end

  # GET /photos/new
  # GET /photos/new.xml
  def new
    @photo = Photo.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @photo }
      format.json  { render :json => @photo }
    end
  end

  # GET /photos/1/edit
  def edit
    @photo = Photo.find(params[:id])
  end

  # POST /photos
  # POST /photos.xml
  def create
    @photo = Photo.new(params[:photo])

    respond_to do |format|
      if @photo.save
        flash[:notice] = 'Photo was successfully created.'
        format.html { redirect_to(@photo) }
        format.xml  { render :xml => @photo, :status => :created, 
		                                       :location => @photo }
        format.json  { render :json => @photo, :status => :created, 
		                                       :location => @photo }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @photo.errors, :status => 
		                                        :unprocessable_entity }
        format.json  { render :json => @photo.errors, :status => 
		                                        :unprocessable_entity }
      end
    end
  end

  # PUT /photos/1
  # PUT /photos/1.xml
  def update
    @photo = Photo.find(params[:id])

    respond_to do |format|
      if @photo.update_attributes(params[:photo])
        flash[:notice] = 'Photo was successfully updated.'
        format.html { redirect_to(@photo) }
        format.xml  { head :ok }
        format.json  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @photo.errors, :status => 
		                                       :unprocessable_entity }
        format.json  { render :json => @photo.errors, :status => 
		                                       :unprocessable_entity }
      end
    end
  end

  # DELETE /photos/1
  # DELETE /photos/1.xml
  def destroy
    @photo = Photo.find(params[:id])
    @photo.destroy

    respond_to do |format|
      format.html { redirect_to(photos_url) }
      format.xml  { head :ok }
      format.json  { head :ok }
    end
  end
end

请注意这里的几个元素。首先,protect_from_forgery 命令允许以 Web 服务的形式访问所有操作,但是创建操作除外。接下来,注意 respond_to do |format| 块,我们在其中添加了 format.json { render :json ... }。这会用 RoR 内置的 JSON 支持响应以 .json 结尾的 URL。所以,/photos.json 将用序列化为 JSON 的数据调用索引方法。正如在本系列的 第一篇文章 中看到的,这是一种向 Prototype 传递数据的非常方便的方法。我们已经看到了后端上的数据形式以及提供数据的方式。现在看看前端以及如何在前端使用 Scriptaculous。

照片管理器前端

为了理解前端,我们来看看代码并研究它的工作方式。这个前端是一个简单的 HTML 页面,见清单 2:

清单 2. organize.html Web 页面
 
                
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">

<html lang="en">
<head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <title>Photo Organizer</title>
     <script type="text/javascript" src="javascripts/prototype.js"></script>
     <script type="text/javascript" src="javascripts/effects.js"></script>
     <script type="text/javascript" src="javascripts/dragdrop.js"></script>
     <script type="text/javascript" src="javascripts/builder.js"></script>
     <script type="text/javascript" src="javascripts/organizer.js"></script>
     <script type="text/javascript" src="javascripts/organize_page.js"></script>
     <link rel="stylesheet" href="stylesheets/organizer.css"/>
</head>
<body onLoad="initPage()">
     <div id="pageTitle">Organize Your Photos</div>
     <div id="allPics">
          <span id="setPics"></span>
          <span id="availablePics"></span>
     </div>
          <div id="trash">&nbsp;</div>     
</body>
</html>

这看起来非常简单,对吗?注意,我们装载了几个 JavaScript 文件。因为 Scriptaculous 要使用 Prototype(我们自己也要使用它),所以必须装载 prototype.js。然后,使用属于 Scriptaculous 的三个库:effects.js、dragdrop.js 和 builder.js。Scriptaculous 是模块化的,所以可以只使用需要的特性。但是,dragdrop.js 依赖于 effects.js,所以只要使用拖放特性,就必须同时包含这两个文件。当然,在这两个 JavaScript 文件中,我们最感兴趣的代码是与这个应用程序相关的代码。当装载页面时,它调用 organize_page.js 中的 initPage 函数,见清单 3:

清单 3. 页面初始化 JavaScript
 
                
// the mode for the page
var model = {};

// page initialization
function initPage(){
     var options = {
          method : "get",
          onSuccess : initUi
     }
     new Ajax.Request("photos.json",options);
}
function initUi(xhr){
     model = new Organizer(xhr.responseJSON);
     var list = Builder.node("span", {id: "inList"});
     var list2 = Builder.node("span", {id: "outList"});
     var pix = model.pixList;
     for (var i=0; i < pix.length; i++){
          var pic = buildPicUi(pix[i]);
          if (pix[i].inSet){
               list.appendChild(pic);
          } else {
               list2.appendChild(pic);
          }
     }
     $("setPics").appendChild(list);
     $("availablePics").appendChild(list2);
     // setup drag-and-drop
     makeDraggables();
     initDropZones();
}

如果您以前使用过 Prototype,应该很熟悉第一个函数 initPage 了。它仅仅向服务器请求一个 JSON 形式的照片列表。它把 initUi 函数设置为回调函数,这个函数处理服务器返回的响应。下面看看这个函数。

initUi 函数获取服务器返回的数据,并创建一个 Organizer 对象。这个对象包含在一个单独的 JavaScript 文件中,作为应用程序的模型。您可以看看它的代码(见本文的 源代码)。目前,我们主要关注 UI 组件。我们为两组照片动态地创建视觉组件:照片集中的一组照片,照片集之外的一组照片。我们使用 Scriptaculous 的 Builder 库动态地为它们创建 DOM 元素。接下来,循环遍历模型中的照片,使用 buildPicUi 函数为它们创建视觉表示。这个函数见清单 4:

清单 4. buildPicUiJavaScript 函数
 
                
function buildPicUi(pic){
     var picId = "pic" + pic.id;
     var img = Builder.node("img", {src : "images/"+pic.thumb, alt: pic.caption});
     var picNode =  Builder.node("div",{id : picId, class: "pic"},img);
     return picNode;
}

再次使用 Builder 库创建 DOM 元素。在这里,我们在一个 div 中创建一个图像(img 标记)。现在,如果再看看 initUi 函数,会看到对另外两个函数的调用:makeDraggablesinitDropZones。这些函数见清单 5:

清单 5. 启用拖放
 
                
function makeDraggables(){
     for (var picId in model.pixMap){
          new Draggable(picId, {revert:true});
     }
}
function initDropZones(){
     Droppables.add("setPics",{onDrop: addToSet, accept: "pic"});
     Droppables.add("availablePics",{onDrop: removeFromSet, accept: "pic"});
     Droppables.add("trash",{onDrop: addToTrash, accept: "pic"});
}

这两个函数在应用程序中启用所有拖放功能。makeDraggables 函数循环遍历所有照片并为每张照片创建一个 Draggable 对象。这个函数只需要照片的 DOM ID(见清单 4 中的 buildPicUi 函数)和选项。在这里,惟一的选项是 revert = true。这表示,除非照片位于放置区,否则应该把它放回原来的位置。放置区由 initDropZones 创建。

initDropZones 获取页面上的三个区域:in-list、out-list 和底部的 “垃圾” 区。对于每个区域,它设置一个处理函数(onDrop),当对象被拖放到放置区时,会调用相应的函数。它还设置一个过滤器,用来控制可以拖放到这个区域的对象种类。在这里,所有放置区只接受 “pic” 类的对象。这个类是在清单 4 中的 buildPicUi 函数中为每个照片设置的。因此,只能把照片拖放到放置区中,而且照片只能拖放到接受它们的放置区中。每个区有不同的处理函数。我们来看看 adToTrash 处理函数,见清单 6:

清单 6. addToTrash JavaScript 函数
 
                
function addToTrash(pic){
     pic.style.left = "";
     pic.style.top = "";
     $("trash").appendChild(pic);          
     Effect.Puff(pic.id, {duration: 0.8});
     model.deletePic(pic.id);
}

这个函数做几件事。首先,它清除在拖动对象时创建的一些 CSS。然后,把这个照片添加到 “垃圾” 区中。注意这里如何使用 Prototype 的 $("trash") 表示法。接下来,使用一种称为 Puff 的 Scriptaculous 效果。这种视觉效果显示照片消失的过程,表示已经删除它。Scriptaculous 把这样的效果称为组合效果:它使用库的核心效果的组合(也可以使用许多其他效果)。最后,通过调用模型(Organizer 对象)发出 Ajax 调用,从数据库中删除这个对象。Organizer 对象见清单 7:

清单 7. Organizer
 
                
var Organizer = Class.create({
     initialize : function(pix){
          this.pixMap = {};
          this.pixList = pix;
          pix.each(function(pic){
               this.pixMap["pic"+pic.id] = pic;
          }.bind(this));
     },
     updatePic : function(picId, data){
          var photo = this.pixMap[picId];
          params = [];
          
          // Rails uses _method since browsers
          // do not properly send HTTP PUT and DELETE
          params["_method"] = "put";
          
          // Rails uses className[propertyName] for
          // request parameters to auto-bind them to
          // object properties
          $H(data).each(function(pair){
               params["photo["+pair.key+"]"] = pair.value;
          });
          var options = {
               method : "post",
               parameters : params
          };
          new Ajax.Request("photos/"+photo.id,options);
     },
     deletePic : function(picId){
          var photo = this.pixMap[picId];
          var params = {};
          params["_method"] = "delete";
          var options = {
               method : "post",
               parameters : params
          };
          new Ajax.Request("photos/"+photo.id,options);                    
     }
});

这个类有许多有意思的地方。与 Scriptaculous 一样,它大量使用了 Prototype。它使用 Prototype 创建一个类(Class.create(...))。请看一下 initialize 函数。在创建 Organizer 实例时会调用这个函数。它使用 Prototype 在 JavaScript 数组和对象中添加的 each 函数。在许多编程语言中都有相似的构造,但是在 JavaScript 中没有。在我们的示例中,它与 Prototype 的 bind 函数结合使用。这个函数是 Prototype 在 JavaScript 的 function object 中添加的。它会把当前上下文绑定到这个函数。在这里,在执行 each 函数调用的匿名函数期间,我们希望能够修改 Organizer 的 pixMap 字段。如果不进行绑定,this.pixMap 就没有意义。另外,updatePic 函数使用另一个 Prototype 语法简写方式,即 $H()。这会创建一个 Prototype 散列对象,这个对象在我们的对象中添加 each 函数等。这也是 Prototype 在 JavaScript 中添加的从其他语言借鉴的构造。最后,updatePicdeletePic 使用 Prototype 的 Ajax.Request 函数创建一个跨浏览器兼容的 Ajax 服务器请求。

运行示例

为了运行这个示例,需要启动 Rails:ruby script/server start。现在,可以在浏览器中打开这个示例,见图 2:

图 2. 照片管理器示例
照片管理器示例

在装载这个页面时,可以使用 Firebug 等工具观察异步装载数据的过程,见图 3:

图 3. Firebug 中显示的初始数据装载过程
Firebug 中显示的初始数据装载过程

在把照片从一个列表拖放到另一个列表或屏幕底部的 “垃圾” 区时,应该会看到另一个 Ajax 请求,见图 4:

图 4. 拖放所触发的 Ajax 请求
拖放所触发的 Ajax 请求

现在,我们的 Web 应用程序具备了许多与桌面应用程序相似的功能。它是使用 Scriptaculous 提供的高级控件和效果构建的,并利用 Prototype 简化了 Ajax 开发。

结束语

本文是讨论 JavaScript 库的 包含三部分的系列文章 的第 2 部分。在本文中,我们研究了 Scriptaculous JavaScript 库提供的一些控件和效果。了解 Scriptaculous 是如何在 Prototype 的基础上构建出来的,以及它如何帮助简化 Ajax 开发。我们还看到如何在 Prototype 的 Ajax 中结合使用 Scriptaculous 控件和效果,创建丰富的用户体验。但是,我们只涉及到了 Scriptaculous 的皮毛。Scriptaculous 还有可以在 Web 应用程序中使用的其他控件和效果,值得您花时间进一步研究。

下载

描述 名字 大小 下载方法
示例代码
wa-aj-ajaxpro2.zip
170KB

参考资料

学习 获得产品和技术 讨论
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号