使用混合应用程序编程模型为 WebSphere Commerce 构建移动应用程序

学习如何使用混合应用程序编程模型为 WebSphere® Commerce 构建移动应用程序。本文描述混合模型,它与其他移动应用程序编程模型的区别,并通过两个用例研究介绍如何使用它为 WebSphere Commerce 构建 iPhone® 和 Android® 应用程序。

概述

随着拥有 Internet 访问能力的移动设备的快速增长,移动应用程序已经成为许多跨渠道零售解决方案的必要组成部分。特别是,iPhone OS 和 Android 设备的日益流行已经推动了对原生应用程序的需求,这些应用程序拥有在移动浏览器中运行的 web 应用程序所不能提供的能力。但是,从头开始构建一个完全原生的应用程序需要投入极大的技能和资源,而且为一个平台编写的大部分代码将不能在另一个平台上重用。这正是混合应用程序编程模型的用武之地 — 通过混合模型,您可以重用您的许多现有 web 应用程序资产来构建移动应用程序。

注意,本文面向有兴趣为 WebSphere Commerce 构建移动应用程序的架构师和开发人员。本文假定您对 WebSphere Commerce 和移动应用程序开发有基本的了解。本文的配套白皮书 Leveraging mobile commerce in your multi-channel strategy 讨论了移动商务的市场机遇,简要描述了一些在 WebSphere Commerce 中利用那些机遇的解决方案。.

移动应用程序编程模型

我们可以将提供移动应用程序的方法归为三类:web 应用程序、原生(设备上)应用程序,或二者的结合。下面几个小节简要描述每个模型的部分特征。

移动 web 应用程序

通过结合使用 HTML、层叠样式表(CSS)和 JavaScript 将移动 web 应用程序交付给移动设备。您可以或多或少地对移动设备进行一些定制。还可以定制特定设备。

这个模型的好处是,它基于我们熟悉的 web 编程技巧。它不需要最终用户安装任何特殊应用程序,因为它依赖内置浏览器。迭代开发很容易实现。更新不需要用户操作,只需更新服务器提供的内容。它还拥有一个好处:相同的、或近乎相同的内容能够服务不断增长的移动设备系列产品。

WebSphere Commerce 第 7 版包含了一个 Madisons Mobile store,它为智能手机提供定制的 web 体验。但是,这种方法也有一些缺点,包括应用程序可能缺乏原生应用程序的观感,可能有性能影响,以及不能 访问一些设备功能,比如本地地址簿或相机。

这种方法也依赖移动浏览器的功能,这可能是一个严重的不兼容性来源,特别是对于比较老的手持设备。HTML5 和 Webkit 浏览器引擎的流行将有助于缓解这些问题,但前提是功能更强大的新设备获得市场份额。

原生应用程序

相比之下,原生应用程序专门针对一个单一的移动平台编码,使用制造商提供的特定于设备的函数和库。这样的应用程序能够轻松集成平台的独特观感,通常比 web 应用程序表现更出色。它们可以任意访问 API 支持的原生设备功能。

问题是必须为每个受支持的平台从头编写应用程序。这样的设备数量众多,并且更新换代速度很快,因为大量公司通过功能越来越强大的设备争夺市场份额。编写原生应用程序也需要专业技术,平台之间的技术要求差异很大,不容易从一个平台迁移到另一个平台。

混合应用程序

混合应用程序试图结合 web 应用程序和原生应用程序的优势,是本文关注的焦点。基本的思路是编写一个原生应用程序 shell,但通过使用 web 视图(集成通常使用的 HTML、CSS 和 JavaScript)来提供主内容。原生框架可能包含一些可以从 JavaScript 访问的库,以便访问一些设备功能,比如本地地址列表、GPS、相机和其他设备特性。这允许 web 应用程序包含访问设备功能的代码。

混合应用程序模型的一个关键特性是在 JavaScript 例程和原生设备功能之间架起一座桥梁。如果每个用户都必须针对每个受支持的设备重新开发这个框架,那么这个开发任务非常艰巨。但是,实际上,有几个现成的这种性质的框架可用,既有专有的,也有开源的。如果这样一个框架支持多个设备,则意味着可以对所有受支持的设备使用相同的服务器端代码,只需要最小的细微定制,这意味着可能能够极大地节约需要的资源。

注意,原生和混合应用程序之间的差别主要位于实现层面。从最终用户的角度看,混合应用程序和原生应用程序通常没有区别。混合应用程序可以通过平台的应用程序市场(例如 iPhone App Store)交付给最终用户并提供原生用户体验,就像原生应用程序那样。


混合应用程序的基本原理

本质上,混合应用程序是包含嵌入式 web 内容的原生应用程序。混合应用程序的 UI 通常结合使用原生视图和 web 视图 — 可以显示本地或远程 web 内容的视图。一个典型的混合应用程序示例是应用程序中的主内容区域由一个单一的 web 视图构成。可以使用特定于平台的 CSS 来向 web 内容提供原生观感,同时可以使用原生视图来呈现使用 web 内容和 CSS 难以构造的平台特有控件,比如菜单和选项卡栏。

注意,没有关于使用原生视图和 web 内容的固定规则。有些场景允许主要使用 web 内容,而另一些场景可能需要大量使用原生视图。

在图 1 中的左边,可以看到在 Mobile Safari 中显示的 Madisons Mobile starter store(蓝色的 web 内容)。右边显示了混合应用程序中的相同内容(web 内容为蓝色,原生内容为红色)。

图 1. 混合 vs. 移动 web 应用程序的用户界面
混合 vs. 移动 web 应用程序的用户界面

图 2 比较了不同移动平台上的 UI 元素。

图 2. 比较不同平台上的 UI 元素
比较不同平台上的 UI 元素

原生代码和嵌入的 web 内容的交互

在许多移动平台上,应用程序的原生代码可以执行嵌入的 web 内容中的 JavaScript 函数。这允许原生代码从 web 内容获取信息(比如一个 HTML 表单输入的值)并触发 web 内容中的动作(比如,隐藏/显示一个 HTML 元素)。相反,原生代码可以截获由嵌入的 web 内容发起的请求并代表 web 内容执行原生函数。这实际上是原生代码和嵌入的 web 内容之间的一个双向桥梁,通常用于向嵌入的 web 内容提供 web 内容通常无法访问的 web 功能的访问权。

