内容


结合使用移动应用程序服务和 Dojo

结合使用 Web 2.0、Mobile 应用程序服务构建块和 Dojo 创建移动客户端应用程序

Comments

简介

IBM WebSphere Application Server Feature Pack for Web 2.0 and Mobile(后面简称为 Web 2.0 and Mobile 特性包)将企业应用程序从桌面扩展到移动设备。版本 1.1 包括的应用程序构建块可以极大地简化客户端功能到服务器功能的集成。

通过详细介绍如何创建样例移动应用程序的高级任务,本文将演示如何使用特性包中的应用程序构建块构建一个基于 Dojo 的 Web 应用程序客户端,它可以提供快速而又有意义的用户体验。

开始之前

在了解本文介绍的原理时,没有必要下载和安装以下组件,但这样做会使您掌握更多的知识。

要在您自己的实验环境中完成所有步骤,则需要访问 IBM WebSphere Application Server V7 或更高版本(或另一个 Java EE 应用服务器),同时正确安装 IBM WebSphere Application Server Feature Pack for Web 2.0 and Mobile V1.1。Web 2.0 and Mobile 特性包提供了多个应用程序服务构建块和 Dojo Toolkit,它们是完成本项目所需的全部内容。

当您在 WebSphere Application Server 上安装 Web 2.0 and Mobile 特性包时,您会发现以下这些资源(将在后面的小节中描述):

  • WEBSPHERE_APP_SERVER_ROOT/installableApps/application_services/graphics/appsvcs-graphics.ear
  • WEBSPHERE_APP_SERVER_ROOT/installableApps/application_services/analytics/appsvcs-analytics.ear
  • WEBSPHERE_APP_SERVER_ROOT/installableApps/application_services/optimizer/appsvcs-optimizer.ear
  • WEBSPHERE_APP_SERVER_ROOT/samples/application_services/fileuploader/appsvcs-fileuploader.ear
  • WEBSPHERE_APP_SERVER_ROOT/samples/application-services/directorylisting/appsvcs-directorylisting.ear

这些应用程序中的每一个应用程序都是一个可安装的企业应用程序归档 (EAR)。但是,您必须编辑一些配置参数,将它们联系在一起。配置和安装每个 EAR 文件的步骤都是一样的,因此接下来将一并介绍这些步骤,以方便您进行参考。

配置应用程序选项

一个 EAR 文件可以包含一个或多个 Web 应用程序归档 (WAR) 文件。在本例中,每个 EAR 包含一个 WAR。IBM 使用 REST 样式方法实现服务器端应用程序;在本例中,REST 实现是一个 JAX-RS 实现。每个 WAR 文件都包含一个具有典型的 servlet 定义的 web.xml servlet 定义文件,该 servlet 定义中包括一些自定义初始化程序 (init-params),用于配置特定于 WAR 中包含的应用程序的选项。

具体来讲,必须修改 java.io.tmpdir 参数值。如果在 param-value 中使用 java.io.tmpdir 作为特殊值,则要求底层应用程序服务对关键的 javax.servlet.context.tempdir 使用特殊 servlet 容器值。在 web.xml 文件中修够这个值不会影响 JVM 或 servlet 值。

要在每个 WAR 文件中编辑 init-params(以 FileUploader 为例),请执行以下操作:

  1. 解压缩 appsvcs-fileuploader.ear 文件并找到 appsvcs-fileuploader-war-1.0.0.0.war。
  2. 解压缩 appsvcs-fileuploader-war-1.0.0.0.war 文件并找到 WEB-INF/web.xml 文件。
  3. 编辑 WEB-INF/web.xml 文件。找到 <param-value>java.io.tmpdir</param-value>,并将 java.io.tmpdir 的值更改为适当的值。
  4. 将 WEB-INF/web.xml 文件压缩成 appsvcs-fileuploader-war-1.0.0.0.war 文件。
  5. 将 appsvcs-fileuploader-war-1.0.0.0.war 文件压缩成 appsvcs-fileuploader.ear 文件。

本文的目的是展示如何将 Dojo 与服务器端应用程序服务建立联系。因此,将从较高的层次介绍与 JavaScript™ 和 Dojo 编程有关的概念。本文假设您具备 JavaScript 编程的基本知识。

安装应用程序

