目前,几乎所有的网站都宣称 “ 使用 [Browser X] 提供最佳视图”。现代的 Ajax 库,比如 Prototype、Dojo 和 YUI,有效地缩小了 Firefox、Internet Explorer 和 Safari 之间的差距。但是使用 Nokia、Motorola 或者 Apple 手机的人可能不会喜欢浏览器的独立性。即使是最新的呼吁 “支持完整 HTML ” 的移动浏览器也可以从简单更改网络内容中获益。本文将向您展示如何优化 Grails 应用程序,使其适用于移动浏览器。
|
如果您还在疑虑为什么非要使网站便于移动浏览的话,看一看 移动 Web 的使用率正在增长 边栏里面的数字就会明白了。这份全球的统计数字确实让人过目难忘,但热衷移动 Web 源于我个人的兴趣。我在 2007 年春天买了一个 iPhone,那时它刚刚上市。从那以后,我就一直在寻找可以用它来浏览的网站。当然,我可以用它访问任何的网站(只要不是基于 Flash 或者 Java™ applet 的网站,因为它不支持)。问题是,适合在分辨率为 800x600 (或更高)的显示器上显示的内容,在 3.5 英寸的屏幕上显示效果就没有那么好了。
我经常访问的那些带有 UI 的网站,因为它们符合我的手机的特定限制。我的手机会用 m 代替普通站点中传统的 URL www,这就是一个很好的起点。http://m.cnn.com、http://m.yahoo.com 和 http://m.google.com 这样的页面就能在我的手机上显示。有些网站,比如 http://www.twitter.com,则会做出相应的调整,以输出合适的内容:在电脑上浏览,我可以获得全部功能;而在手机上浏览时,则删剪了一些内容,使它刚好适合我的屏幕。我将向您展示如何实现不改变 URL,但提供最佳 UI。
作为一个 Java 开发人员,我已经被 “只写一次,到处运行(Write Once,Run Anywhere)” 的承诺给宠坏了。我甚至从来没有考虑过优化 Java 应用程序使其适合某一特定的操作系统或硬件型号。但是如果是要开发移动 Web 的话,就应该熟悉三种支持不同型号移动设备的主要技术:
- 无线标记语言(Wireless Markup Language 1.x,WML 1.x)
- WML 2.x 或 Mobile Profile(XHTML-MP)
- 针对 iPhone 的 HTML 标记
正如我展示的一样,您可以将 WML 和 XHTML-MP 标记与用 Grails 构建的 Groovy Server Pages(GSPs)结合起来,以生成便于移动的页面。此外,我还会向您展示如何修改 Grails 生成的 HTML,使页面在 iPhone 上显示得更好。
WML 是一种类似于 HTML 的标记语言,但它并不是真正的 HTML(WML 1.0 于 1998 年标准化。WML 1.3 为最新版本)。WML 并无法在 Web 浏览器中查看(至少不借助于仿真器是不行的),同样您也无法在 WML 浏览器中查看 HTML。手机供应商通常都会提供在后台实现 HTML 与 WML 之间相互转换的网关。
|
WML 通过无线访问协议(Wireless Access Protocol,WAP)传输,这和 HTML 通过 HTTP 传输很相似。WAP 和 WML 在临时对话中通常可以相互转换:手机说明书总是会夸耀该手机带有 WAP 浏览器,或者支持 WML 1.x(参阅 参考资料 获得 WML 和 WAP 规范的官方链接)。
如果您针对的是 Research in Motion 的 BlackBerry 用户的话,就得提高关于 WML 的知识了。(BlackBerry 大约占了智能手机市场的 40%,iPhones 和 Windows® Mobile 居于第二和第三位)。虽然很多技术过硬的用户也可以下载真正的 Web 浏览器,比如 Opera Mini(参见 参考资料),但是 BlackBerry 智能手机还是配备了 WAP 浏览器。
如果您一直都在关注 精通 Grails 系列文章的话,那么您可以修改已经熟悉的旅行计划应用程序,使它便于在手机上浏览。在旅行计划应用程序的 web-app 目录中创建一个文件,命名为 testwml.gsp,并输入清单 1 中的静态 WML:
<% response.setContentType("text/vnd.wap.wml") %> <?xml version="1.0"?> <!DOCTYPE wml PUBLIC "-//PHONE.COM//DTD WML 1.1//EN" "http://www.phone.com/dtd/wml11.dtd" > <wml> <card id="f1" title="Flight 1"> <p mode="wrap">From: DEN</p> <p mode="wrap">To: ORD</p> <p mode="wrap">UAL 1234</p> <p mode="wrap">Jun 30, 10:30am</p> <p> <anchor>Next<go href="#f2"/></anchor> </p> </card> <card id="f2" title="Flight 2"> <p mode="wrap">From: ORD</p> <p mode="wrap">To: DEN</p> <p mode="wrap">UAL 9876</p> <p mode="wrap">Jul 02, 1:15pm</p> <p> <anchor>Previous<go href="#f1"/></anchor> </p> </card> </wml> |
您用手机访问 http://www.davisworld.org/testwml.gsp 同样可以看到这个页面。您可能习惯于在 GSP 中查看 HTML。但在这里使用的是 WML。
当从 GSP 中发送出 WML 时,切记要将默认的 MIME 类型 text/html
替换为 text/vnd.wap.wml
,如清单 1 中的第一行所示。如果直接提供静态 WML,那么只需赋予文件一个 WML 扩展名,而不是 GSP 扩展名。大多数 Web 服务器会在这之后自动返回一个正确的 MIME 类型,无需调用 response.setContentType
。在 $TOMCAT_HOME/conf/web.xml 中,您会发现 MIME 针对 WML 文件的映射已经就位。如果您使用的是 Apache HTTPD 的话,那么您可以在 $APACHE_HOME/conf/mime.types 文件中找到类似的 WML 文件的映射。清单 2 向展示了 Tomcat 的 MIME 类型映射:
<mime-mapping> <!-- WML Source --> <extension>wml</extension> <mime-type>text/vnd.wap.wml</mime-type> </mime-mapping> |
回过头来在看一下 清单 1,接下来需要注意的是 DOCTYPE
。包含文档类型定义(Document Type Definition,DTD)语句可以帮助将 WML 文档识别为 testwml.gsp。
注意该文件并未打包在常见的 <html>
标记中。它的开头和结尾为 <wml>
。您可能还注意到 清单 1 中缺少 <head>
和 <body>
部分。每一个 WML 页面为一个 card
,拥有一个独立的 id
属性和便于用户使用的 title
属性。
通常情况下,在一个单一文件中可以下载多个页面/卡片。早期的手机的数据通道非常狭窄,而这种方法刚好就缓和了这些设备及其网络的局限性。一次下载得越多,手机与服务器之间的数据转就越少。因为一次只能查看一个页面,这样就可以有效地提前获取其余的页面。对于这种情况,导航只发生在客户端。
HTML 开发人员一定要熟悉 <p>
标记。WML <anchor>
标记在本质上与 HTML <a>
标记是类似的,即使它们在语法上有所不同(参阅 参考资料 了解更多关于 WML 的信息)。
下面是 WML 的一个小技巧。由于处理的是专用于手机的内容,因此可以创建一个超链接,用户一旦选定链接,就可以拨出电话。清单 3 的中例子可以拨出电话号码 303-555-1212 :
<do type="accept"> <go href="wtai://wp/mc;3035551212"/> </do> |
注意该链接使用的协议并非常见的 http://
— 而是 wtai://
,这是无线电话应用程序界面(Wireless Telephony Applications Interface)的缩写。
要使这个页面在个人电脑上显示,则需要一个 WAP 仿真器(参阅 参考资料,查看文中提到的所有仿真器的链接)。访问 dotMobi 仿真器,它是一个 Java applet。输入 URL davisworld.org/testwml.gsp
(注意 http:// 前缀已提供在输入框的左侧),您将看到类似于图 1 的内容:
注意 dotMobi 仿真器有两种不同的皮肤,它不仅代表着不同设备的外观和感觉,还代表设备的不同功能。如果对仿真某个设备感兴趣的话,它的硬件制造商通常会提供一个开发者网站,您可以从哪里下载安装所需的仿真器。
第一个 WML 例子为静态代码。清单 4 是一个使用常见的 <g:each>
和 <g:if>
标记的例子:
<% response.setContentType("text/vnd.wap.wml") %> <% def flightList = [] flightList << [iata1:"DEN", iata2:"ORD"] flightList << [iata1:"ORD", iata2:"DEN"] %> <?xml version="1.0"?> <!DOCTYPE wml PUBLIC "-//PHONE.COM//DTD WML 1.1//EN" "http://www.phone.com/dtd/wml11.dtd" > <wml> <g:each in="${flightList}" var="${flight}" status="i"> <card id="f${i}" title="Flight ${i}"> <p mode="wrap">From: ${flight.iata1}</p> <p mode="wrap">To: ${flight.iata2}</p> <g:if test="${flightList.size() > i+1}"> <p> <anchor>Next<go href="#f${i}"/></anchor> </p> </g:if> </card> </g:each> </wml> |
注意我仅仅模仿了 HashMap 中的一些机载数据,而不是设置完整的 MVC 基础设施。重要的是它能保证了 GSP 标记与 WML 的结合,就像我在 “精通 Grails: 用 Groovy 服务器页面(GSP)改变视图” 中处理 JavaScript 一样(可以在 http://davisworld.org/testwml2.gsp. 中查看到这个页面的示例)。
虽然常有人断言 WML 的生命快到头了,但仅支持 WML 的手机仍在流通。没错!— WML 1.x 正在淡出江湖。越来越多的现代手机开始避免这种 “分离但平等的” WML 平台,转而使用真正的 Web 浏览器。正如下一节所演示的一样,要为 WML 2.x 设备或 iPhones 创建一个便于移动的浏览网站,只需在现存的 HTML 上做些变动,而不是将其转换成完全不同的标记语言。
结合使用 Grails 与 WML 2.X(或 XHTML-MP)
提到 WML 2.x,WML 更像是一个品牌的名称,而不是一个独立的标记语言(WML 1.x 才是)。事实上,WML 2.x 只不过是 XHTML 的一个方言:明确地说是 XHTML-MP。
XHTML-MP 严格要求创建格式良好的 XML。这就意味必须正确地关闭每一个容器标记(<p></p>
、<li></li>
),属性前后要用引号(<a href="http://somewhere.com">
),并且元素名称只能用小写字母(<h1>
而不是 <H1>
)。
XHTML-MP 是 XHTML-Basic 的一个超集。只要稍作调整,您的网站就遵循 XHTML-Basic 规范。XHTML-MP 不可以使用嵌套的表格或框架。它只支持 gif 和 png 图像格式。至于其他的最佳实践(比如指定图像大小和替换文本)则是 XHTML-Basic 的要求。很多常见的 HTML 标记(虽然不是全部)都可以找到。参阅 参考资料 获得可用于 XHTML-Basic 和 XHTML-MP 的标记的列表链接。
要优化网站使其适合较小屏幕,就必须减少针对每个请求发回的数据。Web 网页(包括 HTML、CSS 和图像)最好小于 20KB。并且要使用 Expires
或者 Cache-Control
header 来主动缓冲文件。当为移动设备提供内容时,要将网页分割为 2 到 3 页。http://m.cnn.com 在这方面做的就很好,它可以将整篇文章分成 3 到 4 页显示,但也提供了 “整篇文章” 的链接,如果您不介意额外开销的话。
就像使用 WML 1.x 一样,必须在文件的开始包含正确的 DTD。同时还要修改 <html>
标记,使其包含 xmlns
属性,如清单 5 所示:
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> ... </html> |
虽然很多移动设备也接受更加通用的 MIME 类型 application/xhtml+xml
,但您仍然要用适当的 MIME 类型 application/vnd.wap.xhtml+xml
将其发送。application/xhtml+xml
可以帮助您在标准桌面浏览器中调试代码。
访问 http://m.yahoo.com 可以查看 XHTML-MP。(虽然它在 Web 浏览其中看起来很简朴,但在手机上的效果却很好)。选择 View > Source,就会在文档顶端看见 XHTML-MP DTD 了。
想要更深入地了解这样的网站在真机上的效果到底如何,您还需要找到另外一个仿真器。例如,Sandip Chitale 的博客提供了 Firefox 插件,它看来真的很象一个 iPhone(参阅 参考资料)。注意,这个仿真器要比真机大一些,但是它显示的网站效果跟在 iPhone 上显示的效果非常接近。(我将指出一些更精确的验证器)。图 2 展示了用 Chitale 的仿真器仿真的 m.yahoo.com:
图 2. 使用 iPhone 仿真器查看 Yahoo 移动 web
有几个在线验证器可以确保您发出的 XHTML-MP 是格式良好的。您可以尝试一下 W3C mobileOK Basic Checker 或 ready.mobi 测试工具(参阅 参考资料)。二者都很好,但是 ready.mobi 的仿真器提供的信息要比 W3C 多得多。
例如,图 3 展示了 W3C 验证器所提供的关于 http://m.google.com 的信息:
图 3. W3C 验证器提供的有关 Google 的移动 web 的报告
图 4 为 ready.mobi 工具提供的关于 http://m.yahoo.com 的报告的前一部分:
图 4. ready.mobi 提供的关于 Yahoo 的移动 web 的报告
它显示了 Yahoo! 的 4/5。再往下拉一点点,您就可以看到很多不同的可视化器,使您可以看到网页的真正显示效果。图 5 展示了它在 Nokia N70 的效果:
在页面的底部,ready.mobi 验证器展示了一组详细的测试结果,每一个结果都带有这样的标记:通过(绿色)、失败(红色)或警告(黄色)。例如,即使 http://m.yahoo.com 好像在各种设备上都显示得不错,其 XHTML 也不是 100% 遵从的,如图 6 所示:
再往下看,如图 7 所示,您会看到 Yahoo! 在图像上遗漏了一些 alt
属性,而且在某些情况下没有指定图像的大小:
那么,Grails 已经可以直接用于开发移动 Web 了吗?图 8 展示了 ready.mobi 验证器提供的关于旅行计划应用程序的原始清单页面的信息:
那就是说,还需要做一些工作。首先,在 grails-app/controllers/AirportController.groovy 中创建一个 mlist
闭包。除了会返回 5 个(而不是 10 个)元素外,它与默认的闭包没什么不同。创建一个单独的闭包保留,您就可以原样保持 list.gsp,以进行比较,如清单 6 所示:
def mlist = { if(!params.max) params.max = 5 [airportList:Airport.list(params)] } |
现在将 grails-app/views/airport/list.gsp 复制到 mlist.gsp。(一会儿,我将提供一些策略,从而将移动用户无缝地重定向到正确的内容。这个强有力的方法目前还能满足需求)。
验证器指出网站未返回 XHTML-MP。编辑 mlist.gsp 使它在 <html>
标记中包含有必要的 DTD 和 xmlns
属性。您还需要禁用 meta
标记,因为它会自动将内容类型设置为 text/html
。最后一步:将 grails-app/views/layout/main.gsp 中包含 CSS 的行复制到该文件中。(SiteMesh — Grails 使用的模板库 — 配置为只在默认情况下修饰 text/html 文件)。清单 7 展示了 mlist.gsp:
<% response.setContentType("application/xhtml+xml")%> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/--> <link rel="stylesheet" href="${createLinkTo(dir:'css',file:'main.css')}" /> <meta name="layout" content="main" /> <title>Airport List</title> </head> ... </html> |
在编辑文件时,也可以简化表布局。验证器指出 <thead>
和 <tbody>
标记有误,所以需要将其删除。由于手机屏幕的纵向空间要比横向空间大,所以清单 8 所示的布局看起来会好一些:
<table> <tr> <g:sortableColumn property="id" title="Id" /> <g:sortableColumn property="name" title="Name" /> </tr> <g:each in="${airportList}" status="i" var="airport"> <tr class="${(i % 2) == 0 ? 'odd' : 'even'}"> <td> <g:link action="show" id="${airport.id}">${airport.id?.encodeAsHTML()} </g:link> </td> <td>${airport.iata?.encodeAsHTML()}<br/> ${airport.name?.encodeAsHTML()} </td> </tr> </g:each> </table> |
图 9 展示了新页面在 iPhone 仿真器中的显示效果:
图 10 展示了此时 ready.mobi 验证器所提供的信息:
这就好多了!而且针对验证器的必要更改是很少的。回顾 “精通 Grails: 用 Groovy 服务器页面(GSP)改变视图”,可以通过输入 grails install-templates
来相应地更改默认模板。
iPhone 可能就是三种类型的设备中最容易支持的设备。它页面的开发与普通的 Web 页面开发完全相同。iPhone 上的 Safari 浏览器与桌面浏览器的代码库完全相同,因此,用户在两者中所看到的东西是一样的。但是您可以去掉一些显示提示,因为通过 iPhone 查看网站时它们会影响外观和感觉。
例如,一个 iPhone 的屏幕尺寸为 320x480,但有趣的是,浏览器设置的网页的默认宽度为 980 像素。这使文本在手机的景色模式下不能读取,并且在肖像模式下会变得很小。但是不用担心,使用一个简单的只有 iPhone 才能识别的 meta
标记,就可以将网页校正到 “正确的尺寸” 了:viewport
标记允许为移动 Safari 浏览器添加提示。清单 9 中的代码就在很大程度上提高了在 iPhone 上查看的网页的可读性。(不幸的是,基于 Firefox 的 iPhone 仿真器无法识别这个 meta
标记。只有在真正的 iPhone 上才可以看到它的实际运行)。
<meta name="viewport" content="initial-scale=1.0" /> |
inital-scale
的范围为 0 到 10,且支持分数值。也可以输入显式的 width
和 height
值,上限为 10,000 像素(如清单 10 所示):
清单 10. 为 viewport
设置 width
和 height
<meta name="viewport" content="width=600;height=400" /> |
说到超链接,iPhone 提供了一些特殊的性能。如果使用 tel:
前缀代替 http://
,单击链接就可以拨出一个电话号码,如清单 11 所示:
<p> telephone number: <a href="tel:303-555-1212">303-555-1212</a> </p> |
如果使用传统的 mailto:
前缀的话,单击链接就会运行邮件应用程序,如清单 12 所示:
<p> mail: <a href="mailto:scott@aboutgroovy.com">Scott Davis</a> </p> |
如果您为 Google Map 提供一个链接,单击链接就会运行本地 Google Maps 应用程序,而不是将其转交到 Safari 中,如清单 13 所示:
<p> local google maps: <a href="http://maps.google.com/maps?q=denver+international+airport">DEN</a> </p> |
输入一个起点和终点,单击链接,它就会为用户提供驾驶方向,如清单 14 所示:
<p> driving directions: <a href="http://maps.google.com/maps?daddr= denver+airport&saddr=coors+field+denver,+co">Directions</a> </p> |
您已经知道为三种基本设备创建内容都需要什么工具,现在的任务是如何根据需要使用它们。有三种基本的策略可供选择。
正如前面看到的一样,m 是许多 Web 站点所采用的策略。Google、Yahoo! 以及 CNN,都设置了一个 m 域,它独立于主站点,用于提供移动内容。如果改动域名系统(Domain Name System,DNS)的话,可以创建一个类似 http://mysite.org/mobile 的 URL。您也可以注册一个移动内容专用的 .mobi 域。
每一个 Web 浏览器在请求数据时都向服务器表明身份。可以利用这个信息来提供为设备定制的内容。(http://twitter.com 使用的就是这种技术)。
访问 http://davisworld.org/echo.gsp。清单 15 中的页面只用了个简单的循环,就回应了 HTTP 的请求:
<h2>Request Headers</h2> <table border="1"> <tr> <th>Header</th> <th>Value</th> </tr> <g:each in="${request.headerNames}" var="${name}"> <tr> <td>${name}</td> <td>${request.getHeader(name)}</td> </tr> </g:each> </table> |
正如您在图 11 中所看到的,当我打开 http://davisworld.org/echo.gsp 时,Firefox 浏览器提供了足够身份提示:
根据图 11 中展示的 user-agent
字符串,就可以断定请求程序通过 Intel CPU 运行 Mac 系统。对于 OS(10.5)、HTML 呈现引擎(Gecko)、和真实浏览器(Firefox)的版本,您已经很熟悉了。清单 17 展示了其他常见的用户-代理字符串:
BlackBerry7520/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Browser/5.0.3.3 UP.Link/5.1.2.12 Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3 Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322) |
通过捕获 request.getHeader("user-agent")
值为请求浏览器提供适当的内容。
第三种策略就是满足浏览器的所有请求。每一个请求都会包含一个 accept
值和一个 user-agent
值。Firefox 返回的 accept
值如下:
text/html,application/xhtml+xml,application/xml; |
这告诉服务器 Firefox 偏好 text/html
。如果服务器不包含 text/html
数据,那么它可以发送 application/xhtml+xml
。如果两者都没有的话,服务器会在列表中逐个查找,直到找到一个可以返回的 MIME 类型。
WAP 1.x 浏览器需要 text/vnd.wap.wml
,更现代的手机会需要 application/vnd.wap.xhtml+xml
。只要多加注意,聪明的开发者便可以返回适当的数据。
当然,这三种策略并不是相互排斥的。您可以全部选用,确保您的网站已经准备好为全球 33 亿的手机用户提供服务了。
让 Grails 应用程序便于手机使用的方法有很多种,可以不做任何改动(对于 iPhone 而言)、做很小的改动(对于 XHTML-MP 设备而言)、或者全部重写(对于 WML 1.x 设备而言)。借助文中所介绍的这些仿真器和验证器,您一定可以实现顺利支持移动 Web。
在下一篇文章中,您将会学习如何处理 Grails 中的遗留数据库。您将学习 Mapping DSL、使用 Hibernate 注释和 HBM 文件。学会了这些技术,您就可以让 Grails 使用现有的表和字段名,即使它们不符合 Grails 标准命名规定。到时候就尽情享受精通 Grails 的带来的乐趣了。