一个例子是向嵌入的 web 内容提供对本地地址簿的访问权。通常, 移动设备的本地地址簿不允许 web 内容访问。但是,通过原生代码和 web 内容之间的这个双向桥梁,混合应用程序的嵌入 web 内容就能以 web 内容的名义请求原生代码显示本地地址簿。用户在本地地址簿中执行完需要的操作(例如,选择一个电子邮件地址)后, 原生代码可以执行一个 JavaScript 回调函数来将控件和数据传回嵌入的 web 内容(见图 3)。

图 3. 原生代码和嵌入的 web 内容之间的交互
原生代码和嵌入的 web 内容之间的交互

如果针对多个移动平台,那么使 JavaScript 和原生代码之间的这个桥梁在所有平台之间尽可能相似肯定有好处。这允许跨多个移动平台使用相同的 web 内容(因此也是相同的 JavaScript)。开源 PhoneGap 就是这样一个开发框架,尽管它没有在本文讨论的原型中使用。

在下面的用例研究中,我们将介绍如何应用这些模式来为 WebSphere Commerce 构建 iPhone 和 Android 应用程序。


用例研究 1. 使用混合模型为 WebSphere Commerce 构建一个 iPhone 应用程序

我们的第一个用例研究是一个 Madisons iPhone 应用程序,它已作为一个概念证明构建。这个应用程序本质上是作为一个具有原生观感的 iPhone 应用程序打包的 Madisons Mobile starter store, 它包含移动 web 应用程序中没有的一些附加特性,其中包括:

  • 包含一些交互式电子营销点的增强主屏幕
  • 具有地理位置支持和集成地图视图的增强商店定位器
  • 一个可翻页(swipeable)的产品细节屏幕
  • 与 iPhone 地址簿的集成

图 4 显示应用程序的主屏幕。可以看到,交互式电子营销点占据了主屏幕的主要内容区域,还可以看到应用程序的原生导航控件。

图 4. Madisons iPhone 应用程序 - 主屏幕
Madisons iPhone 应用程序 - 主屏幕

图 5 展示了应用程序的目录浏览流。您看到一位购物者如何导航到一个产品并将其添加到关注列表或购物车。

图 5. Madisons iPhone 应用程序 - 目录浏览
Madisons iPhone 应用程序 - 目录浏览

图 6 展示了应用程序的商店定位器流。您看到一位购物者如何使用应用程序的商店定位器和地理位置支持来定位附近的商店。

图 6. Madisons iPhone 应用程序 - 商店定位器
Madisons iPhone 应用程序 - 商店定位器

图 7 展示了应用程序的结账流。您看到一位购物者如何使用应用程序进行店内采购。

图 7. Madisons iPhone 应用程序 - 购物车和结账
Madisons iPhone 应用程序 - 购物车和结账

高级设计

我们将这个 Madisons iPhone 应用程序设计为一个广泛使用导航和选项卡条界面的生产力应用程序。它的屏幕流与 Madisons Mobile starter store 的页面流基本相同。应用程序被组织为一些选项卡,每个选项卡对应 starter store 的一个部分。每个选项卡下方的屏幕通过一个导航界面管理, 替代了 starter store 中的标题栏和浏览路径记录。应用程序的主内容区域由一些 UIWebView 实例组成,这些实例使用一个特定于 iPhone 的 CSS 显示一些远程 Madisons Mobile 页面,而导航和选项卡栏通过原生视图和控件呈现。

与所有 iPhone 应用程序一样,Madisons iPhone 应用程序使用 iPhone SDK 予以开发。iPhone SDK 包含一组开发工具来编写和测试 iPhone 应用程序,主要包括:

  • Xcode:这是集成开发环境。
  • Interface builder:这是以界面构建程序(NIB)文件格式创建和更新 iPhone 应用程序的 UI 的工具。
  • iPhone simulator:这是在一个模拟设备上运行和测试 iPhone 应用程序的工具。

注意,下面的小节假设您对 iPhone 应用程序开发有基本的了解。要了解 iPhone 应用程序开发的细节,请参阅 iPhone Dev CenteriPhone Human Interface Guidelines


服务器端更改

设备检测

我们使用 WebSphere Commerce 的设备检测功能来识别来自 Madisons iPhone 的请求。我们在 WC_eardir/xml/config/com.ibm.commerce.foundation-ext 目录中创建一个 wc-devices.xml 文件来将 UIWebView 用户代理映射到一个设备格式 ID。然后,我们使用这个设备格式 ID 来:

  • 将来自应用程序的视图请求映射到 Struts 配置条目中的 Madisons Mobile JSP 文件。
  • 在是包含特定于 iPhone 的 CSS 还是执行 JSP 文件中特定于 iPhone 的代码之间切换。

清单 1 展示将来自应用程序的请求映射到适当的设备格式的自定义 wc-devices.xml 文件。参阅 表 1 了解更改后的文件的位置。

清单 1. 将 UIWebView 用户代理映射到设备格式 ID -12 的 wc-devices.xml
<_config:Devices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.ibm.com/xmlns/prod/commerce/foundation/config \
 ../xsd/wc-devices.xsd"
 xmlns:_config="http://www.ibm.com/xmlns/prod/commerce/foundation/config">

 <_config:DeviceGroup internalID="-11" channelID="-6">
  <_config:Device name="BlackBerry Bold 9000" userAgentPattern="BlackBerry9000.*"/>
  <_config:Device name="BlackBerry Storm 9530" userAgentPattern="BlackBerry9500.*"/>
  <_config:Device name="BlackBerry Curve 8320" userAgentPattern="BlackBerry8320.*"/>
  <_config:Device name="HTC S740" userAgentPattern=".*MSIEMobile.*"/>
  <_config:Device name="Nokia S60"
   userAgentPattern=".*SymbianOS.*Series60/3.1.*Nokia3250.*" />
  <_config:Device name="Nokia N97"
   userAgentPattern=".*SymbianOS.*Series60/5.0.*Nokia3250.*" />
  <!-- Maps the Mobile Safari user agent to device format ID -11. -->
  <_config:Device name="Apple iPhone"
   userAgentPattern=".*iPhone.*Safari.*"/>
 </_config:DeviceGroup>

 <!--
  Maps the UIWebView user agent to device format ID -12.
  The UIWebView user agent is different from the Mobile Safari user agent in that the
  UIWebView user agent does NOT contain the substring "Safari".
 -->
 <_config:DeviceGroup internalID="-12" channelID="-6" >
  <_config:Device name="Apple iPhone Native App"
   userAgentPattern=".*iPhone.*"/>
 </_config:DeviceGroup>