许多 Java™ EE 应用程序都有一个文件夹,其中可以放置用于 “热部署” 的 EAR 文件。如果愿意的话,您可以使用这种特性,或者可以通过安装脚本安装 EAR 文件,或使用管理控制台。无论您选择哪种方式安装 EAR,都应当接受提示的所有默认配置,并确认安装成功,且应用程序正在运行。在某些应用服务器中,包括 WebSphere Application Server,确保应用程序正在运行是一个单独的步骤。

创建客户端应用程序

您可能会问,“服务器在哪”?这个问题提得很好,但是在回答它之前,您必须首先准备好快速创建和开发客户端所需的所有内容。在一个活动测试服务器上编辑 HTML 和 JavaScript 最简单不过了,因此您可以立即对做出的更改进行测试。在测试 Dojo JavaScript 客户端代码如何与服务器端应用程序服务交互时,这种方法尤其有用。首先,您创建 dojox.mobile 骨架,然后在配置和安装应用程序构建块的过程中逐渐向其中添加内容。

您需要创建一个 HTML 文件,然后收集将要放在应用服务器中的 Dojo Toolkit,应用程序构建块 EAR 文件稍后也将安装到这个应用服务器。您将在一个新的 WAR 文件中完成这些操作。有一些出色的 IBM 产品支持构建大型客户端应用程序,但是对于这个简单示例,使用基本的工具即可。

  1. 在一个临时目录中创建该目录结构:
    • WEB-INF
    • dojo
  2. 在 WEB-INF 目录下创建如清单 1 所示的 web.xml 文件,这样就可以提供来自 WAR 的静态文件内容。您可能有一个 Web 服务器或其他提供静态文件内容的方法,但是通常最简单的方法是将静态内容 WAR 放到与应用程序构建块相同的服务器上,根据浏览器的同源策略,您将与该构建块进行交互。
    清单 1
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app id="My_Dojo_App" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
      http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
        <display-name>Dojo Client Application</display-name>
    
        <welcome-file-list>
            <welcome-file>index.html</welcome-file>
        </welcome-file-list>
    </web-app>
  3. WEBSPHERE_APP_SERVER_ROOT/web2mobilefep_1.1/ajax-rt_1.X/ 中的所有内容复制到 dojo 目录中。您的 Dojo 客户端应用程序不会使用该目录提供的所有组件,但这并不妨碍将所有组件都打包到该 WAR 中;Dojo 会根据 JavaScript 代码中的 requires 语句下载它所需的内容。在生产环境中,将创建一个经过优化的 Dojo build(参见 参考资料)。
  4. 使用清单 2 所示的文件创建 index.html 样板文件。
    清单 2
    <!DOCTYPE html>
    <html lang="en">
    <head>
    
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,
            initial-scale=1,
            maximum-scale=1,
            minimum-scale=1,
            user-scalable=no"/>
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <title>My Application Name</title>
    
    <script
            type="text/javascript"
            djConfig="parseOnLoad: true"
            src="dojo/dojo/dojo.js">
    </script>
    <script type="text/javascript">
    // your Dojo-based application code goes here
    </script>
    </head>
    <body>
    </body>
    </html>
  5. 将 dojo 目录、WEB-INF 目录和 index.html 文件打包成一个 WAR 文件,文件名为 MyDojoClientApplication.war。将该 WAR 文件安装到您的应用服务器上。在安装时,服务器可能会提示您提供一个上下文 root。如果是这样的话,请输入 myclient 之类的值。最后,应当可以通过 http://<SERVER>:<PORT>/myclient/index.html 访问 index.html 文件。

您现在已经准备好安装一些应用程序服务,并将 Dojo 客户端与它们连接起来。

集成第一个服务:Analytics

对于 Analytics 服务,不需要进行特殊的服务器端配置。请查找 WEBSPHERE_APP_SERVER_ROOT/installableApps/application_services/analytics/appsvcs-analytics.ear 文件,并将其安装到您的应用服务器上。然后编辑 index.html 文件,将清单 3 所示的代码修改为如清单 4 所示。

清单 3
<script
        type="text/javascript"
        djConfig="parseOnLoad: true"
        src="dojo/dojo/dojo.js">
</script>
清单 4
<script
        type="text/javascript"
        djConfig="parseOnLoad: true,analyticsUrl: '/appsvcs-analytics/rest/analytics/logger'"
        src="dojo/dojo/dojo.js">
</script>

同样,将清单 5 所示的代码行添加到目前为止仍然空着的脚本标记,代码注释为 your Dojo-based application code goes here

清单 5
// analytics
dojo.require("dojox.analytics.plugins.dojo");

在 Web 浏览器中刷新 index.html 文件,您将看到,Analytics 服务被报告到应用服务器的默认日志中。(analytics 服务和客户端中提供了一些额外的配置选项和特性,比如记录客户端事件,这些内容超出了本文的讨论范围)。

集成第二个服务:DirectoryListing

directory listing 服务实现了 dojox.data.FileStore 指定的协议,但是这里使用的技巧也可以应用于各种可从延迟加载数据中获益的情形。

  1. 如前所述,第一件事是配置 DirectoryListing 服务。这意味着您将先解压缩 DirectoryListing EAR 文件,再解压缩 WAR 文件,编辑 web.xml 以指向所选择的现有目录,然后再次压缩该目录并安装。完成这些工作后,需要编写一些客户端代码。
  2. 告知 Dojo 您需要的所有模块。在对 analytics 调用 dojo.require 的情况下,添加 dojo.require 语句(清单 6)。
    清单 6
    // mobile bootstrap
    dojo.require("dojox.mobile.parser");
    dojo.require("dojox.mobile");
    dojo.requireIf(!dojo.isWebKit, "dojox.mobile.compat");
    dojo.require("dojox.mobile.deviceTheme");
    
    // directorylisting
    dojo.require("dojox.data.FileStore");
    dojo.require("dojox.mobile.RoundRectDataList");
    
    // app
    dojo.require("dojo.data.ItemFileReadStore");
    dojo.require("dojo.NodeList-traverse");

    注意,所开发的应用程序针对的是智能手机。因此要遵守 dojox.mobile.* 要求。您正在构建的应用程序将与 DirectoryListing 服务通信,会根据需要延迟加载子目录,因此,在 dojox.mobile.RoundRectDataLists 依赖于 dojox.data.FileStore 的情况下,需要创建 dojox.mobile.Views,而 dojox.data.FileStore 会与服务器端 DirectoryListing 服务进行通信。
  3. 创建并呈现 initial dojox.mobile.View。使用 dojo.require 语句创建应用程序定义,如清单 7 所示。
    清单 7
    var MyApp = {
    
        // Create a new view
        // id - the element id to give to the newly created node
        createView: function(id) {
    
            var view = new dojox.mobile.View({
                id: id,
                selected: true
            }, dojo.create("DIV", null, dojo.body()));
            view.startup();
    
            var headingArgs = {
                label: "Dynamic View"
            }
    
            var heading = new dojox.mobile.Heading(headingArgs);
        
            view.addChild(heading);
    
        },
    
        loadInitialList: function() {
            this.createView('view1');
        }
    
    };
    
    dojo.addOnLoad(function() {
        MyApp.createView("view1");
    });
  4. 您现在已经创建了一个视图,但是它并没有显示来自 DirectoryListing 服务的文件列表,因为您还没有与其建立连接。您现在需要将带有正确属性的 dojox.data.FileStore 对象提供给 createView 函数,使它能够用 RoundRectDataList 填充创建的视图。注意清单 8 中所示的 createView 的参数,您在 onLoad 事件中调用的是 loadInitialList 而不是 createView。
    清单 8
    var MyApp = {
    
        // Create a new view if necessary and make transition
        // li - the list item node that was clicked
        // id - the element id to give to the newly created node
        // store - the *store object you will pass to the RoundRectDataList
        createView: function(id, store) {
    
            var view = new dojox.mobile.View({
                id: id,
                selected: true
            }, dojo.create("DIV", null, dojo.body()));
            view.startup();
    
            var headingArgs = {
                label: "Dynamic View"
            }
    
            var heading = new dojox.mobile.Heading(headingArgs);
        
            view.addChild(heading);
    
            var items = [];
            var scope = this;
    
            // you need to do some manipulation of the items so you can pass them in
            // a store to the new RoundRectDataList you'll be creating in onComplete
            store.fetch({
                onItem: dojo.hitch(scope, function(item, request) {
                    var path = item.path;
                    var name = item.name;
                    var targetId = path.replace('/', '_') + '_' + name;
                    if (item.directory == true) {
                        // this item is a directory, so you enable
                        // clicking on it to traverse into its contents
                        item.moveTo = '#';
                        item.transition = "slide";
                        item.id = targetId;
                        item.onclick = function(){alert("clicked!")};
                    }
                    items[items.length] = item;
                }),
                onComplete: function() {
                    var list = new dojox.mobile.RoundRectDataList({
                        label: 'name',
                        store: new dojo.data.ItemFileReadStore({
                            data: {label: 'name', items: items}
                        }),
                    });
                    view.addChild(list);
                    view.show();
                }
            });
        },
    
    
        loadInitialList: function() {
        
            var store = new dojox.data.FileStore({
                url: "rest/directorylisting"
            });
    
            this.createView('view1', store);
        }
    
    };
    
    dojo.addOnLoad(function() {
        MyApp.loadInitialList();
    });
  5. 现在,可以在 DirectoryListing 服务中看到顶级文件和目录。因为您希望能够遍历整个目录结构,所以必须注册一个事件侦听器,侦听对目录列表中的列表项执行的单击(或碰触)操作。因此,不应调用 alert("clicked!"),而应调用另一个函数。将 alert("clicked!") 改为 scope.handleClick(item),并向 MyApp 添加 handleClick 函数(清单 9)。
    清单 9
        handleClick: function(item) {
    
            // show animated gif icon to indicate background activity
            var prog = dojox.mobile.ProgressIndicator.getInstance();
            dojo.body().appendChild(prog.domNode);
            prog.start();
    
            // internal function to be called once all of the child items are loaded
            var triggerView = dojo.hitch(this, function(arrOfItems) {
                var store = new dojo.data.ItemFileReadStore({
                    data: {label: 'name', items: arrOfItems}
                });
                // find the dom node that was clicked
                var li = dojo.byId(item.id[0]);
                // stop the animated gif icon
                prog.stop();
                this.createView(li, 'view' + li.id, store);
            })
    
            dojo.xhrGet({
                url: "rest/directorylisting" + item.path[0],
                handleAs: "json",
                load: function(data) {
                    var dataLength = data.children.length;
                    var items = [];
                    dojo.forEach(data.children, dojo.hitch(this, function(item, i) {
                        dojo.xhrGet({
                            url: this.url + "/" + item,
                            handleAs: "json",
                            load: function(subdata) {
                                items[items.length] = subdata;
                                if (dataLength == items.length) {
                                    triggerView(items);
                                }
                            },
                        });
                    }));
                },
            });
        },
  6. 如果现在尝试运行视图,您会发现它没有按照期望运行。在单击某个项目后,将在下方显示一个视图,其中没有显示列表。这是因为没有进行从现有 View 到新 View 的转换,并且错误地处理了存储数据。dojox.data.FileStore 保持项目的方式与 dojo.data.ItemFileReadStore 不同,它将项目的属性封装到数组中以提高效率。因此,您需要处理两个新需求;createView 必须知道从哪个视图进行转换,以及传递给它的存储的类型。请使用新的版本替换 createView 函数(参见清单 10)。
    清单 10
        // Create a new view if necessary and make transition
        // li - the list item node that was clicked
        // id - the element id to give to the newly created node
        // store - the *store object you will pass to the RoundRectDataList
        createView: function(li, id, store) {
    
            if(dijit.byId(id)) {
                // target view already exists, just transition to it
                dijit.byNode(li).transitionTo(id);
                return;
            } else {
                var view = new dojox.mobile.View({
                    id: id,
                    selected: true
                }, dojo.create("DIV", null, dojo.body()));
                view.startup();
    
                var headingArgs = {
                    label: "Dynamic View"
                }
                if (li) {
                    headingArgs.back = "Back";
                    // find the parent View's id so you can enable the back button
                    var ulParent = dojo.query('#'+li.id).parent();
                    var divParent = dojo.query('#'+ulParent[0].id).parent();
                    headingArgs.moveTo = divParent[0].id;
                };
    
                var heading = new dojox.mobile.Heading(headingArgs);
            
                view.addChild(heading);
    
                var items = [];
                var scope = this;
    
                // you need to do some manipulation of the items so you can pass them in
                // a store to the new RoundRectDataList you'll be creating in onComplete
                store.fetch({
                    onItem: dojo.hitch(scope, function(item, request) {
                        // if we're fetching from dojox.data.FileStore, the item
                        // properties are *not* wrapped in an array like they are
                        // with ItemFile*Store
                        var path;
                        var name;
                        if (item.path instanceof Array) {
                            path = item.path[0];
                            name = item.name[0];
                        } else {
                            path = item.path;
                            name = item.name;
                        }
                        var targetId = path.replace('/', '_') + '_' + name;
                        if ((item.directory == true) ||
                                ((item.directory instanceof Array)
                                  && (item.directory[0] == true))) {
                            // this item is a directory, so you enable
                            // clicking on it to traverse into its contents
                            item.moveTo = '#';
                            item.transition = "slide";
                            item.id = targetId;
                            item.onclick = function(){scope.handleClick(item)};
                        }
                        items[items.length] = item;
                    }),
                    onComplete: function() {
                        var list = new dojox.mobile.RoundRectDataList({
                            label: 'name',
                            store: new dojo.data.ItemFileReadStore({
                                data: {label: 'name', items: items}
                            })
                        });
                        view.addChild(list);
                        if (li) { // you got here from a click
                            dijit.byNode(li).transitionTo(id);
                        } else {
                            view.show();
                        }
                    }
                });
            }
        },
  7. 现在,createView 获取了三个参数,必须同时在 loadList 和 handleClick 函数中修改对 createView 的调用,才能传递适当的参数:
    • handleClick 中,将 this.createView('view' + li.id, store); 更改为 this.createView(li, 'view' + li.id, store);
    • loadInitialList 中,将 this.createView('view1', store); 更改为 this.createView(undefined, 'view1', store);