</_config:Devices>

清单 2 展示添加到 struts-config-ext.xml 的自定义 Struts 配置条目。这个文件将来自我们的应用程序的请求映射到 Madisons Mobile starter store。

清单 2. 将来自设备格式 ID -12 的视图请求映射到 Madisons Mobile starter store JSP 文件的 Struts 配置条目节选
<forward className="com.ibm.commerce.struts.ECActionForward"
 name="ChangePassword/10000001/-12"
 path="/mobile/UserArea/AccountSection/PasswordSubsection/PasswordUpdateForm.jsp"/>
<forward className="com.ibm.commerce.struts.ECActionForward"
 name="ReLogonFormView/10000001/-12"
 path="/mobile/UserArea/AccountSection/LogonSubsection/UserTimeoutView.jsp"/>
<forward className="com.ibm.commerce.struts.ECActionForward"
 name="RememberMeLogonFormView/10000001/-12"
 path="/mobile/UserArea/AccountSection/LogonSubsection/logon.jsp"/>
<forward className="com.ibm.commerce.struts.ECActionForward"
 name="AccessControlErrorView/10000001/-12"
 path="/mobile/GenericError.jsp"/>

清单 3 展示对 JSTLEnvironmentSetup.jspf 文件进行的更改,以支持特定于我们的应用程序的 web 内容增强,特别是特定于 iPhone 的 CSS。

清单 3. 基于设备格式 ID 更改 CSS 路径的 JSTLEnvironmentSetup.jspf 代码
<%-- Set variables for device specific rendering --%>
<c:if test="${EC_deviceAdapter.deviceFormatId == -12}">
 <c:set var="_iPhoneNativeApp" value="true"/>
 <c:set var="mobileBasePath" value="mobile/iPhone"/>
 <c:set var="pageMax1" value="500"/>
 <c:set var="pageMax2" value="500"/>
</c:if>
<c:set var="cssPath" value="${jspStoreImgDir}${mobileBasePath}/${vfileStylesheet}"/>

注意,我们已经将这些更改集成到 WebSphere Commerce V7 Feature Pack 1 中。如果您已安装 Feature Pack 1 并发布了 Madisons Mobile 增强商店归档,那么 Madisons Mobile starter store 将使用这些更改更新。

特定于 iPhone 的 CSS

我们使用了一个特定于 iPhone 的 CSS(清单 4)来向嵌入的 web 内容提供原生观感。我们还使用 CSS 来:

  • 禁用默认高亮颜色,因为我们使用 :active 伪类来突出活动项目。
  • 禁用文本选择和复制粘贴调出。

注意 CSS 使用许多带有 -webkit- 前缀的属性,这是 CSS3 属性的 WebKit 实现。大多数这些属性可以在 iPhone 和 Android 上工作,因为这两个平台都使用 WebKit 作为 web 内容的渲染引擎。