现在已完成了目录清单集成。可以通过延迟加载的方式,从 DirectoryListing 应用程序服务遍历文件结构。

作为应用程序的一项增强,您可以将 onclick 事件附加到非目录项中,显示有关这些文件的信息,或者在可以通过 Web 浏览器访问它们时,用它们来显示文件的实际内容。

本文的 下载 部分附带了一个文件,它包含 web.xml 和 index.html 文件,可以将这些文件作为起点,演示本节提出的客户端应用程序。附带的 WAR 文件是作为从 Dojo V1.7 开始构建 Dojo 应用程序的推荐形式的示例提供的。

集成第三个服务:FileUploader

FileUploader 应用程序服务样例在 JAX-RS 资源类中检索标准的 HTML multipart/form-data 格式的提交。在使用提交的数据调用 FileUploader 方法时,大部分解析入站消息负荷的工作已经完成。本文提供了该示例的 Java 源代码以供您学习。

由于本文的目的是将这些服务集成到一个 dojox.mobile 客户端中,在撰写本文时,只有有限的智能手机支持从移动 Web 浏览器中浏览文件系统,因此到目前为止,无法展示服务与您创建的样例应用程序的集成。如果需要的话,您可以查看本样例服务的 Java 和 Dojo 源代码。

集成第四个服务:Graphics

Graphics 应用程序服务支持动态的缩图以及其他一些特性。它还可以将 HTML5 SVG 标记内容转换为多种需要的图形格式之一。这两种特性的每一种都可以用于移动环境。例如,如果托管了一个照片库,其中的图像具有较大的尺寸,将用于在桌面显示器中浏览,那么您可能希望根据访问您的站点的客户端设备的能力缩小图像的尺寸。这将节省网络延迟,极大地缩短加载时间(特别是在手机网络中),并通过提高应用程序的响应速度改善客户端体验。

集成 Graphics 应用程序服务的一种方式是检测客户端浏览器窗口的大小并将值和图像 URL 传递给 Graphics 服务,这样客户端就不会尝试下载超过智能手机浏览器像素的在线 HTML 图片。

例如,您可以动态创建所有 img 标记,这样 src 属性中的所有 URL 都通过 Graphics 服务过滤。在 Dojo 中,这只需要很少的代码即可实现。首先,在打开和闭合 body 标记中包含 <div id="gallery"></div>,然后写入 Dojo 代码来填充 IMG 标记(参见清单 11)。

清单 11
<script type="text/javascript" src="dojo/dojo/dojo.js"></script>

<script type="text/javascript">
dojo.require("dojo.window");

var MyImageURLs = [
    "/image1.gif",
    "/image2.jpg",
    "/image3.png"
];