清单 4. 特定于 iPhone 的 CSS 节选
/* Defines the look of section headers */
div.heading_container {
 /* Gives the background a slight gradient */
 background-image:
  -webkit-gradient(linear, left top, left bottom, from(white), to(#e0e0e0));
 border-color: #e0e0e0;
 border-style: solid;
 border-width: 1px 0px 0px 0px;
 margin: 0px;
 padding: 5px 10px;
}

/* Defines the look of section titles */
div.heading_container > h2 {
 margin: 0px;
 padding: 0px;
 color: gray;
 font-weight: bold;
 font-size: 14px;
 overflow: hidden;
 /* Truncates the title and adds a trailing ellipsis (...) if it overflows */
 text-overflow: ellipsis;
 white-space : nowrap;
 /* Gives the title an embossed look */
 text-shadow: white 0px 1px 0px;
}

div.content_box > ul {
 margin: 0px;
 padding: 0px;
 list-style: none;
}

/* Hides the bullets (>>) */
span.bullet {
	display: none
}

/* Gives lists the look of edge-to-edge tables */
div.content_box > ul > li {
 background-color: white;
 border-color: #e0e0e0;
 border-style: solid;
 border-width: 0px 0px 1px 0px;
 margin: 0px;
 padding: 10px;
}

/* Gives clickable list items the look of table items */
div.content_box > ul > li > a {
 /* Adds trailing chevrons (>) to the table items */
 background-image: url('../images/chevron.png'), url('../images/link.png');
 background-position: center right, top left;
 background-repeat: no-repeat, repeat-x;
 display: block;
 margin: -10px;
 padding: 10px 20px 10px 10px;
 color: black;
 font-size: 17px;
 font-weight: bold;
 overflow: hidden;
 /* Truncates the text and adds a trailing ellipsis (...) if it overflows */
 text-decoration: none;
 text-overflow: ellipsis;
 white-space : nowrap;
 -webkit-background-size: auto auto, auto 100%;
}

/* Defines the look of table items when tapped */
a.arrow:active {
 background-color: #ff5000;
 /* Reverses the color of the chevron and text */
 background-image: url('../images/chevron_white.png'), url('../images/link.png');
 color: white;
}

* {
 /* Disables the default tap highlight color */
 -webkit-tap-highlight-color: rgba(0,0,0,0);
 /* Disable text selection and the copy-and-paste callout */
 -webkit-touch-callout: none;
 -webkit-user-select: none;
}

图 8 展示 HTML 列示前后。

图 8. HTML 列示,之前和之后
HTML 列示,之前和之后

同样,特定于 iPhone 的 CSS 包含在 Feature Pack 1 Madisons Mobile enhancements store 归档中。它位于 store 目录下的 mobile/iPhone/css 目录中,在 store 归档发布之后。

表 1 列示了服务器端更改的摘要。

表 1. 服务器端更改摘要
路径说明
WC_eardir/xml/config/com.ibm.commerce.foundation/wc-devices.xmliPhone (UIWebView) 用户代理的映射
WC_eardir/Stores.war/WEB-INF/struts-config-ext.xmliPhone 的 Struts 配置条目
WC_eardir/Stores.war/Madisons/mobile/include/JSTLEnvironmentSetup.jspf为 iPhone 设置 CSS 和样式路径
WC_eardir/Stores.war/Madisons/mobile/iPhone/css/common1_1.css特定于 iPhone 的主 CSS
WC_eardir/Stores.war/Madisons/mobile/iPhone/css/general.css
WC_eardir/Stores.war/Madisons/mobile/iPhone/css/table.css
...
特定于 iPhone 的主 CSS 包含的其他 CSSs
WC_eardir/Stores.war/Madisons/mobile/iPhone/images/chevron.png
WC_eardir/Stores.war/Madisons/mobile/iPhone/images/chevron_white.png
...
特定于 iPhone 的 CSS 图像
WC_eardir/Stores.war/Madisons/mobile/iPhone/include/styles/style1/CachedHeaderDisplay.jspiPhone 的替代页面
WC_eardir/Stores.war/Madisons/mobile/iPhone/include/styles/style1/CachedFooterDisplay.jspiPhone 的替代页面脚注

应用程序代理和视图层级

我们使用 Interface Builder 在应用程序的主 NIB 文件(MainWindow.xib)中定义 Madisons iPhone 应用程序的视图层级。它由一组通过原生导航和选项卡栏界面管理的视图组成 — 通过 UINavigationControllerUITabBarController 控制的合成视图。视图层级顶部是一个选项卡栏界面,位于应用程序的 UIWindow 实例正下方。选项卡栏界面由一个 UITabBarController 实例控制,该实例构造和管理选项卡栏界面下方的视图,包括选项卡栏及其选项卡,每个选项卡都有一个由导航界面管理的视图堆栈。每个导航界面都由一个 UINavigationController 实例控制,该实例构造和管理导航界面下方的视图,包括导航栏及其视图堆栈(参见表 2)。

表 2. MainWindow.xib 的结构

  • UITabBarController - 控制选项卡栏界面
    • UITabBar
    • UINavigationController - 控制主选项卡的导航界面
      • UINavigationBar
      • WebViewController - 控制主页,例如主选项卡导航界面的根视图
        • UINavigationItem
      • UITabBarItem - 定义主选项卡的标题/图像
    • UINavigationController - 控制商店定位器选项卡的导航界面
      • ...
      • ...
    • UINavigationController - 控制关注列表选项卡的导航界面
      • ...
      • ...
    • ...

如图 9 所示,每个导航界面的顶级视图是一个由视图控制器(WebViewController 类)控制的视图。这个视图在其自己的 NIB 文件(WebViewController.xib)中定义,由一个 UIWebView 实例和一个 UIActivityIndicatorView 实例组成。当应用程序启动时,应用程序代理指导每个 WebViewController 实例加载每个选项卡的初始 web 内容,比如 Home 选项卡的 http://host/webapp/wcs/stores/servlet/mIndex_storeId_catalogId_langId

图 9. 应用程序的视图层级
应用程序的视图层级

清单 5 展示 TestUIShellAppDelegate 类中用于指定每个选项卡的根视图的初始 URL 的代码。

清单 5. TestUIShellAppDelegate - applicationDidFinishLaunching
- (void)applicationDidFinishLaunching:(UIApplication *)application {
 
 NSString *langId = NSLocalizedString(@"langId", @"");
 
 WebViewController *webViewController
  = (WebViewController *)homeTabNavigationController.topViewController;
 NSString *urlString = [NSString
  stringWithFormat:@"http://%@/webapp/wcs/stores/servlet/mIndex_%@_%@_%@",
  host, storeId, catalogId, langId];
 NSURLRequest *request = [NSURLRequest
  requestWithURL:[NSURL URLWithString:urlString]];
 webViewController.request = request;
 
 webViewController
  = (WebViewController *)storeLocatorTabNavigationController
  .topViewController;
 urlString = [NSString
  stringWithFormat:@"http://%@/webapp/wcs/stores/servlet/mStoreLocatorView?
   catalogId=%@ \
  &fromPage=&storeId=%@&langId=%@",
  host, catalogId, storeId, langId];
 request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
 webViewController.request = request;
 
 // Similar code for the other tabs...
 
 [window addSubview:tabBarController.view];
 [window makeKeyAndVisible];
 
}

清单 6 展示 WebViewController 类中用于加载 URL 请求的代码。

清单 6. WebViewController - viewDidLoad
// Implement viewDidLoad to do additional setup after loading the view, typically from a
// nib.
// The view is loaded from the NIB file when the view first appears,
// or when it needs to reappear after being unloaded due to memory warning
- (void)viewDidLoad {
 [super viewDidLoad];
 // Load the request
 [webView loadRequest:request];
}

导航界面

UIWebView instances 之间的导航(例如,用户通过点击一个类别从主屏幕导航到类别屏幕)由 UINavigationControllerWebViewController 实例管理。总之,应用程序视图层级中的每个 UIWebView 实例都由一个 WebViewController 实例控制。WebViewController 实例还被配置为 UIWebView 实例的代理(WebViewController 类实现 UIWebViewDelegate 协议),允许 WebViewController 实例截获由 UIWebView 实例的 web 内容发出的 URL 请求。

例如,当用户点击主屏幕上的一个类别(见图 10)时,UIWebView 实例的 web 内容(Madisons Mobile 主页)发出一个 URL 请求来加载那个类别页面。WebViewController 实例充当 UIWebView 实例的代理,截获 URL 请求,将一个新 WebViewController 实例推到 UINavigationController 实例的导航堆栈上,并提示新的 WebViewController 实例加载其 UIWebView 实例中的 URL 请求。

图 10. 导航界面
导航界面

加载开始时,将显示 UIActivityIndicatorView 实例,表明屏幕正在加载。加载结束时,UIActivityIndicatorView 实例隐藏,通过 stringByEvaluatingJavaScriptFromString: 方法从 web 内容提取文档标题并将其设置为屏幕标题。导航栏上的后退按钮被自动编程为受到点击时从导航堆栈自动弹出顶部视图控制器。

清单 7 展示 WebViewController 类中用于截获并处理普通 URL 请求的代码。

清单 7. WebViewController.m 节选 - 处理普通 URL 请求
- (BOOL)webView:(UIWebView *)aWebView
 shouldStartLoadWithRequest:(NSURLRequest *)aRequest
 navigationType:(UIWebViewNavigationType)navigationType {
 if (webView.request == nil || [activityIndicatorView isAnimating]) {
  // Continue loading if it is the initial request of the web view or if it is 
  // a redirect (in which case loading is not "finished")
  return YES;
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else {
  // Push a new WebViewController instance onto the navigation stack and use it 
  // to load the request
  WebViewController *newWebViewController
   = [[WebViewController alloc] initWithNibName:@"WebViewController" bundle:nil];
  [webViewController.navigationController
   pushViewController:newWebViewController animated:YES];
  [newWebViewController release];
  return NO;
 }
 return NO;
}

- (void)webViewDidStartLoad:(UIWebView *)aWebView {
 webView.hidden = YES;
 [activityIndicatorView startAnimating];
} 

- (void)webViewDidFinishLoad:(UIWebView *)aWebView {
 // Extract the document title from the web content and set it as the title 
 // of the view
 if (self.navigationItem.title == nil) {
  self.navigationItem.title
   = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
 }
 webView.hidden = NO;
 [activityIndicatorView stopAnimating];
}

可翻页的产品细节屏幕

为增强产品细节屏幕的可用性,我们定义了一个自定义视图控制器(ProductScrollViewController 类)来支持用户通过翻页在产品之间水平滚动。它的视图包含一个 UIScrollView 实例,这是一个可滚动的视图,其中产品细节并排显示;还有一个 UIPageControl 实例,充当页面指示器。

当用户点击子类别屏幕(见图 11)上的一个产品时,UIWebView 实例(Mobile 子类别页面)上的 web 内容将发出一个 URL 请求来加载产品细节页面。WebViewController 实例充当 UIWebView 实例的代理,截获 URL 请求,然后将一个新 ProductScrollViewController 实例而不是典型的 WebViewController 实例推到导航堆栈上。

图 11. 可翻页的产品细节屏幕
可翻页的产品细节屏幕

当它的视图加载完一个空 UIScrollView 实例后,ProductScrollViewController 实例开始使用 WebViewController 实例加载产品细节视图并将它们添加为 UIScrollView 实例下方的子视图。ProductScrollViewController 实例首先加载当前产品的视图,以及前一个和后一个产品的视图。当用户滚动 UIScrollView 实例时,ProductScrollViewController 实例加载新产品的视图(如果还没有加载)以及前一个和后一个产品的视图。

清单展示 ProductScrollViewController 类中用于处理页面滚动的代码。

清单 8. ProductScrollViewController.m 节选
- (void)viewDidLoad {
	
 [super viewDidLoad];
 
 pageControl.numberOfPages = [urls count];
 pageControl.currentPage = currentPage;
 
 scrollView.pagingEnabled = YES;
 scrollView.contentSize
  = CGSizeMake(scrollView.frame.size.width * pageControl.numberOfPages,
  scrollView.frame.size.height);
 
 scrollView.showsHorizontalScrollIndicator = NO;
 scrollView.showsVerticalScrollIndicator = NO;
 scrollView.scrollsToTop = NO;
 scrollView.delegate = self;
 
 [scrollView setContentOffset:CGPointMake(scrollView.frame.size.width * 
  currentPage, 0)];
 [scrollView setBackgroundColor:[UIColor blackColor]];
 
 [self loadScrollViewWithPage:pageControl.currentPage - 1];
 [self loadScrollViewWithPage:pageControl.currentPage];
 [self loadScrollViewWithPage:pageControl.currentPage + 1];
}

- (void)loadScrollViewWithPage:(int)page {
 if (page < 0 || page >= pageControl.numberOfPages) {
  return;
 }
 WebViewController *controller = [viewControllers objectAtIndex:page];
 if (controller == [NSNull null]) {
  controller = [[WebViewController alloc] initWithNibName:
    @"WebViewController"
   bundle:[NSBundle mainBundle]];
  controller.navigationController
   = (UINavigationController *)self.parentViewController;
  controller.request = [NSURLRequest requestWithURL:[URLS 
   objectAtIndex:page]];
  [viewControllers replaceObjectAtIndex:page withObject:controller];
  [controller release];
 }
 if (nil == controller.view.superview) {
  CGRect frame = scrollView.frame;
  frame.origin.x = frame.size.width * page;
  frame.origin.y = 0;
  controller.view.frame = frame;
  [scrollView addSubview:controller.view];
 }
}

- (void)scrollViewDidScroll:(UIScrollView *)sender {
 if (pageControlUsed) {
  // do nothing - the scroll was initiated from the page control,
  // not the user dragging
  return;
 }
 
 // Switch the indicator when more than 50% of
 // the previous/next page is visible
 CGFloat pageWidth = scrollView.frame.size.width;
 int page = floor((scrollView.contentOffset.x - pageWidth / 2) / 
  pageWidth) + 1; 
 pageControl.currentPage = page;

 [self loadScrollViewWithPage:page - 1];
 [self loadScrollViewWithPage:page];
 [self loadScrollViewWithPage:page + 1];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
 pageControlUsed = NO;
}

- (IBAction)changePage:(id)sender {
 int page = pageControl.currentPage;
 [self loadScrollViewWithPage:page - 1];
 [self loadScrollViewWithPage:page];
 [self loadScrollViewWithPage:page + 1];
 
 CGRect frame = scrollView.frame;
 frame.origin.x = frame.size.width * page;
 frame.origin.y = 0;
 [scrollView scrollRectToVisible:frame animated:YES];
 // Scroll initiated by page control
 pageControlUsed = YES;
}

集成 iPhone 地址簿

与针对 web 内容执行 JavaScript 代码的能力配合使用的技术也被应用程序用于允许 Madisons Mobile 电子邮件关注列表页面访问 iPhone 地址簿中的电子邮件地址。一个包含特制的 URL 的链接被添加到电子邮件关注列表页面,比如 testuishell:///emailpicker#callbackHandler。协议 testuishell:// 用于区分它和普通 HTTP 请求,而片段 callbackHandler 是动作结束时执行的 JavaScript 回调函数的名称。

注意:除了语法正确和区别于普通 HTTP 请求外,这个特制的 URL(包括它的协议)的格式没有任何特别要求。

图 2. 集成 iPhone 地址簿
集成 iPhone 地址簿

当用户点击链接(见图 12)时,页面发出这个特制的 URL 请求。当 WebViewController 实例截获这个 URL 请求时,它创建一个新的 ABPeoplePickerNavigationController 实例并使用它的 presentModalViewController:animated: 方法来以模态方式显示 iPhone 地址簿。一旦用户选择一个电子邮件地址,消息 peoplePickerNavigationController:shouldContinueAfterSelectingPerson:property:identifier: 被发送到 WebViewController 实例,该实例被设置为 ABPeoplePickerNavigationController 实例的代理。接收到这条消息时,WebViewController 实例以这个电子邮件地址作为参数调用 JavaScript 回调函数 callbackHandler。在电子邮件关注清单页面上,JavaScript 函数 callbackHandler() 使用这个作为参数传入的电子邮件地址来填充电子邮件地址输入字段。

清单 9 展示了 WebViewController 类中用于截获 testuishell:///emailpicker 请求并显示 iPhone 地址簿的代码。它还展示了当用户选择这个电子邮件地址时这个类中用于调用 JavaScript 回调函数的代码。

清单 9. WebViewController.m - 集成 iPhone 地址簿
- (BOOL)webView:(UIWebView *)aWebView
 shouldStartLoadWithRequest:(NSURLRequest *)aRequest
 navigationType:(UIWebViewNavigationType)navigationType {
 if (webView.request == nil || [activityIndicatorView isAnimating]) {
  // Continue loading if it is the initial request of the web view or if it is 
  // a redirect (in which case loading is not "finished")
  return YES;
 }
 else if ([aRequest.URL.scheme isEqualToString:@"testuishell"]) {
  if ([aRequest.URL.path isEqualToString:@"/emailpicker"]) {
   // Code to handle e-mail address selection
   // Remember the request
   self.api = aRequest.URL.path;
   // Extract the JavaScript callback function from the URL and remember it
   self.callbackHandler = aRequest.URL.fragment;
   // Create the address book view controller...
   ABPeoplePickerNavigationController *peoplePickerNavigationController
    = [[ABPeoplePickerNavigationController alloc] init];
   peoplePickerNavigationController.displayedProperties
    = [NSArray arrayWithObject:[NSNumber numberWithInt:kABPersonEmailProperty]];
   peoplePickerNavigationController.peoplePickerDelegate = self;
   // ... and present it modally
   [self presentModalViewController:peoplePickerNavigationController 
     animated:YES];
   [peoplePickerNavigationController release];
  }
  else if (...) {
   // Code to handle physical address selection
  }
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else {
  // Code to intercept normal URL requests
  // See listing 7
 }
 return NO;
}

- (void)peoplePickerNavigationControllerDidCancel:
 (ABPeoplePickerNavigationController *)peoplePicker {
 [self dismissModalViewControllerAnimated:YES];
}

- (BOOL)peoplePickerNavigationController:
 (ABPeoplePickerNavigationController *)peoplePicker
 shouldContinueAfterSelectingPerson:(ABRecordRef)person {
 if ([api isEqualToString:@"/emailpicker"]) {
  // Continue to show the address book until an e-mail address has been 
  // selected
  return YES;
 }
 else if (...) {
  // Code to handle physical address selection
 }
}

- (BOOL)peoplePickerNavigationController:
 (ABPeoplePickerNavigationController *)peoplePicker
  shouldContinueAfterSelectingPerson:(ABRecordRef)person
  property:(ABPropertyID)property
  identifier:(ABMultiValueIdentifier)identifier {
 if ([api isEqualToString:@"/emailpicker"]) {
  // E-mail address has been selected, extract e-mail address from address book
  ABMultiValueRef emails = ABRecordCopyValue(person, property);
  CFIndex index = ABMultiValueGetIndexForIdentifier(emails, identifier);
  CFStringRef email = ABMultiValueCopyValueAtIndex(emails, index);
  // Construct the JavaScript callback (i.e. callback function + argument)
  NSString *callback = [NSString stringWithFormat:@"%@('%@')", callbackHandler, 
   email];
  [webView stringByEvaluatingJavaScriptFromString:callback];
  CFRelease(email);
  CFRelease(emails);
  // Dismiss the address book view controller
  [self dismissModalViewControllerAnimated:YES];
 }
 return NO;
}

清单 10 展示了添加到 EmailWishList.jsp 文件来触发原生代码的按钮。它还展示了添加到这个文件来处理来自原生代码的回调的 JavaScript 回调函数。

清单 10. EmailWishlist.jsp - 集成 iPhone 地址簿
<c:if test="${_iPhoneNativeApp == true}">
 <a class="button"
  href="testuishell:///emailpicker#emailPickerCallbackHandler">
  Address Book
 </a>
 <script type="text/javascript">
  function emailPickerCallbackHandler(email) {
   document.getElementById("recipient").value = email;
  }
 </script>
</c:if>

集成的地图视图

可以使用用于展示可滚动的产品细节屏幕的 URL 请求截获技术来使用原生视图替代 web 内容,就像商店地图中所做的那样。Madisons Mobile 商店定位器被定制为提供到 Google® Maps 的商店地图链接。在 Mobile Safari 中,这样的 URL 请求被路由到显示商店地图的原生 Maps 应用程序。这种方法也适用于嵌入的 web 内容,但这会导致退出应用程序并启动 Maps 应用程序。为阻止这种情况发生,应用程序截获这样一个 URL 请求,但使用一个 MKMapView 实例和一个自定义 UIViewController 实例(MapViewController 类,见图 13)来显示商店地图。

图 13. 集成的地图视图
集成的地图视图

清单 11 展示了 WebViewController 类中用于截获并处理通常路由到原生 Maps 应用程序的请求的代码。

清单 11. WebViewController.m - 集成的地图视图
- (BOOL)webView:(UIWebView *)aWebView
 shouldStartLoadWithRequest:(NSURLRequest *)aRequest
 navigationType:(UIWebViewNavigationType)navigationType {
 if (webView.request == nil || [activityIndicatorView isAnimating]) {
  // Continue loading if it is the initial request of the web view or if 
  // it is a redirect (in which case loading is not "finished")
  return YES;
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else if ([aRequest.URL.host isEqualToString:@"maps.google.com"]) {
  // Code to intercept Google Maps URL requests
  // Extract latitude, longitude and query (i.e. the address) from 
  // the URL
  NSString *ll = nil;
  NSString *q = nil;
  NSString *query = aRequest.URL.query;
  NSArray *parameters = [query componentsSeparatedByString:@"&"];
  for (NSString *parameter in parameters) {
   if ([parameter hasPrefix:@"ll="]) {
    ll = [[[parameter substringFromIndex:3]
     stringByReplacingOccurrencesOfString:@"+" withString:@" "]
     stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
   }
   if ([parameter hasPrefix:@"q="]) {
    q = [[[parameter substringFromIndex:2]
     stringByReplacingOccurrencesOfString:@"+" withString:@" "]
     stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
   }
  }
  NSArray *latitudeAndLongitude = [ll componentsSeparatedByString:@","];
  NSArray *addressAndName = [q componentsSeparatedByString:@" ("];
  // Create the map view controller
  MapViewController *mapViewController
   = [[MapViewController alloc] initWithNibName:@"MapViewController"
   bundle:[NSBundle mainBundle]];
  // Create the annotation pin
  MyAnnotation *annotation = [[MyAnnotation alloc] init];
  CLLocationCoordinate2D coordinate;
  coordinate.latitude = [[latitudeAndLongitude objectAtIndex:0] 
   doubleValue];
  coordinate.longitude = [[latitudeAndLongitude objectAtIndex:1] 
   doubleValue];
  annotation.coordinate = coordinate;
  annotation.title = [addressAndName objectAtIndex:1];
  annotation.title
   = [annotation.title substringToIndex:annotation.title.length - 1];
  annotation.subtitle = [addressAndName objectAtIndex:0];
  mapViewController.annotation = annotation;
  [annotation release];
  // Push the map view controller onto the navigation stack
  UINavigationController *theNavigationController
   = (UINavigationController *)self.parentViewController;
  if (theNavigationController == nil) {
   theNavigationController = self.navigationController;
  }
  [theNavigationController pushViewController:mapViewController
   animated:YES];
  [mapViewController release];
  return NO;
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else {
  // Code to intercept normal URL requests
  // See listing 7
 }
 return NO;
}

地理定位支持

从 iPhone OS 3 开始,HTML5 地理定位 API 受到 Mobile Safari 和 UIWebView 的支持。应用程序使用这个 API 而不是原生代码来支持商店地址的地理定位(见图 14)。

图 14. 地理定位支持
地理定位支持

清单 12 展示了用于获取地理定位信息的 JavaScript 代码。

清单 12. 执行地理定位的样例 JavaScript 代码
function captureCurrentLocation() {
 // Get location no more than 10 minutes old. 600000 ms = 10 minutes.
 navigator.geolocation.getCurrentPosition(showLocation, showError,
  {enableHighAccuracy:true,maximumAge:600000});
}

function showLocation(position) {
 var fromForm = document.getElementById("store_locator_gps_form");
 var geoCodeLatitude = fromForm.geoCodeLatitude;
 var geoCodeLongitude = fromForm.geoCodeLongitude; 
 geoCodeLatitude.value = position.coords.latitude;
 geoCodeLongitude.value = position.coords.longitude;
 fromForm.action = "mStoreLocatorResultView";
 fromForm.submit();
}

针对 iPads 改写应用程序

这个混合模型也适用于构建 iPad® 应用程序,使用的技术也相同。尽管 iPhone 应用程序可以在 iPads 上原样运行,但最好创建特定于 iPad 的应用程序版本(比如 “Madisons HD”),或者增强现有应用程序来同时支持 iPhones 和 iPads,从而利用新平台的优势。注意,iPad 拥有自己的人工界面指南,与 iPhone 的指南区别很大。您需要在应用程序设计中考虑这个因素 —— 例如,iPad HIG 强调减少全屏过渡,您可以通过增加分屏和弹出窗口来实现这一点。为此,可以使用前面介绍的 设备检测机制 来区分来自 iPhones 和来自 iPads 的请求。


用例研究 2. 构建一个 Android 应用程序

Android 是一个开源软件堆栈,已经在 Open Handset Alliance 的指导下针对移动设备优化。它基于一个 Linux 内核版本,并提供一个软件开发工具包,该工具包基于一个作为开发语言的 Java™ 版本。它包含一组库和几个开发实用程序,使用开源 WebKit 引擎作为其浏览器的基础,并在应用程序中提供嵌入的 web 视图(iPhone 也使用这种方法,还使用 WebKit)。

一个应用程序通常包含一个或多个 “活动”,每个活动对应一个窗口或对话框。屏幕布局由 Java 代码动态创建,或由 XML 静态描述。Android 中的一个关键概念是 “意图”,这是一条系统消息。应用程序可能注册为接收某种类型的意图(比如来电呼叫),且可能会反过来生成一些意图以便其他应用程序捕获。应用程序模型还包含 “内容提供者”,用于提供对存储在设备上的数据的访问权;以及 “服务”,服务通常在后台运行,用于监控或处理各类事件。 Android 应用程序模型鼓励应用程序、内容提供者和可以互操作的服务的一个松散联盟,以便使一个应用程序轻松利用设备上可用的其他应用程序、内容和服务。


高级设计

本文描述的混合方法涉及创建一个用 Java 编写的独立应用程序,但该应用程序包含用户将作为一个 web 视图看到的大部分内容,这些内容通过经过最小定制的 WebSphere Commerce 服务器服务。在需要对一个设备功能的特殊访问权(比如对 GPS 或商店定位器中的其他位置确定机制的访问权)的用例中,服务器提供的 web 页面可能包含调用原生(Java)库函数的 JavaScript 函数,这些库函数反过来获取和返回请求的信息。这种方法暗示存在一个或多个这样的 “桥梁库”,这些桥梁库包含一些可调用 JavaScript 的方法来访问本地设备功能。当然,如果这些特定于 Android 的库被构造来呈现用例研究 1 中描述的特定于 iPhone 的库所呈现的 API,那么这种暗示将立即变得很明显;在大多数情况下,服务器端代码无需更改即可用于这两个平台。


观感

目前为止,移动设备上的绝大部分屏幕固定资产通过 WebView 掌管,WebView 本质上是原生应用程序框架中嵌入的一个 web 页面。适用于 iPhone 的许多注意事项同样也适用 Android 设备,因为它们中的大部分都是支持触摸操作的。这意味着按钮和链接都必须足够大,以便允许通过手指触摸选择。类似地,文本也必须足够大,以便可以轻松阅读。这些显示特性的绝大部分通过由 WebSphere Commerce 服务器提供的 web 页面中嵌入的样式表或 CSS 控制。

一个区别是,尽管 iPhone 模型目前只支持两种屏幕大小,一个用于 iPhone 和 iPod Touch,另一个用于 iPad,但 Android 设备包含的屏幕大小范围更复杂多样。这对于为内容页面选择适当的图像大小,以及选择字体和按钮大小有一定影响。


应用程序菜单

纯 web 应用程序中难以集成的一个典型 Android 传统项目是应用程序菜单,对于大多数设备而言,应用程序菜单通常通过一个专用硬件键激活。因此,提供应用程序菜单的任务被留给原生框架处理。图 15 展示了一个应用程序菜单示例。

图 15. Madisons Android 应用程序 - 应用程序菜单
Madisons Android 应用程序 - 应用程序菜单

清单 13 展示了这里演示的菜单的定义。默认情况下,Android 最多显示 6 个菜单项。如果定义了更多的菜单项,则超出部分由一个标签为 “More” 的项目代替,如图 15 所示。触摸那个选项将显示一个包含额外菜单项的子菜单。

清单 13. 应用程序菜单 XML
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:id="@+id/locator"
  android:title="@string/StoreLocator"
  android:icon="@drawable/ic_menu_compass">
 </item>
 <item android:id="@+id/wishlist"
  android:title="@string/WishList"
  android:icon="@drawable/ic_menu_save">
 </item>
 <item android:id="@+id/cart"
  android:title="@string/ShoppingCart"
  android:icon="@drawable/ic_menu_cart">
 </item>
 <item android:id="@+id/home"
  android:title="@string/Home"
  android:icon="@drawable/ic_menu_home">
 </item>
 <item android:id="@+id/scan"
  android:title="@string/Scan"
  android:icon="@drawable/ic_menu_scan">
 </item>
 <item android:id="@+id/account"
  android:title="@string/MyAccount"
  android:icon="@drawable/ic_menu_account">
 </item>
 <item android:id="@+id/contact"
  android:title="@string/Contact">
 </item>
</menu>

应用程序的原生部分中需要一些代码来处理菜单事件。清单 14 和 15 展示了如何实例化菜单本身。

清单 14. 实例化应用程序菜单
@Override
public boolean onCreateOptionsMenu(Menu menu) {
 new MenuInflater(getApplication()).inflate(R.menu.options, menu);
 return (super.onCreateOptionsMenu(menu));
}
清单 15. 处理菜单事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
 String message = "No option selected";
 
 if (item.getItemId() == R.id.home) {
  // Handle "Home" menu item
  mWebView.loadUrl(homePage);
  return (true);
 } else if (item.getItemId() == R.id.cart) {
  // Handle "Shopping Cart" menu item
  mWebView.loadUrl(cartURL);
  return true;
 } else ...  // Other handlers not shown
 
 Toast.makeText(this, message, Toast.LENGTH_LONG).show();
 return (super.onOptionsItemSelected(item));
}

访问本地地址簿

对 Android 的通讯录的访问的处理方法与前面介绍的 iPhone 方法相同。访问地址簿的实际 JavaScript 在 Android 用例中不同,但这两种原型之间的地址簿界面是相同的,以便由 WebSphere Commerce 服务的页面在两个用例中一致。


条形码扫描

条形码扫描是 Android 的一个设计特征,对于应用程序之间的协作很有用。在这个原生代码示例中,“Scan” 菜单项调用 zxing(“Zebra Crossing”)条形码库方法 IntentIntegrator 来激活 Barcode Scanner 应用程序,打开相机,拍摄条形码(这里是一个 2D QR 码),在设备上解码图像,然后将结果返回应用程序。应用程序将结果解释为一个将在 web 视图中显示的产品页面的 URL。如果由于某种原因用户的设备没有预先安装 Barcode Scanner,这个方法将询问是否需要现在下载并安装它。参见清单 16。

清单 16. 条形码扫描
import com.google.zxing.integration.android.IntentIntegrator;
...
// Barcode scan
private void barcodeScan() {
  IntentIntegrator.initiateScan(MyActivity.this); 
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 switch (requestCode) {
  case IntentIntegrator.REQUEST_CODE: {
   if (resultCode == RESULT_OK) {
    IntentResult scanResult = IntentIntegrator.parseActivityResult(
     requestCode, resultCode, data);
    if (scanResult != null) {
     String qrCode = scanResult.getContents();
     // Process the returned string.
     mWebView.loadUrl(qrCode);
    }
   }
   break;
  }
 }
}

清单 16 中的代码扫描一个 2D QR 条形码,将其解释为一个产品页面的 URL,并从 WebSphere Commerce 服务器解释该页面。图 16 展示了一个典型的 QR 代码和从 WebSphere Commerce 服务器获取的对应产品页面。

图 16. QR 代码和对应的产品页面
QR 代码和对应的产品页面

商店定位器

与 iPhone 版本一样,作为一个选项,允许用户找到相对于设备的当前位置的一个商店很有用。

Android 设备上的浏览器实现同样基于 iPhone(以及几个其他移动平台)使用的开源 WebKit 浏览器引擎。当前的 Android 版本还支持 HTML 5 地理定位功能,因此您无需原生代码即可使用它,就像此前介绍的 iPhone 一样。


结束语

随着移动设备和应用程序,特别是智能手机销售额和高速网络部署的持续快速增长,我们预计移动商务将对消费者发挥日益重要的作用。在这样的环境中,能够为移动消费者提供良好的体验是零售商的一个关键竞争优势。本文简要介绍了一种将移动商务集成为一个多渠道战略的方法。

参考资料

学习

获得产品和技术

讨论

条评论

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=WebSphere
ArticleID=626688
ArticleTitle=使用混合应用程序编程模型为 WebSphere Commerce 构建移动应用程序
publish-date=02142011