dojo.addOnLoad(function() {
    
    var maxWidth = dojo.window.getBox().w;
    var maxHeight = dojo.window.getBox().h;
    dojo.forEach(MyImageURLs, function(url, i) {
        // use the graphics service to convert all images to jpg and
        // limit their dimensions to the size of the browser window
        var imgTag = dojo.create('img', {
            src:"/appsvcs-graphics/rest/graphics/convert/binaryResponse?sourceUrl="
                + url
                + "&desiredFormat=jpg&maxWidth=" + maxWidth + "&maxHeight=" + maxHeight
        });
        dojo.place(imgTag, dojo.byId("gallery"), "last");
    });
});
</script>

集成第五个服务:Optimizer

特性包的版本说明和文档中将该服务称为 Browser Detection 服务,但是最好将其描述为一个文件优化器服务。它提供了许多配置选项来处理 HTTP 缓存头文件 (header)。典型的 Web 浏览器会在一定时间内缓存某个文件,如服务器响应中的 Cache-Control 或 Expires 头文件所示。客户端可以向服务器发送针对某个缓存文件的请求,并且只会接收到一个 304 Not Modified 回复。例如,如果您希望智能手机客户端的网络流量变得平稳,可以将 com.ibm.ws.mobile.appsvcs.optimizer.cacheDeltaMin 设置为希望使网络流量变平稳的秒数。如果应用程序的一般使用时长为 5 分钟,那么可以将该值设置为 300。

除了对缓存头文件执行微调优,该服务还压缩返回给客户的的文件,如果客户端能够接收和处理 gzip 内容的话。这个特性对于客户端是无缝的;不需要进行配置或特殊代码就可以在服务中启用该特性。对于没有自动压缩静态文件内容的应用服务器,这个服务解决了这一问题。

要使用该服务,首先编辑 Optimizer 服务的 web.xml 文件中的配置,使其指向您希望对其使用该文件的 root 目录。然后安装该服务,重定向 HTML 中的 URL,以便从 Optimizer 服务中检索内容。清单 12 提供了一个示例。

清单 12
<script
        type="text/javascript"
        djConfig="parseOnLoad: true,
            analyticsUrl: '/appsvcs-analytics/rest/analytics/logger'"
        src="/appsvcs-optimizer/rest/optimizer/dojo/dojo/dojo.js">
</script>

注意,script 标记的 src 属性的 URL。dojo.js 和根据 dojo.require 语句下载的所有后续文件都将通过 Optimizer 服务下载。客户端 Web 浏览器将执行您定义的缓存配置,并将接收压缩过的文件(如果支持的话)。这将改善网络性能,通过限制网络流量提高应用程序的响应性,从而在整体上增强客户端用户体验。

Dojo build

现在已经完成了应用程序部署,只剩下最后一个任务。将应用程序部署到生产环境之前的最后一步是生成一个 Dojo build。创建 Dojo build 会将许多模块聚集到一个 JavaScript 文件,还将聚集任何需要的级联样式表 (CSS),并缩小它们。这将极大地减少客户端应用程序下载的文件数,以及下载内容的整体大小。应用程序的初始加载时间将明显少于部署非构建 Dojo 应用程序时所用的时间。

参见 参考资料,获得有关如何执行 Dojo build 的信息。

结束语

将应用程序服务集成到您的移动客户端应用程序中可以极大地增强整体用户体验,同时显著提升性能。如果操作适当的话,结合使用 Dojo build、Optimizer 服务、Graphics 服务、DirectoryListing 服务中内置的延迟加载技术和相应的 djox.data.FileStore 这些内容,将创建快速的、灵敏的客户端应用程序。添加 FileUploader 和 Analytics 这样的特性会使应用程序变得更强大、更易于使用。如果将这些特性与应用程序的其余部分组合在一起,那么您将得到一个快速、特性丰富、易于使用的应用程序!


下载资源


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere
ArticleID=753652
ArticleTitle=结合使用移动应用程序服务和 Dojo
publish-date=08252011